1.準(zhǔn)備源碼程序
源碼分析alloc&init&new的流程,使用從github上下載的LGCooci的源碼https://github.com/LGCooci/KCCbjc4_debug。
因?yàn)樵O(shè)備限制,我是基于818的源碼進(jìn)行學(xué)習(xí)。
從github下載完成后,在KCObjcBuild所在的目錄新建一個(gè)OC類命名為Person,Person類中什么都不寫。
在main.m中寫入如下代碼:
#import "Person.h"
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@"%@ - %p - %p",p1,p1,&p1);
NSLog(@"%@ - %p - %p",p2,p2,&p2);
NSLog(@"%@ - %p - %p",p3,p3,&p3);
選擇target為KCObjcBuild,就可以運(yùn)行了。

指針在棧上開辟的地址,指針是int類型,64位機(jī)上大小為8字節(jié)沒有問題。但是MAC上與iOS上開辟的順序有一點(diǎn)區(qū)別,在iOS上P1、P2、P3的地址依次減小8;Mac上卻是P1、P3、P2的順序依次減小8。
執(zhí)行的結(jié)果分析:
在OC中alloc用來分配內(nèi)存空間,init進(jìn)行初始化。上面代碼中只分配了一個(gè)Person對(duì)象的內(nèi)存空間,聲明了3個(gè)指針P1、P2、P3,都指向了同一個(gè)聲明的Person對(duì)象。
%@打印指針對(duì)象P1、P2、P3,打印的是他們所指向的對(duì)象,也就是那同一份Person對(duì)象,輸出<Person: 0x100698e90>
%p打印指針對(duì)象P1、P2、P3,%p是打印地址,P1、P2、P3指向?qū)ο蠖际?strong><Person: 0x100698e90>,打印的是Person對(duì)象的地址,所以打印的都是0x100698e90
%p打印指針&P1、&P2、&P3,P1、P2、P3是指針,&取地址符是指指針本身的地址,指針存放在棧上,所以打印的是它們?cè)跅I系牡刂贰?/p>

那么alloc和init具體做了什么呢?
2.用源碼分析alloc創(chuàng)建過程
2.1修復(fù)斷點(diǎn)不走問題
在Person *p1 = [Person alloc]那一行,打一個(gè)斷點(diǎn)。運(yùn)行,會(huì)出現(xiàn)斷點(diǎn)無(wú)效。
如果遇到斷點(diǎn)無(wú)效的問題,確保如下兩步是正確的:
- 在
Build Phases的Compile Sources中,將main.m拖到最前面 - 找到
Targets->Build Settings->Enable Hardened Runtime,值置為NO
2.2跟蹤源碼
直接step into跟蹤源碼。
2.2.1先走NSObject.mm的+ (id)alloc方法
+ (id)alloc {
return _objc_rootAlloc(self);
}
2.2.2再走NSObject.mm的id _objc_rootAlloc(Class cls)方法
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
2.2.3再走NSObject.mm的callAlloc方法
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
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
這個(gè)方法走的是上面那部分:
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
objective-c2.0是2006年蘋果發(fā)布版本,但是至此之后沒有再重新命名objective-c3.0,雖然官方的源代碼已經(jīng)以O(shè)BJC4命名,但是項(xiàng)目中依然是OBJC2。
出于好奇,到底OBJC2是什么玩意。沒有找到相關(guān)的宏定義。
看到代碼中有如下一處#if !defined(__cplusplus) && !__OBJC2__,便打印一下這其中到底是什么值:

宏
__cplusplus定義編程語(yǔ)言和編譯器之間的關(guān)系。
199711L(until C++11), 201103L(C++11), 201402L(C++14), 201703L(C++17), or 202002L(C++20)
宏UNAVAILABLE_ATTRIBUTE:告知方法失效。
關(guān)于fastpath和slowpath,雖然不重要,但是還是了解一下:
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
C++中的
__builtin_expect(),這個(gè)指令是gcc引入的,作用是"允許程序員將最有可能執(zhí)行的分支告訴編譯器"。這個(gè)指令的寫法為:__builtin_expect(EXP, N)。意思是:EXP==N的概率很大。__builtin_expect是為了生成高效的代碼。fastpath定義中
__builtin_expect((x),1)表示x 的值為真的可能性更大;即執(zhí)行if里面語(yǔ)句的機(jī)會(huì)更大。slowpath定義中的__builtin_expect((x),0)表示x 的值為假的可能性更大。即執(zhí)行else里面語(yǔ)句的機(jī)會(huì)更大
在日常的開發(fā)中,也可以通過設(shè)置來優(yōu)化編譯器,達(dá)到性能優(yōu)化的目的,設(shè)置的路徑為:
Build Setting--> Optimization Level--> Debug-->將None改為fastest或者smallest
2.2.4再走objc-runtime-new.mm的_objc_rootAllocWithZone方法
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
2.2.5再走objc-runtime-new.mm的_class_createInstanceFromZone方法
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 performance
bool 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);//將內(nèi)存空間與類關(guān)聯(lián)
} 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);
}
2.2.6 alloc重點(diǎn)方法分析
很明顯_class_createInstanceFromZone是我們要重點(diǎn)研究的對(duì)象,首先思考:
- 要開辟多少內(nèi)存
- 怎么取申請(qǐng)內(nèi)存
- ISA如何跟內(nèi)存進(jìn)行綁定
對(duì)應(yīng)在代碼中
-
cls->instanceSize:計(jì)算需要開辟的內(nèi)存空間大小 -
calloc:申請(qǐng)內(nèi)存,返回地址指針(Zone已經(jīng)廢棄) -
obj->initInstanceIsa:將類 與 isa 關(guān)聯(lián)
2.2.6.1 instanceSize分析
instanceSize方法在objc-runtime-new.h中,查看源碼,斷點(diǎn)調(diào)試
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
通過斷點(diǎn),會(huì)執(zhí)行到cache.fastInstanceSize方法,快速計(jì)算內(nèi)存大小。也就是執(zhí)行cache.fastInstanceSize(extraBytes),繼續(xù)跟進(jìn)去:
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
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
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
跟進(jìn)去后發(fā)現(xiàn)會(huì)走到第10行,align16(size + extra - FAST_CACHE_ALLOC_DELTA16);,繼續(xù)往下跟:
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
align16是16字節(jié)對(duì)齊算法。
內(nèi)存字節(jié)對(duì)齊原則
-
數(shù)據(jù)成員對(duì)齊規(guī)則:struct 或者 union 的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大?。ㄖ灰摮蓡T有子成員,比如數(shù)據(jù)、結(jié)構(gòu)體等)的整數(shù)倍開始(例如int在32位機(jī)中是4字節(jié),則要從4的整數(shù)倍地址開始存儲(chǔ)) -
數(shù)據(jù)成員為結(jié)構(gòu)體:如果一個(gè)結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(chǔ)(例如:struct a里面存有struct b,b里面有char、int、double等元素,則b應(yīng)該從8的整數(shù)倍開始存儲(chǔ)) -
結(jié)構(gòu)體的整體對(duì)齊規(guī)則:結(jié)構(gòu)體的總大小,即sizeof的結(jié)果,必須是其內(nèi)部做大成員的整數(shù)倍,不足的要補(bǔ)齊
為什么需要16字節(jié)對(duì)齊
- 各種數(shù)據(jù)類型長(zhǎng)短不一,需要統(tǒng)一長(zhǎng)度,在磁盤讀取時(shí),取出固定長(zhǎng)度數(shù)據(jù)。
2.2.6.2 calloc:申請(qǐng)內(nèi)存,返回匿名指針
通過instanceSize計(jì)算的內(nèi)存大小,向內(nèi)存中申請(qǐng) 大小 為 size的內(nèi)存,并賦值給obj,因此 obj是指向內(nèi)存地址的指針
2.2.6.3 obj->initInstanceIsa:類與isa關(guān)聯(lián)
經(jīng)過calloc可知,內(nèi)存已經(jīng)申請(qǐng)好了,類也已經(jīng)傳入進(jìn)來了,接下來就需要將 類與 地址指針 即isa指針進(jìn)行關(guān)聯(lián)。主要過程就是初始化一個(gè)isa指針,并將isa指針指向申請(qǐng)的內(nèi)存地址,在將指針與cls類進(jìn)行關(guān)聯(lián)。
2.2.6.4 總結(jié)
- 通過對(duì)
alloc源碼的分析,可以得知alloc的主要目的就是開辟內(nèi)存,而且開辟的內(nèi)存需要使用16字節(jié)對(duì)齊算法,現(xiàn)在開辟的內(nèi)存的大小基本上都是16的整數(shù)倍 - 開辟內(nèi)存的核心步驟有3步:
計(jì)算 -- 申請(qǐng) -- 關(guān)聯(lián)

2.2.7 init方法
工廠方法,用于重寫,自定義初始化內(nèi)容。
+ (id)init {
return (id)self;
}
- (id)init {
return _objc_rootInit(self);
}
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;
}
2.2.8 new方法
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
通過 new 的源碼可以看出,其實(shí)它也是調(diào)用了 [callAlloc init] 方法;
但是我們推薦使用 [alloc init] 方法,因?yàn)檫@樣我們可以自定義 init 方法,使我們的開發(fā)更加的靈活。
3.在打斷點(diǎn)后重新進(jìn)行分析
真正運(yùn)行的時(shí)候會(huì)發(fā)現(xiàn)在callAlloc方法會(huì)進(jìn)入兩次,先走objc_msgSend,再走_(dá)objc_rootAllocWithZone。這里涉及到另外一個(gè)知識(shí)點(diǎn),這個(gè)問題可以通過LLVM源碼分析得到答案,留在以后研究。