研究OC底層原理面褐,就應(yīng)該從最基本和最熟悉的開始,那就是對(duì)象的創(chuàng)建alloc
底層實(shí)現(xiàn)鞠苟。本文就我自己探索和學(xué)習(xí)到的alloc
實(shí)現(xiàn)進(jìn)行總結(jié)全跨,有問題請(qǐng)指出,大家一起交流探索成肘。
幾種源碼探索方法(建議真機(jī)環(huán)境)
- 打斷點(diǎn)卖局,control + ↓
- 打符號(hào)斷點(diǎn)(例如
alloc
) - 顯示匯編語言,Debug -> Debug Workflow -> Always Show Disassembly
- 下載源碼探索(本文源碼下載地址objc-750/objc-756.2和配置教程)
以上方法前三種方法可以探索出簡(jiǎn)單的流程方法順序双霍,只有下載源碼才能看到流程方法的具體實(shí)現(xiàn)吼驶。探索起來雖說比較枯燥無味,但是只要堅(jiān)持下去店煞,對(duì)于自己技術(shù)幫助還是很大的蟹演,當(dāng)你探索出結(jié)果也會(huì)有很大的成就感。
alloc流程源碼探索
流程
第一步
+ (id)alloc {
return _objc_rootAlloc(self);
}
第二步
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
第三步
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
//cls為空
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
//看下邊官方注釋 -->判斷是否自定義alloc/allocWithZone顷蟀,并且不是繼承于NSObject/NSProxy
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
//canAllocFast通過點(diǎn)進(jìn)去可以得出在arm64系統(tǒng)下酒请,canAllocFast一直返回false
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 {
//第三步之后,進(jìn)入這里
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
//此處打斷點(diǎn)鸣个,輸出一下obj羞反,發(fā)現(xiàn)是LGTeacher
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
-
hasCustomAWZ()
是用來判斷是否有alloc/allocWithZone
的imp
,以及是否有自定義allocWithZone
囤萤。 -
canAllocFast()
通過點(diǎn)進(jìn)去可以得出在arm64系統(tǒng)下昼窗,一直返回false。 -
hasCxxDtor()
是否有c++
析構(gòu)函數(shù)涛舍。
在if (slowpath(!obj)) return callBadAllocHandler(cls);
打斷點(diǎn)澄惊,輸出obj
對(duì)象,說明追蹤正確,如下圖:
第四步
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
第五步
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
//cls為空
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
//判斷class or superclass 是否有 .cxx_construct 方法實(shí)現(xiàn)
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
//判斷是否需要優(yōu)化的isa
bool fast = cls->canAllocNonpointer();
//instanceSize計(jì)算需要為對(duì)象開辟的空間
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
//第四步之后掸驱,進(jìn)入這里
//calloc是開辟內(nèi)存空間
obj = (id)calloc(1, size);
if (!obj) return nil;
//isa初始化
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
//isa初始化
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
-
canAllocNonpointer()
判斷是否需要優(yōu)化的isa肛搬。 -
instanceSize(extraBytes)
計(jì)算需要開辟的空間大小,extraBytes
等于0毕贼。 -
calloc
->malloc_zone_calloc
開辟內(nèi)存空間温赔。
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
//對(duì)象內(nèi)存地址至少分配16字節(jié)
//一個(gè)對(duì)象內(nèi)存都會(huì)分配16字節(jié),實(shí)際在64位系統(tǒng)下鬼癣,只使用了8字節(jié)陶贼;32位系統(tǒng)下,只使用了4字節(jié)
if (size < 16) size = 16;
return size;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize;
}
-
if (size < 16) size = 16;
對(duì)象內(nèi)存地址至少分配16字節(jié)待秃。一個(gè)對(duì)象內(nèi)存都會(huì)分配16字節(jié)骇窍,實(shí)際在64位系統(tǒng)下,只使用了8字節(jié)锥余;32位系統(tǒng)下,只使用了4字節(jié)痢掠。 - 此處
word_align()
方法用到了一個(gè)內(nèi)存的8字節(jié)對(duì)齊計(jì)算驱犹,保證返回的size大小是8的倍數(shù)。WORD_MASK
在arm64系統(tǒng)下等于7足画。假如傳入word_align()
的x = 10雄驹,計(jì)算過程:
x = 10 ; x + WORD_MASK = 17
0000 0111 //WORD_MASK二進(jìn)制(4 + 2 + 1)1111 1000 //~WORD_MASK二進(jìn)制
0001 0001 //17的二進(jìn)制:(16 + 1)
0001 0000 //(x + WORD_MASK) & ~WORD_MASK 二進(jìn)制(16)
所以return 16淹辞; 8的倍數(shù)
calloc開辟內(nèi)存空間
這里探索需要用到源碼libmalloc
源碼医舆,打開源碼直接調(diào)用calloc
方法,得出如下結(jié)果象缀,接下來就一步一步探索下去蔬将,看看如何得出此結(jié)果。
calloc
方法
void *
calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
進(jìn)入malloc_zone_calloc
方法
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
//malloc追蹤央星,DBG_FUNC_START 開始
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
//malloc檢查
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
//調(diào)用calloc
ptr = zone->calloc(zone, num_items, size);
//malloc 記錄器 三種類型
if (malloc_logger) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone, (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
//malloc追蹤霞怀,DBG_FUNC_END 結(jié)束
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
return ptr;
}
通過這段代碼可以看出系統(tǒng)在這里開啟了malloc
追蹤、malloc
監(jiān)測(cè)莉给、malloc
記錄(看我注釋)毙石。真正執(zhí)行開辟內(nèi)存的是ptr = zone->calloc(zone, num_items, size);
這句代碼。zone
通過->
調(diào)用calloc
颓遏,那么和屬性一樣我們可以打印zone->calloc
徐矩,結(jié)果如下:
default_zone_calloc
,找到default_zone_calloc
的實(shí)現(xiàn)叁幢,如下:
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
又碰到了zone->calloc
(頭大)滤灯,打斷點(diǎn)正好還能走到這里,說明探索的過程是正確的。沒辦法接著打印zone->calloc
力喷,得到nano_calloc
刽漂,搜到nano_calloc
的實(shí)現(xiàn)如下:
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
//異常情況
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
//異常情況-->失敗會(huì)走h(yuǎn)elper zone
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
可以看出沒有異常情況,會(huì)進(jìn)入_nano_malloc_check_clear
方法弟孟,打斷點(diǎn)進(jìn)入贝咙。
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
//加鹽處理,size在此處使用
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {
//都是一些異常情況處理拂募,代碼沒有粘貼
} else {
//正常情況
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
到了此處確實(shí)不知該如何進(jìn)行下去(一臉懵逼)庭猩。我們既然要探索size
是如何開辟到內(nèi)存的,那我們就跟蹤size
陈症,進(jìn)入segregated_size_to_fit
方法蔼水。(我不會(huì)告訴你這都是老師教的)
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
//40 + 16 - 1 >> 4
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
// << 4
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;
這句代碼翻譯出來就是
k = (40 + 16 - 1)>>4 //右移4位
slot_bytes = 55 << 4 //再左移4位
我們?cè)囍?jì)算一下。
k = 40 + 16 - 1 = 55
0011 0111 //k的二進(jìn)制
右移4位录肯,空的補(bǔ)0(>>4)
0000 0011
左移4位趴腋,空的補(bǔ)0(<<4)
0011 0000 //正好等于48
這里其實(shí)運(yùn)用了16字節(jié)內(nèi)存對(duì)齊,和上邊的8字節(jié)對(duì)齊有著異曲同工之妙(把~WORD_MASK
換成>>3论咏、<<3也可以實(shí)現(xiàn)8字節(jié)對(duì)齊)优炬。上邊的8字節(jié)對(duì)齊是為了保證每個(gè)對(duì)象占用8字節(jié)內(nèi)存(64位),16字節(jié)對(duì)齊則是保證了每個(gè)對(duì)象開辟了16字節(jié)的內(nèi)存厅贪。關(guān)于對(duì)象占用內(nèi)存和開辟內(nèi)存的關(guān)系可以看下 Cocoi老師的文章蠢护。
進(jìn)行到這里我們大致也就驗(yàn)證出來了,那些沒看懂可以以后慢慢探索养涮,現(xiàn)在我們至少了解了對(duì)象占用內(nèi)存和開辟內(nèi)存蘋果底層都是怎么實(shí)現(xiàn)的葵硕。
objc_alloc的探索
其實(shí)使用探索方法1和3的時(shí)候,我們都看到了objc_alloc
這個(gè)方法贯吓,那這個(gè)方法到底是干什么的呢懈凹。
全局搜索objc_alloc
,可以找到兩個(gè)方法悄谐。
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
// Calls [cls allocWithZone:nil].
id
objc_allocWithZone(Class cls)
{
return callAlloc(cls, true/*checkNil*/, true/*allocWithZone*/);
}
可以看出objc_alloc
也會(huì)調(diào)用callAlloc
蘸劈,只不過和_objc_rootAlloc
調(diào)用傳入的參數(shù)不同。在objc_alloc
打斷點(diǎn)調(diào)試得出objc_alloc
在_objc_rootAlloc
之前執(zhí)行尊沸,進(jìn)入callAlloc
執(zhí)行[cls alloc]
威沫,會(huì)調(diào)起alloc
。然后才是上邊總結(jié)的流程洼专。
那么objc_alloc
做了什么事情棒掠?全局搜索objc_alloc
,可以看到一個(gè)調(diào)用objc_alloc
的地方屁商,方法如下:
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == SEL_alloc) {
msg->imp = (IMP)&objc_alloc;
} else if (msg->sel == SEL_allocWithZone) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == SEL_retain) {
msg->imp = (IMP)&objc_retain;
} else if (msg->sel == SEL_release) {
msg->imp = (IMP)&objc_release;
} else if (msg->sel == SEL_autorelease) {
msg->imp = (IMP)&objc_autorelease;
} else {
msg->imp = &objc_msgSend_fixedup;
//下邊代碼省略
此處判斷sel
如果是SEL_alloc
(即alloc
)就把objc_alloc
綁定為他的IMP烟很。
我們接著尋找fixupMessageRef
方法颈墅,結(jié)果如下:
_read_images
是系統(tǒng)讀取鏡像文件,就是dyld進(jìn)行符號(hào)綁定的時(shí)候雾袱,由此我們可以猜想msg->imp = (IMP)&objc_alloc;
就是一個(gè)符號(hào)綁定的過程恤筛。那我們驗(yàn)證一下,編譯一下工程芹橡,把目錄Products下的可執(zhí)行文件用MachOView(鏈接:https://pan.baidu.com/s/10k5BFJueUpz_ccBS54DQHQ 密碼:eadw)打開毒坛,可以看到下圖:至此也就驗(yàn)證了我們的猜想。
init&new探索
init
init
流程方法
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.(事實(shí)上林说,很難依靠此方法)
// Many classes do not properly chain -init calls.(很多類并未實(shí)現(xiàn)此方法)
return obj;
}
可以看出init
源碼直接返回obj
煎殷,其實(shí)init
方法更多的是為了開發(fā)者自定義類使用的,在實(shí)際中我們自定義類都會(huì)重寫init腿箩,把自定義的內(nèi)容寫在init
方法內(nèi)豪直,這個(gè)時(shí)候init
才真正發(fā)揮了作用。創(chuàng)建對(duì)象時(shí)調(diào)用init
珠移,是為了防止有自定義內(nèi)容弓乙。
new
new
流程方法
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
new
方法是調(diào)用了callAlloc
&init
方法,其實(shí)和alloc
&init
是一樣的钧惧。
總結(jié)
alloc流程圖努力就有收獲暇韧,想要在開發(fā)的路上走遠(yuǎn),就要不斷地更新自己的知識(shí)庫(kù)垢乙!