準備工作
下載 objc4-781 源碼葡幸,選擇mac電腦進行編譯臭蚁。
編譯源碼涧窒,可參考iOS-底層原理 03:objc4-781 源碼編譯 & 調(diào)試。
alloc 源碼探索
整體的源碼流程探索如下:
首先我們用xcode運行項目菩帝,建立好相關的符號斷點。
【第一步】根據(jù)main
函數(shù)的XXPersion
類的alloc
方法進入具體源碼的實現(xiàn)。
//alloc源碼分析
+(id)alloc{return_objc_rootAlloc(self);}
【第二步】跳進return_objc_rootAlloc()
方法查看源碼實現(xiàn)胁附。
//cls就是上面說的XXPersion類
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
//注意第一次會進入這個方法酒繁,調(diào)用callAlloc
id objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
【第三步】跳進callAlloc
方法查看源碼實現(xiàn)。
重磅提示 這里是核心方法
static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
注意:經(jīng)過調(diào)試發(fā)現(xiàn)先走objc_msgSend
方法控妻,向XXPersion
類發(fā)送alloc
消息然后走到了第一步alloc
方法州袒,然后執(zhí)行_objc_rootAlloc
方法,最后進入_objc_rootAllocWithZone
方法弓候。那么為什么這樣子走兩次呢郎哭?帶著疑問往下走,哈哈~~~~
//第二次進入菇存,調(diào)起`callAlloc`方法夸研,走到第三部的流程。
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
slowpath & fastpath
其中關于是slowpath
和fastpath
這里需要簡要說明下依鸥,這兩個都是objc源碼中定義的宏亥至,其定義如下:
//x很可能為真, fastpath 可以簡稱為 真值判斷
#define fastpath(x) (__builtin_expect(bool(x), 1))
//x很可能為假贱迟,slowpath 可以簡稱為 假值判斷
#define slowpath(x) (__builtin_expect(bool(x), 0))
其中的__builtin_expect
指令是由gcc
引入的姐扮,
1、目的:編譯器可以對代碼進行優(yōu)化衣吠,以減少指令跳轉(zhuǎn)帶來的性能下降茶敏。即性能優(yōu)化
2、作用:允許程序員將最有可能執(zhí)行的分支告訴編譯器缚俏。
3惊搏、指令的寫法為:__builtin_expect(EXP, N)
。表示 EXP==N
的概率很大忧换。
4恬惯、fastpath
定義中__builtin_expect((x),1)
表示x
的值為真的可能性更大;即 執(zhí)行if
里面語句的機會更大
5包雀、slowpath
定義中的__builtin_expect((x),0)
表示 x
的值為假的可能性更大宿崭。即執(zhí)行else
里面語句的機會更大
6、在日常的開發(fā)中才写,也可以通過設置來優(yōu)化編譯器葡兑,達到性能優(yōu)化的目的,設置的路徑為:Build Setting --> Optimization Level --> Debug -->
將None
改為 fastest
或者 smallest
cls->ISA()->hasCustomAWZ()
其中fastpath
中的 cls->ISA()->hasCustomAWZ()
表示判斷一個類是否有自定義的 +allocWithZone
實現(xiàn)赞草,這里通過斷點調(diào)試讹堤,是沒有自定義的實現(xiàn),所以會執(zhí)行到 if 里面的代碼厨疙,即走到_objc_rootAllocWithZone
洲守。
【第四步】進入_objc_rootAllocWithZone
方法,其源碼實現(xiàn)如下:
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
//zone 參數(shù)不再使用 類創(chuàng)建實例內(nèi)存空間
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
【第五步】進入_class_createInstanceFromZone
方法,這是alloc源碼的核心操作梗醇,大致可以分為一下的三部分:
-
cls->instanceSize
:計算需要開辟的內(nèi)存空間大小 -
calloc
:申請內(nèi)存知允,返回地址指針 -
obj->initInstanceIsa
:將 類 與 isa 關聯(lián)
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)// alloc 源碼 第五步
{
ASSERT(cls->isRealized()); //檢查是否已經(jīng)實現(xiàn)
// Read class's info bits all at once for performance
//一次性讀取類的位信息以提高性能
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
//計算需要開辟的內(nèi)存大小,傳入的extraBytes 為 0
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
//申請內(nèi)存
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
//將 cls類 與 obj指針(即isa) 關聯(lián)
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)
根據(jù)以上的源碼分析叙谨,得出以下的流程圖
重點:calloc方法
cls->instanceSize:計算所需內(nèi)存大小
開辟內(nèi)存大小過程如下:
如果fastpath為
true
温鸽,跳轉(zhuǎn)到instanceSize
方法:
ize_t instanceSize(size_t extraBytes) const {
//編譯器快速計算內(nèi)存大小
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
// 計算類中所有屬性的大小 + 額外的字節(jié)數(shù)0
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
//如果size 小于 16,最小取16
if (size < 16) size = 16;
return size;
}
然后跳轉(zhuǎn)到fastInstanceSize
手负,源碼實現(xiàn):
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
//Gcc的內(nèi)建函數(shù) __builtin_constant_p 用于判斷一個值是否為編譯時常數(shù)涤垫,如果參數(shù)EXP 的值是常數(shù),函數(shù)返回 1竟终,否則返回 0
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
//刪除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8個字節(jié)
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
然后跳轉(zhuǎn)到alight16
蝠猬,這是一個16進制對齊的算法
,源碼如下:
//16字節(jié)對齊算法
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
內(nèi)存對齊規(guī)則
每個特定平臺上的編譯器都有自己的默認“對齊系數(shù)”(也叫對齊模數(shù))统捶。程序員可以通過預編譯命令#pragma pack(n)榆芦,n=1,2,4,8,16來改變這一系數(shù),其中的n就是你要指定的“對齊系數(shù)”瘾境。
1.數(shù)據(jù)成員對齊規(guī)則:struct 或 union (以下統(tǒng)稱結(jié)構體)的數(shù)據(jù)成員歧杏,第一個數(shù)據(jù)成員A放在偏移為 0 的地方,以后每個數(shù)據(jù)成員B的偏移為(#pragma pack(指定的數(shù)n) 與 該數(shù)據(jù)成員(也就是 B)的自身長度中較小那個數(shù)的整數(shù)倍迷守,不夠整數(shù)倍的補齊。
2.數(shù)據(jù)成員為結(jié)構體:如果結(jié)構體的數(shù)據(jù)成員還為結(jié)構體旺入,則該數(shù)據(jù)成員的“自身長度”為其內(nèi)部最大元素的大小兑凿。(struct a 里存有 struct b,b 里有char,int,double等元素茵瘾,那 b “自身長度”為 8)
3.結(jié)構體的整體對齊規(guī)則:在數(shù)據(jù)成員按照上述第一步完成各自對齊之后礼华,結(jié)構體本身也要進行對齊。對齊會將結(jié)構體的大小調(diào)整為(#pragma pack(指定的數(shù)n) 與 結(jié)構體中的最大長度的數(shù)據(jù)成員中較小那個的整數(shù)倍拗秘,不夠的補齊圣絮。
為什么要進行16字節(jié)對齊
- 通常內(nèi)存是由一個個字節(jié)組成的,cpu在存取數(shù)據(jù)時雕旨,并不是以字節(jié)為單位存儲扮匠,而是以塊為單位存取,塊的大小為內(nèi)存存取力度凡涩。頻繁存取字節(jié)未對齊的數(shù)據(jù)棒搜,會極大降低cpu的性能,所以可以通過減少存取次數(shù)來降低cpu的開銷
- 16字節(jié)對齊活箕,是由于在一個對象中力麸,第一個屬性isa占8字節(jié),當然一個對象肯定還有其他屬性,當無屬性時克蚂,會預留8字節(jié)闺鲸,即16字節(jié)對齊,如果不預留埃叭,相當于這個對象的isa和其他對象的isa緊挨著翠拣,容易造成訪問混亂
- 16字節(jié)對齊后,可以加快CPU讀取速度游盲,同時使訪問更安全误墓,不會產(chǎn)生訪問混亂的情況。
16字節(jié)對齊算法的計算過程
- 首先將原始的內(nèi)存
8
與size_t(15)
相加益缎,得到8 + 15 = 23
- 將
size_t(15)
即15
進行~(取反)
操作谜慌,~(取反)
的規(guī)則是:1變?yōu)?,0變?yōu)?
- 最后將 23 與 15的取反結(jié)果 進行
&(與)
操作莺奔,&(與)
的規(guī)則是:都是1為1欣范,反之為0
,最后的結(jié)果為 16令哟,即內(nèi)存的大小是以16的倍數(shù)增加的
calloc:申請內(nèi)存恼琼,返回地址指針
通過instanceSIze計算內(nèi)存的大小,向內(nèi)存申請為size大小的內(nèi)存屏富,拿到賦值給obj晴竞,obj是指向內(nèi)存地址的指針。
obj = (id)calloc(1, size);
注意:未執(zhí)行calloc時候obj返回的是nil狠半,執(zhí)行完calloc之后返回一個16進制的地址噩死。在平時開發(fā)中,一個對象的打印的格式都是類似于這樣的<XXPerson: 0x01111111f>(是一個指針),而obj打印的是一個地址神年,只要是因為還沒有傳入cls的關聯(lián)已维,同時印證了alloc的根本作用就是開辟內(nèi)存。
obj->initInstanceIsa:類與isa關聯(lián)
當calloc完成之后已日,內(nèi)存空間就申請好了垛耳。然后就進行isa的關聯(lián),流程圖如下:
這個過程主要是初始化一個isa指針飘千,并將isa指針指向申請的內(nèi)存地址堂鲜,將指針與cls(XXPersion類)進行關聯(lián)。
總結(jié)
- 通過對alloc源碼的系統(tǒng)學習占婉,可以知道alloc主要目的就是
開辟內(nèi)存
泡嘴,開辟內(nèi)存需要16字節(jié)對齊算法
,開辟的內(nèi)存大小基本上都是16
的整數(shù)倍逆济。 - 開辟內(nèi)存的核心步驟:
計算------申請------關聯(lián)
酌予。
上面的疑點:為什么第三步會走兩次
通過llvm源碼
得知在編譯的過程中磺箕,alloc
方法被hook成上面說的objc_alloc
方法,這樣做的目的就是標記一個receiver抛虫,在標記完這個類為receiver之后都會進入普通的消息發(fā)送判斷(即第二次進入的alloc方法)
松靡,這樣做的目的其實就是間接符號的綁定
。