你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 深入解析 Objective

深入解析 Objective

編輯:IOS開發基礎

maxresdefault550.jpg

本文授權轉載,作者:左書祺(關注倉庫,及時獲得更新:iOS-Source-Code-Analyze)

因為 ObjC 的 runtime 只能在 Mac OS 下才能編譯,所以文章中的代碼都是在 Mac OS,也就是 x86_64 架構下運行的,對於在 arm64 中運行的代碼會特別說明。

在上一篇分析 isa 的文章《從 NSObject 的初始化了解 isa》中曾經說到過實例方法被調用時,會通過其持有 isa 指針尋找對應的類,然後在其中的 class_data_bits_t 中查找對應的方法,在這一篇文章中會介紹方法在 Objective-C 中是如何存儲方法的。

這篇文章的首先會根據 ObjC 源代碼來分析方法在內存中的存儲結構,然後在 lldb 調試器中一步一步驗證分析的正確性。

方法在內存中的位置

先來了解一下 ObjC 中類的結構圖:

objc-method-class.png

  • isa 是指向元類的指針,不了解元類的可以看:Classes and Metaclasses

  • super_class 指向當前類的父類

  • cache 用於緩存指針和 vtable,加速方法的調用

  • bits 就是存儲類的方法、屬性、遵循的協議等信息的地方

class_data_bits_t 結構體

這一小結會分析類結構體中的 class_data_bits_t bits。

下面就是 ObjC 中 class_data_bits_t 的結構體,其中只含有一個 64 位的 bits 用於存儲與類有關的信息:

objc-method-class-data-bits-t.png

在 objc_class 結構體中的注釋寫到 class_data_bits_t 相當於 class_rw_t 指針加上 rr/alloc 的標志。

class_data_bits_t bits;// class_rw_t * plus custom rr/alloc flags

它為我們提供了便捷方法用於返回其中的 class_rw_t * 指針:

class_rw_t* data() {
   return (class_rw_t *)(bits & FAST_DATA_MASK);
}

將 bits 與 FAST_DATA_MASK 進行位運算,只取其中的 [3, 47] 位轉換成 class_rw_t * 返回。

在 x86_64 架構上,Mac OS 只使用了其中的 47 位來為對象分配地址。而且由於地址要按字節在內存中按字節對齊,所以掩碼的後三位都是 0。

因為 class_rw_t * 指針只存於第 [3, 47] 位,所以可以使用最後三位來存儲關於當前類的其他信息:

objc-method-class_data_bits_t.jpg

#define FAST_IS_SWIFT           (1UL<<0)
#define FAST_HAS_DEFAULT_RR     (1UL<<1)
#define FAST_REQUIRES_RAW_ISA   (1UL<<2)
#define FAST_DATA_MASK          0x00007ffffffffff8UL
  • isSwift():FAST_IS_SWIFT 用於判斷 Swift 類

  • hasDefaultRR():FAST_HAS_DEFAULT_RR 當前類或者父類含有默認的 retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法

  • requiresRawIsa():FAST_REQUIRES_RAW_ISA 當前類的實例需要 raw isa

執行 class_data_bits_t 結構體中的 data() 方法或者調用 objc_class 中的 data() 方法會返回同一個 class_rw_t * 指針,因為 objc_class 中的方法只是對 class_data_bits_t 中對應方法的封裝。

// objc_class 中的 data() 方法
class_data_bits_t bits;
class_rw_t *data() {
   return bits.data();
}
// class_data_bits_t 中的 data() 方法
uintptr_t bits;
class_rw_t* data() {
   return (class_rw_t *)(bits & FAST_DATA_MASK);
}

class_rw_t 和 class_ro_t

ObjC 類中的屬性、方法還有遵循的協議等信息都保存在 class_rw_t 中:

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    Class firstSubclass;
    Class nextSiblingClass;
};

其中還有一個指向常量的指針 ro,其中存儲了當前類在編譯期就已經確定的屬性、方法以及遵循的協議。

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;
    const uint8_t * ivarLayout;
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

在編譯期間類的結構中的 class_data_bits_t *data 指向的是一個 class_ro_t * 指針:

objc-method-before-realize.jpg

然後在加載 ObjC 運行時的過程中在 realizeClass 方法中:

  • 從 class_data_bits_t 調用 data 方法,將結果從 class_rw_t 強制轉換為 class_ro_t 指針

  • 初始化一個 class_rw_t 結構體

  • 設置結構體 ro 的值以及 flag

  • 最後設置正確的 data。

const class_ro_t *ro = (const class_ro_t *)cls->data();
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);

下圖是 realizeClass 方法執行過後的類所占用內存的布局,你可以與上面調用方法前的內存布局對比以下,看有哪些更改:

objc-method-after-realize-class.jpg

但是,在這段代碼運行之後 class_rw_t 中的方法,屬性以及協議列表均為空。這時需要 realizeClass 調用 methodizeClass 方法來將類自己實現的方法(包括分類)、屬性和遵循的協議加載到 methods、 properties 和 protocols 列表中。

XXObject

下面,我們將分析一個類 XXObject 在運行時初始化過程中內存的更改,這是 XXObject 的接口與實現:

// XXObject.h 文件
#import @interface XXObject : NSObject
- (void)hello;
@end
// XXObject.m 文件
#import "XXObject.h"
@implementation XXObject
- (void)hello {
    NSLog(@"Hello");
}
@end

這段代碼是運行在 Mac OS X 10.11.3 (x86_64)版本中,而不是運行在 iPhone 模擬器或者真機上的,如果你在 iPhone 或者真機上運行,可能有一定差別。

objc-method-target.png

這是主程序的代碼:

#import #import "XXObject.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class cls = [XXObject class];
        NSLog(@"%p", cls);
    }
    return 0;
}

編譯後內存中類的結構

因為類在內存中的位置是編譯期就確定的,先運行一次代碼獲取 XXObject 在內存中的地址。

0x100001168

接下來,在整個 ObjC 運行時初始化之前,也就是 _objc_init 方法中加入一個斷點:

objc-method-after-compile.png

然後在 lldb 中輸入以下命令:

(lldb) p (objc_class *)0x100001168
(objc_class *) $0 = 0x0000000100001168
(lldb) p (class_data_bits_t *)0x100001188
(class_data_bits_t *) $1 = 0x0000000100001188
(lldb) p $1->data()
warning: could not load any Objective-C class information. This will significantly reduce the quality of type information available.
(class_rw_t *) $2 = 0x00000001000010e8
(lldb) p (class_ro_t *)$2 // 將 class_rw_t 強制轉化為 class_ro_t
(class_ro_t *) $3 = 0x00000001000010e8
(lldb) p *$3
(class_ro_t) $4 = {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000   name = 0x0000000100000f7a "XXObject"
  baseMethodList = 0x00000001000010c8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000   baseProperties = 0x0000000000000000
}

objc-method-lldb-print-before-realize.png

現在我們獲取了類經過編譯器處理後的只讀屬性 class_ro_t:

(class_ro_t) $4 = {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000   name = 0x0000000100000f7a "XXObject"
  baseMethodList = 0x00000001000010c8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000   baseProperties = 0x0000000000000000
}

可以看到這裡面只有 baseMethodList 和 name 是有值的,其它的 ivarLayout、 baseProtocols、 ivars、weakIvarLayout 和 baseProperties 都指向了空指針,因為類中沒有實例變量,協議以及屬性。所以這裡的結構體符合我們的預期。

通過下面的命令查看 baseMethodList 中的內容:

(lldb) p $4.baseMethodList
(method_list_t *) $5 = 0x00000001000010c8
(lldb) p $5->get(0)
(method_t) $6 = {
  name = "hello"
  types = 0x0000000100000fa4 "v16@0:8"
  imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13)
}
(lldb) p $5->get(1)
Assertion failed: (i < count), function get, file /Users/apple/Desktop/objc-runtime/runtime/objc-runtime-new.h, line 110.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
(lldb)

objc-method-lldb-print-method-list.png

使用 $5->get(0) 時,成功獲取到了 -[XXObject hello] 方法的結構體 method_t。而嘗試獲取下一個方法時,斷言提示我們當前類只有一個方法。

realizeClass

這篇文章中不會對 realizeClass 進行詳細的分析,該方法的主要作用是對類進行第一次初始化,其中包括:

  • 分配可讀寫數據空間

  • 返回真正的類結構

static Class realizeClass(Class cls)

上面就是這個方法的簽名,我們需要在這個方法中打一個條件斷點,來判斷當前類是否為 XXObject:

objc-method-lldb-breakpoint.png

這裡直接判斷兩個指針是否相等,而不使用 [NSStringFromClass(cls) isEqualToString:@"XXObject"] 是因為在這個時間點,這些方法都不能調用,在 ObjC 中沒有這些方法,所以只能通過判斷類指針是否相等的方式來確認當前類是 XXObject。

直接與指針比較是因為類在內存中的位置是編譯期確定的,只要代碼不改變,類在內存中的位置就會不變(已經說過很多遍了)。

objc-method-breakpoint-before-set-rw.png

這個斷點就設置在這裡,因為 XXObject 是一個正常的類,所以會走 else 分支分配可寫的類數據。

運行代碼時,因為每次都會判斷當前類指針是不是指向的 XXObject,所以會等一會才會進入斷點。

在這時打印類結構體中的 data 的值,發現其中的布局依舊是這樣的:

objc-method-before-realize (1).png

在運行完這段代碼之後:

objc-method-after-realize-breakpoint.png

我們再來打印類的結構:

(lldb) p (objc_class *)cls // 打印類指針
(objc_class *) $262 = 0x0000000100001168
(lldb) p (class_data_bits_t *)0x0000000100001188 // 在類指針上加 32 的 offset 打印 class_data_bits_t 指針
(class_data_bits_t *) $263 = 0x0000000100001188
(lldb) p *$263 // 訪問 class_data_bits_t 指針的內容
(class_data_bits_t) $264 = (bits = 4302315312)
(lldb) p $264.data() // 獲取 class_rw_t
(class_rw_t *) $265 = 0x0000000100701f30
(lldb) p *$265 // 訪問 class_rw_t 指針的內容,發現它的 ro 已經設置好了
(class_rw_t) $266 = {
  flags = 2148007936
  version = 0
  ro = 0x00000001000010e8
  methods = {
    list_array_tt = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  properties = {
    list_array_tt = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  protocols = {
    list_array_tt = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = nil
  demangledName = 0x0000000000000000 }
(lldb) p $266.ro // 獲取 class_ro_t 指針
(const class_ro_t *) $267 = 0x00000001000010e8
(lldb) p *$267 // 訪問 class_ro_t 指針的內容
(const class_ro_t) $268 = {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000   name = 0x0000000100000f7a "XXObject"
  baseMethodList = 0x00000001000010c8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000   baseProperties = 0x0000000000000000
}
(lldb) p $268.baseMethodList // 獲取基本方法列表
(method_list_t *const) $269 = 0x00000001000010c8
(lldb) p $269->get(0) // 訪問第一個方法
(method_t) $270 = {
  name = "hello"
  types = 0x0000000100000fa4 "v16@0:8"
  imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13)
}
(lldb) p $269->get(1) // 嘗試訪問第二個方法,越界
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
Assertion failed: (i < count), function get, file /Users/apple/Desktop/objc-runtime/runtime/objc-runtime-new.h, line 110.
(lldb)

objc-method-print-class-struct-after-realize.png

最後一個操作實在是截取不到了

const class_ro_t *ro = (const class_ro_t *)cls->data();
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);

在上述的代碼運行之後,類的只讀指針 class_ro_t 以及可讀寫指針 class_rw_t 都被正確的設置了。但是到這裡,其 class_rw_t 部分的方法等成員都指針均為空,這些會在 methodizeClass 中進行設置:

objc-method-after-methodizeClass.png

在這裡調用了 method_array_t 的 attachLists 方法,將 baseMethods 中的方法添加到 methods 數組之後。我們訪問 methods 才會獲取當前類的實例方法。

方法的結構

說了這麼多,到現在我們可以簡單看一下方法的結構,與類和對象一樣,方法在內存中也是一個結構體。

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

其中包含方法名,類型還有方法的實現指針IMP:

obj-method-struct.png

上面的 -[XXObject hello] 方法的結構體是這樣的:

name = "hello"
types = 0x0000000100000fa4 "v16@0:8"
imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13

方法的名字在這裡沒有什麼好說的。其中,方法的類型是一個非常奇怪的字符串 "v16@0:8" 這在 ObjC 中叫做類型編碼(Type Encoding),你可以看這篇官方文檔了解與類型編碼相關的信息。

對於方法的實現,lldb 為我們標注了方法在文件中實現的位置。

小結

在分析方法在內存中的位置時,筆者最開始一直在嘗試尋找只讀結構體 class_ro_t 中的 baseMethods 第一次設置的位置(了解類的方法是如何被加載的)。嘗試從 methodizeClass 方法一直向上找,直到 _obj_init 方法也沒有找到設置只讀區域的 baseMethods 的方法。

而且在 runtime 初始化之後,realizeClass 之前,從 class_data_bits_t 結構體中獲取的 class_rw_t 一直都是錯誤的,這個問題在最開始非常讓我困惑,直到後來在 realizeClass 中發現原來在這時並不是 class_rw_t 結構體,而是class_ro_t,才明白錯誤的原因。

後來突然想到類的一些方法、屬性和協議實在編譯期決定的(baseMethods 等成員以及類在內存中的位置都是編譯期決定的),才感覺到豁然開朗。

  • 類在內存中的位置是在編譯期間決定的,在之後修改代碼,也不會改變內存中的位置。

  • 類的方法、屬性以及協議在編譯期間存放到了“錯誤”的位置,直到 realizeClass 執行之後,才放到了 class_rw_t 指向的只讀區域 class_ro_t,這樣我們即可以在運行時為 class_rw_t 添加方法,也不會影響類的只讀結構。

  • 在 class_ro_t 中的屬性在運行期間就不能改變了,再添加方法時,會修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods,對於方法的添加會在之後的文章中分析。

參考資料

Classes and Metaclasses

Tagged Pointer

類型編碼

Type Encodings

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved