OC底層探索之對象原理(上)

OC對象的創(chuàng)建

我們經(jīng)常使用allocinit,new 來創(chuàng)建對象,它們有什么區(qū)別呢

直接上代碼

LGPerson *p1 = [LGPerson alloc];
LGPerson *p2 = [p1 init];
LGPerson *p3 = [p1 init];
        
NSLog(@"p1---%@ - %p - %p", p1, p1, &p1);
NSLog(@"p2---%@ - %p - %p", p2, p2, &p2);
NSLog(@"p3---%@ - %p - %p", p3, p3, &p3);

打印結(jié)果如下:


打印結(jié)果

首先解釋一下

  • %@打印的是對象
  • %p打印的指針地址
  • %p - &p打印的是指向?qū)ο髢?nèi)存的指針地址
    由此可見
  • 第一列: p1,p2,p3相等,對象都是0x100b27a40
  • 第二列:p1,p2p3相等,對象的指針地址都是0x100b27a40,指向同一內(nèi)存空間,說明了我們創(chuàng)建對象其實只用alloc就可以了,不用再init。
  • 第三列: &p1, &p2, &p3 不相等, 打印的是指針地址, obj1、obj2、 obj3三個不同指針, 地址不同

alloc探索

為什么創(chuàng)建對象只用alloc,不用再init,就可以創(chuàng)建一個對象?那我們就基于objc4-838(macOS 12.3 Xcode 13.2)進行探索一下,看系統(tǒng)具體是怎樣實現(xiàn)一個對象的創(chuàng)建的。

下載objc4-838之后打開工程,選擇SXObjcDebug工程


選擇工程

打開main文件,點擊[LGPerson alloc]方法


點擊alloc方法

進入[NSObject alloc]方法


[NSObject alloc]函數(shù)

依次進去,并打上相應(yīng)流程斷點
_objc_rootAlloc函數(shù)
callAlloc函數(shù)
_objc_rootAllocWithZone函數(shù)
_class_createInstanceFromZone函數(shù)

好了,我們開始運行項目,需要注意的是運行項目之前需要把打得斷點取消掉,因為main函數(shù)之前系統(tǒng)也會創(chuàng)建很多的對象,我們只需要研究[LGPerson alloc]這個函數(shù)的具體實現(xiàn)就可以了。
首先定位到[LGPerson alloc]這行代碼,恢復(fù)剛打得斷點。


定位[LGPerson alloc]

具體的調(diào)用流程依次是這樣的


alloc調(diào)用流程

我們發(fā)現(xiàn)先走了這段代碼callAlloc里面的
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));給這個class發(fā)了方法為alloc的一個消息,
打開函數(shù)調(diào)用棧,發(fā)現(xiàn)有一個objc_alloc函數(shù),很顯然是之前沒用調(diào)用[NSObject alloc]這個方法的
WeChate5b8d7e9f91d943474bbb61480375816.png

搜索源碼直接打上個斷點,再運行,發(fā)現(xiàn)首先運行了objc_alloc方法,然后依次執(zhí)行


image.png

image.png

alloc


image.png

_objc_rootAlloc
image.png

callAlloc -> _objc_rootAllocWithZone
image.png

_objc_rootAllocWithZone->_class_createInstanceFromZone


image.png

_class_createInstanceFromZone
image.png

這樣就總結(jié)出來了底層alloc的調(diào)用流程
alloc的調(diào)用流程.png

使用符號斷點探索alloc
添加符號斷點,依次加入objc_alloc, callAlloc, [NSObject alloc],_objc_rootAlloc, _objc_rootAllocWithZone, _class_createInstanceFromZone等。注意main函數(shù)之前生成的對象也會走相應(yīng)的方法,要先把這些斷點disable,斷點到LGPerson *p1 = [LGPerson alloc];


image.png

需要注意的是alloc方法是一個類方法,加斷點的是需要把類名加上,如[NSObject alloc]


[NSObject alloc]斷點.png

開跑,函數(shù)調(diào)用順序如下
objc_alloc

callAlloc

alloc

_objc_rootAlloc.png

callAlloc.png

_objc_rootAllocWithZone.png

_class_creatInstanceFromZone.png

可以看到_class_creatInstanceFromZone方法內(nèi)部是返回的是一個obj的也就是一個NSObject對象

init方法

打開源碼,init函數(shù)返回來_objc_rootInit,_objc_rootInit返回obj,看得出來,init方法沒有進行任何操作,這一點蘋果用到了工廠模式,當(dāng)我們需要自定義某個類時,可以通過重寫init方法來進行自定義操作。


init函數(shù)

_objc_rootInit函數(shù)

編譯器優(yōu)化

編譯器優(yōu)化說明

  • -O0 關(guān)閉所有優(yōu)化 代碼空間大,執(zhí)行效率低
  • -O1 基本優(yōu)化等級 編譯器在不花費太多編譯時間基礎(chǔ)上,試圖生成更快、更小的代碼
  • -O2 O1的升級版,推薦的優(yōu)化級別 編譯器試圖提高代碼性能,而不會增大體積和占用太多編譯時間
  • -O3 最危險的優(yōu)化等級 會延長代碼編譯時間,生成更大體積、更耗內(nèi)存的二進制文件,大大增加編譯失敗的幾率和不可預(yù)知的程序行為,得不償失
  • -Og O1基礎(chǔ)上,去掉了那些影響調(diào)試的優(yōu)化 如果最終是為了調(diào)試程序,可以使用這個參數(shù)。不過光有這個參數(shù)也是不行的,這個參數(shù)只是告訴編譯器,編譯后的代碼不要影響調(diào)試,但調(diào)試信息的生成還是靠 -g 參數(shù)的
  • -Os O2基礎(chǔ)上,進一步優(yōu)化代碼尺寸 去掉了那些會導(dǎo)致最終可執(zhí)行程序增大的優(yōu)化,如果想要更小的可執(zhí)行程序,可選擇這個參數(shù)。
  • -Ofast 優(yōu)化到破壞標(biāo)準(zhǔn)合規(guī)性的點(等效于-O3 -ffast-math ) 是在 -O3 的基礎(chǔ)上,添加了一些非常規(guī)優(yōu)化,這些優(yōu)化是通過打破一些國際標(biāo)準(zhǔn)(比如一些數(shù)學(xué)函數(shù)的實現(xiàn)標(biāo)準(zhǔn))來實現(xiàn)的,所以一般不推薦使用該參數(shù)。

舉個例子

int sum (int a , int b) {
    return a + b;
}

在main函數(shù)里面調(diào)用該sum函數(shù),使用----分割sum函數(shù)調(diào)用前后。


編譯器優(yōu)化示例代碼

運行打開匯編模式,我們會看到3和4的值


編譯器優(yōu)化示例代碼

打開Build Setting在Debug模式下系統(tǒng)默認(rèn)是O0是不優(yōu)化的
修改優(yōu)化等級

修改Debug和Release模式的一樣的優(yōu)化等級Os


image.png

再運行一次,發(fā)現(xiàn)編譯器把sum函數(shù)也優(yōu)化掉了

編譯器優(yōu)化之后

對象的內(nèi)存對齊方式

alloc探索時 _class_createInstanceFromZone返回一個NSObject對象,我們看到這里面有一個instanceSize函數(shù),它是給對象計算需要的內(nèi)存空間的。


instanceSize函數(shù)

cmd+instanceSize點擊 依次進入


對象需要內(nèi)存空間

對象需要內(nèi)存空間

看到了這個內(nèi)存對其算法,在64位的架構(gòu)下WORD_MASK為7,在31位的架構(gòu)下WORD_MASK為3。

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

在64位架構(gòu)下,假設(shè)一個對象實際內(nèi)存是10,即x=10,
x + WORD_MASK = 10 + 7 = 17 = 10001
~WORD_MASK = 17~7 = 11000
(x + WORD_MASK) & ~WORD_MASK = 10001 & 11000 = 10000 = 16

也可以
(x + WORD_MASK) >> 3 << 3 = 10001 >> 3 << 3 = 10 << 3 = 10000 = 16
所以這個對象實際需要的內(nèi)存大小就是16,蘋果是以8字節(jié)對齊來計算對象的內(nèi)存大小的。這樣就可以減少CPU的開銷,以空間換取時間。

結(jié)構(gòu)體內(nèi)存對齊

原則:

  • 數(shù)據(jù)成員對?規(guī)則:結(jié)構(gòu)(struct)的第一個數(shù)據(jù)成員放
    在offset為0的地方,以后每個數(shù)據(jù)成員存儲的起始位置要從
    該成員大小或者成員的子成員大小的整數(shù)倍開始(比如int為4
    字節(jié),則要從4的整數(shù)倍地址開始存儲)。
  • 結(jié)構(gòu)體作為成員:如果一個結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)
    構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存
    儲.(struct a里存有struct b,b里有char,int ,double
    等元素,那b應(yīng)該從8的整數(shù)倍開始存儲)。
  • 收尾工作:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果必須是
    其內(nèi)部最大成員的整數(shù)倍,不足的要補?。
    數(shù)據(jù)類型占的字節(jié)數(shù):


    數(shù)據(jù)類型占的字節(jié)數(shù).png

舉例:
double:8字節(jié) 從0位置開始需要8位,目前到7號位置
char:1字節(jié) 從8號位置開始存儲,需要1字節(jié),到8號位置
int:4字節(jié) 從9號位置開始存儲,需要4字節(jié),9不是4的倍數(shù),所以從12號位置開始存儲,到15位置
short:2字節(jié) 從16號位置開始存儲,需要2字節(jié),到17號位置
因為該結(jié)構(gòu)體內(nèi)的成員最大是double是字節(jié),17不是8的倍數(shù),所以需要32字節(jié)的內(nèi)存。

struct LGStruct1 {
    double a; // 8字節(jié) [0 7] 從0到7
    char b; // 1字節(jié) [8]
    int c; // 4字節(jié) [12 13 14 15]
    short d; // 2字節(jié) [16 17] 所以需要 24字節(jié)的內(nèi)存
}struct1;

驗證一下,打印結(jié)果是24,正確


驗證struct1結(jié)構(gòu)體內(nèi)存

調(diào)整一下結(jié)構(gòu)體內(nèi)部成員的順序
double:8字節(jié) 從0位置開始需要8位,目前到7號位置
int:4字節(jié) 從8號位置開始存儲,需要4字節(jié),到11位置
char:1字節(jié) 從12號位置開始存儲,需要1字節(jié),到12號位置
short:2字節(jié) 從13號位置開始存儲,需要2字節(jié),13不是2的倍數(shù),所以從14號位置開始存儲,到15位置
因為該結(jié)構(gòu)體內(nèi)的成員最大是double是字節(jié),15不是8的倍數(shù),所以需要16字節(jié)的內(nèi)存

struct LGStruct2 {
    double a; // 8字節(jié) [0 7] 從0到7
    int b; // 4字節(jié) [8 9 10 11]
    char c; // 1字節(jié) [12]
    short d; // 2字節(jié) [14 15] 所以需要 16字節(jié)的內(nèi)存
}struct2;

驗證一下,打印結(jié)果是16,正確


驗證struct2結(jié)構(gòu)體內(nèi)存

結(jié)構(gòu)體內(nèi)嵌結(jié)構(gòu)體,分析LGStruct3需要64字節(jié)

struct LGStruct3 {
    double a; // 8字節(jié) [0 7] 從0到7
    int b; // 4字節(jié) [8 9 10 11]
    char c; // 1字節(jié) [12]
    short d; // 2字節(jié) [14 15]
    int e; // 4字節(jié) [16 17 18 19]
    struct LGStruct1 struct1; // 24字節(jié) [24 47]
    struct LGStruct2 struct2; // 16字節(jié) [48 63]
}struct3;

驗證一下,打印結(jié)果是64,perfect


驗證struct3結(jié)構(gòu)體內(nèi)存

\color{red}{ Debug Code Debug The World }

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容