目錄
前言
alloc
alloc核心操作
cls->instanceSize(extraBytes)
calloc
obj->initInstanceIsa
init
類方法:
實例方法:
new
前言
筆者最近在進行對OC語言源碼的學習,學習源碼的過程中經常會出現一些從來沒有遇見過的函數,因此很難把整個過程理解清楚,這篇博客先簡單梳理一下我現在理解的 alloc & init & new 的實現過程以及內存對齊原理
alloc
首先從main函數中找到WGPerson類的alloc方法的實現:
在這個方法中,調用了_objc_rootAlloc,跳轉到該函數的實現:
這個方法中又調用了callAlloc函數,同樣跳轉到該函數的實現:
這個函數是runtime中分配對象的核心方法之一,用于決定走哪條路徑調用alloc或allocWithZone:
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
這兩行是這個函數的定義部分,static說明這個函數只能在當前文件中使用,ALWAYS_INLINE表示這個函數應盡量內聯,提高性能。
函數有三個參數,cls表示要分配內存的類,checkNil是一個bool變量,用于表明是否需要檢查cls是否為nil,allocWithZone表示是否使用allocWithZone:方法。
函數內部實現里有三個判斷,第一個判斷是類是否為nil
#if __OBJC2__if (slowpath(checkNil && !cls)) return nil;
__OBJC2__表示僅在OC2.0環境下有效,<!--slowpath用于分支預測優化,提示這個條件大概率為假-->,checkNil && !cls 表示如果要求檢查且cls是nil,就直接返回nil。
?if (fastpath(!cls->ISA()->hasCustomAWZ())) {return _objc_rootAllocWithZone(cls, nil);}
#endif
<!--fastpath與slowpath相對,也是用于分支預測優化的,它提示這個條件大概率為真。-->cls->ISA()用于獲取類的元類,hasCustomAWZ()用于判斷類是否重寫了allocWithZone:這個方法,
進入條件語句后,_objc_rootAllocWithZone(cls, nil)是調用runtime的根分配方法 ,分配對象。
如果沒有進入這條快路徑,就只能走慢路徑,發消息調用alloc/allocWithZone:
if (allocWithZone) {return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);}
objc_msgSend:是runtime的消息發送函數,cls和@selector(allocWithZone:)是這個函數的兩個參數,這條語句前面的部分((id(*)(id, SEL, struct _NSZone *))是在對objc_msgSend:函數進行類型轉換,把函數轉換成了返回一個id,接收兩個參數:id,SEL的函數,這樣來正確調用alloc方法。
這條語句就相當于給類對象cls發送alloc消息,從而創建一個該類的實例對象。
最后一種情況是當allocWithZone為假時,正常調用 alloc走常規路徑,objc_msgSend: 發送
alloc消息,等效于
[cls alloc]
在這里我們對自定義類進行觀察,會在這幾個分支中走到_objc_rootAllocWithZone,接著我們跳轉到_objc_rootAllocWithZone的實現。
再繼續跳轉到_class_createInstanceFromZone的源碼實現,這個部分是alloc源碼的核心操作,實現主要分為三個部分:
-
cls->instanceSize:計算需要開辟的<!--內存空間大小-->
-
calloc:<!--申請內存-->,返回地址指針
-
obj->initInstanceIsa:將類與isa關聯
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,int construct_flags = OBJECT_CONSTRUCT_NONE,bool cxxConstruct = true,size_t *outAllocatedSize = nil)
{ASSERT(cls->isRealized());
?// Read class's info bits all at once for performancebool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();bool hasCxxDtor = cls->hasCxxDtor();bool fast = cls->canAllocNonpointer();size_t size;
?size = cls->instanceSize(extraBytes);if (outAllocatedSize) *outAllocatedSize = size;
?id obj;if (zone) {obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);} else {obj = (id)calloc(1, size);}if (slowpath(!obj)) {if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {return _objc_callBadAllocHandler(cls);}return nil;}
?if (!zone && fast) {obj->initInstanceIsa(cls, hasCxxDtor);} else {// Use raw pointer isa on the assumption that they might be// doing something weird with the zone or RR.obj->initIsa(cls);}
?if (fastpath(!hasCxxCtor)) {return obj;}
?construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;return object_cxxConstructFromClass(obj, cls, construct_flags);
}
alloc核心操作
cls->instanceSize(extraBytes)
instanceSize(extraBytes)的實現如下:
instanceSize的調用過程如下:
這里自定義類最終走到fastInstanceSize
在fastInstanceSize中,最終調用的是align16這個函數,在834源碼中使用是align16這個函數,在最新版xcode中會報紅。
但是可以看到在906源碼中其實也調用的是align16這個函數。這個函數的實現是一個16字節對齊算法。
這段算法的過程就是將原始的內存 與size_t(15)相加,得到一個數,將 size_t(15) 即 15進行~(取反)
操作,再將 這個數 與 15的取反結果 進行 &(與)
操作,最后的結果為 16的倍數,即內存的大小是以16
的倍數增加的
原理是因為內存必須比數據大,內存對齊的結果只能大于實際數據大小,而不能比它小。將數字加15,使數字大于等于比數據大小要大的最小的那個16的倍數,在和末四位為0的數字進行與操作,抹去后四位,去掉余數,變成16的倍數。
為什么需要內存對齊?
通常內存是由一個個字節組成的,cpu在存取數據時,并不是以字節為單位存儲,而是以
塊
為單位存取,塊的大小為內存存取力度。頻繁存取字節未對齊的數據,會極大降低cpu的性能,所以可以通過減少存取次數
來降低cpu的開銷
16字節對齊,是由于在一個對象中,第一個屬性
isa
占8
字節,當然一個對象肯定還有其他屬性,當無屬性時,會預留8字節,即16字節對齊,如果不預留,相當于這個對象的isa和其他對象的isa緊挨著,容易造成訪問混亂16字節對齊后,可以
加快CPU讀取速度
,同時使訪問更安全
,不會產生訪問混亂的情況
calloc
calloc函數用于申請內存并返回地址指針。
obj = (id)calloc(1, size);
這一行代碼就是在用計算出來的size獲取地址指針obj,此時地址還沒有與傳入的cls進行關聯。
obj->initInstanceIsa
這一步是在將類與isa關聯。內存申請好后,將傳入的類與已經申請好的內存進行關聯,而關聯的方式就是isa指針。關聯的流程如下:
在執行完initInstanceIsa
后,內存便與類關聯了起來。
綜上,alloc的核心操作就是三步:計算內存,申請內存,內存與類關聯。
init
init有兩種,一種是類的init,一種是對象的。
類方法:
+ (id)init {return (id)self;
}
這里的init是一個構造方法 ,是通過工廠設計(工廠方法模式),主要是用于給用戶提供構造方法入口。這里能使用id強轉的原因,主要還是因為 內存字節對齊后,可以使用類型強轉為你所需的類型
實例方法:
init實例方法會跳轉到_objc_rootInit方法,來看看它的實現
可以發現,函數返回的是傳入的self本身。
new
除了alloc與init,初始化還可以使用new方法
new其實就是調用了callAlloc函數(即alloc中分析的函數)以及init函數,因此就相當于[[ alloc] init]。
但是如果重寫了init方法做一些自定義操作,這時會在這個方法中調用[super init],這時不建議使用new進行初始化。