對象原理探究(一)

  • 2019 年 12 月 27 日
  • 筆記

一、定位源碼位置

我們要探究一個對象,那麼就要找到其屬性或者方法等所對應的源碼。首先,我來介紹三種探索源碼(即定位源碼位置)的方式。

示例程式碼:

LaVieLeader *leader1 = [LaVieLeader alloc];LaVieLeader *leader2 = [leader1 init];LaVieLeader *leader3 = [leader1 init];NSLog(@"%@, %@, %@", leader1, leader2, leader3);NSLog(@"%p, %p, %p", &leader1, &leader2, &leader3);

其列印結果:

2019-12-15 08:58:07.799176+0800 msg_send[1184:250725] <LaVieLeader: 0x281e403b0>, <LaVieLeader: 0x281e403b0>, <LaVieLeader: 0x281e403b0>

2019-12-15 08:58:07.799305+0800 msg_send[1184:250725] 0x16bdf0ee8, 0x16bdf0ee0, 0x16bdf0ed8

我們發現,leader1、leader2和leader3列印結果相同,但是&leader1、&leader2和&leader3卻不同。要知道為什麼,就要看其源碼。接下來我們就來探索一下alloc的源碼。

1,直接程式碼中下普通斷點

(1)

(2)Step Into Instruction

即摁住Control鍵,然後點擊如下符號

(3)最終會定位到libobjc.A.dylib`objc_alloc:

需要注意的是,我們要使用真機調試才會定位到libobjc.A.dylib`objc_alloc,而使用模擬機是不能進入到libobjc.A.dylib`objc_alloc的,因為真機是arm64,而模擬機是x86,二者是不一樣的。

2,下符號斷點

(1)

(2)Symbolic Breakpoint

下符號斷點

在Symbol裡面填寫符號標識,要定位哪個方法就填寫哪個方法名:

(3)下完符號斷點,在第一步的斷點處,直接點擊下一步,就會定位到libobjc.A.dylib`+[NSObject alloc]:

需要注意的是,第一步的斷點很重要,如果我們不在對應的位置加上普通斷點,而是直接加上第二步的符號斷點,那麼我們就不知道定位的是哪一個對象的alloc方法。

3,通過彙編

OC是一門高級語言,最終還是會轉換成能被機器識別的彙編語言。所以我們可以直接查看彙編源碼來定位。

(1)允許展示彙編源碼

(2)打一個普通斷點

(3)運行,就會定位到第二步所打斷點的彙編源碼處:

(4)找到打斷點方法(alloc)所對應的C方法(objc_alloc),然後打個斷點:

(5)Step Into Instruction(Control+Step Into)

最後會定位到libobjc.A.dylib`objc_alloc:

以上就是定位源碼位置的三種方式。

二、彙編源碼

前面,我們已經定位到了alloc的方法源碼是在libobjc.A.dylib中,接下來我們就要找到libobjc.A.dylib這個庫的源碼。

蘋果爸爸為我們開源了部分源碼,我們訪問如下網址就可以找到蘋果所有的開源程式碼:

https://opensource.apple.com/tarballs/

我們找到objc4/文件夾,然後下載最新的objc4-756.2即可。

後面的分析,都是基於objc4-756.2源碼。編譯運行之後,截圖如下:

由於我們是要找alloc方法的源碼,而每一個OC方法都是通過大括弧來實現的,所以我們全局搜索alloc {,結果如下:

然後我們一次點擊對應的方法和函數,就會找到alloc方法的調用線:

alloc->_objc_rootAlloc->callAlloc,最後會找到callAlloc函數:

callAlloc(Class cls, bool checkNil, bool allocWithZone=false){    if (slowpath(checkNil && !cls)) return nil;  #if __OBJC2__    if (fastpath(!cls->ISA()->hasCustomAWZ())) {        // No alloc/allocWithZone implementation. Go straight to the allocator.        // fixme store hasCustomAWZ in the non-meta class and         // add it to canAllocFast's summary        if (fastpath(cls->canAllocFast())) {            // No ctors, raw isa, etc. Go straight to the metal.            bool dtor = cls->hasCxxDtor();            id obj = (id)calloc(1, cls->bits.fastInstanceSize());            if (slowpath(!obj)) return callBadAllocHandler(cls);            obj->initInstanceIsa(cls, dtor);            return obj;        }        else {            // Has ctor or raw isa or something. Use the slower path.            id obj = class_createInstance(cls, 0);            if (slowpath(!obj)) return callBadAllocHandler(cls);            return obj;        }    }#endif      // No shortcuts available.    if (allocWithZone) return [cls allocWithZone:nil];    return [cls alloc];}

alloc的流程圖如下:

接下來我們按照上述方法函數調用線,來添加符號斷點,如下:

然後運行程式,就會依次定位到對應的符號斷點處。需要注意的是,我們先將排在後面的符號斷點給關掉,然後定位到前面的符號斷點處,再打開接下來的符號斷點,這樣的話才可以定位到我們所要研究的對象所調用的方法。

alloc方法的源碼實現如下:

+ (id)alloc {    return _objc_rootAlloc(self);}

所以我們將斷點定位到_objc_rootAlloc函數進行研究:

然後在控制台讀取暫存器(register read)

所謂的暫存器,就是存儲指針的容器。這裡的x0、x1、x2……等,是用於程式調用的參數傳遞。

需要特別注意一下x0。x0在暫存器中排在第一個,所以x0是第一個參數的傳遞者,但同時在返回的時候也是返回值的存儲地方

我們都知道,alloc的作用是給對象申請記憶體,那麼是如何實現的呢?使用彙編來分析確實是可以分析的,但是很難跟蹤,所以並不推薦大家使用。接下來我將給大家介紹一個簡潔的方法。

三、直接源碼編譯來分析

這個簡潔的方法就是直接編譯objc4源碼。這裡可以參考文章《iOS_objc4-756.2 最新源碼編譯調試》進行配置:

https://juejin.im/post/5d9c829df265da5ba46f49c9

好,配置完了之後,我們運行最新的objc4-756.2程式碼,我們定位alloc,最終會定位到如下程式碼:

if (!zone  &&  fast) {        obj = (id)calloc(1, size);        if (!obj) return nil;        obj->initInstanceIsa(cls, hasCxxDtor);    }

下面對該程式碼一一解說。

calloc是開闢一塊記憶體,該記憶體就是一個實例對象,但是此時該實例對象的記憶體空間還不能和任何的類對象產生關聯。

initInstanceIsa是初始化上面開闢出來的記憶體空間的isa指針,也就是將實例對象記憶體空間與其Class關聯起來

size指示應該為對象開闢多少大小的記憶體。

關於這個size,也就是給對象開闢的記憶體空間大小,有如下兩個結論:

1,size必須是8位元組的倍數,也就是8位元組對齊

之所以必須是8位元組的倍數,主要是為了方便CPU進行內容的讀取。我們設置了必須是8位元組的倍數的這個規定,那麼CPU就會以8位元組一個單位進行讀取操作,不然的話,它就不知道下一次讀取該讀取多大的記憶體,這勢必將影響CPU的讀取效率。但是這樣做有一個弊端,也就是會浪費部分記憶體空間。也就是說,我們是用空間換取時間

2,size最少是16位元組。這是為了預留出一些記憶體空間以應對特殊情況。

四、查看記憶體段的存儲

前面我們知道了,一個對象的記憶體大小是8位元組的倍數,我們接下來就來看看如何讀取對象的記憶體段。

在某處打好斷點,程式跑到該斷點處的時候,在編譯器輸出欄,進行如下輸入:

x leader1 的作用是以16進位列印leader1對象的地址空間

需要注意的一點是,這裡的0x282424470是棧頂指針(即isa)的起始位置,所以 po 0x282424470 的結果就是leader1所對應的類對象LaVieLeader。

還需要注意的一點是,直接通過x leader1列印出來的地址空間是iOS小端模式,也就是說,其地址空間是反的。

除了x leader1,其實我們還可以通過x/4xg leader2來讀取對象的記憶體空間:

x/4xg leader2的作用是:讀取leader2對象的前四個單位(每一個單位是8位元組)的記憶體空間

五、init的作用

上面我們知道了,alloc開闢記憶體空間;那麼init做了什麼呢?

id_objc_rootInit(id obj){    // In practice, it will be hard to rely on this function.    // Many classes do not properly chain -init calls.    return obj;}

通過源碼我們發現,系統的init實際上什麼都沒做。那麼init有什麼用呢?

init的作用主要是系統提供給我們一個介面,我們可以通過重寫該方法來初始化自定義的一些屬性。這其實是工廠模式的一種體現。

以上。