前言
我們在開發(fā)過程中,接觸最多的就是[[NSObjec alloc] init]或者[NSObject New]了,因此想要探究OC的底層原理,我們先從alloc&init&New入手,看看它們內(nèi)部是如何實(shí)現(xiàn)的。
目錄

簡介
我們知道[[NSObejct alloc] init]是創(chuàng)建了一個(gè)對(duì)象并初始化,即申請為對(duì)象開辟申請一段內(nèi)存,初始化對(duì)象的一些屬性。所以我們在開始探究前拋出2個(gè)問題。
- 問題1:alloc內(nèi)部如何申請開辟內(nèi)存的?
- 問題2:alloc如何把申請的內(nèi)存空間指針和類進(jìn)行關(guān)聯(lián)?
探究思路
- 方式一:通過符號(hào)斷點(diǎn)跟蹤調(diào)試分析
- 方式二:通過閱讀源碼分析,因?yàn)閺墓俜较螺d的objc源碼是無法編譯調(diào)試運(yùn)行的
- 方式三:配置objc源碼,讓其可以編譯運(yùn)行,通過運(yùn)行可編譯的源碼結(jié)合demo進(jìn)行調(diào)試分析
方式一比較麻煩,局限性很大,就不介紹了。
方式二能夠讓我們了解alloc的實(shí)現(xiàn)流程,但是OC底層源碼實(shí)現(xiàn)有很多的分支,具體會(huì)走哪些分支我們不確定,也存在一定的局限性。
方式三能夠就像我們學(xué)習(xí)一個(gè)三方庫一樣,通過配合斷點(diǎn)或者日志來快速了解一個(gè)功能的實(shí)現(xiàn)流程,但是如何配置呢?官方下載的objc源碼不能運(yùn)行時(shí)因?yàn)橐蕾嚻渌麕斓南嚓P(guān)文件,這些文件沒有不在我們下載的objc源碼里,需要我們自己找到依賴文件配置到項(xiàng)目中。具體配置不在不在這里描述,這里提供了可編譯的objc4源碼:
objc4可編譯調(diào)試源碼項(xiàng)目
本文以objc4-750版本進(jìn)行分析介紹。
我們可以先簡單大致閱讀一下alloc在objc源碼的實(shí)現(xiàn),然后運(yùn)行demo結(jié)合斷點(diǎn)、日志的方式來探究alloc的實(shí)現(xiàn)流程。
Person.h:
@interface Person : NSObject
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, assign) NSUInteger height;
@property (nonatomic, assign) NSUInteger weight;
@end
在main.m添加:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
Person *person = [Person alloc];
FHLog(@"[Person class] is %p ",[Person class]);
}
return 0;
}
NSObject.mm
+ (id)alloc {
print_D("self:%p",self);
return _objc_rootAlloc(self);
}
id
_objc_rootAlloc(Class cls)
{
print_D("cls:%p",cls);
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
通過閱讀源碼我們發(fā)現(xiàn)alloc的主體函數(shù)調(diào)用流程如下:

callAlloc內(nèi)部分支較多,會(huì)根據(jù)情況再調(diào)用不同的函數(shù),這個(gè)我們先暫時(shí)不關(guān)注,后面會(huì)進(jìn)行分析。
但實(shí)際是否如此?我們來驗(yàn)證一下:
- 解釋一下FHLog(@"[Person class] is %p ",[Person class]);里面的%p為什么對(duì)應(yīng)[Person class],而不是&[Person class],理解的可以跳過此處。
我們要打印的是Person類的地址,[Person class]返回的是一個(gè)Class,即我們說的類,根據(jù)源碼
typedef struct objc_class *Class;
Class實(shí)際上就是struct objc_class*,是一個(gè)結(jié)構(gòu)體指針
相當(dāng)于把struct objc_class *p = [Person class];分解為
struct objc_class personClass = [Person class];
struct objc_class *p = &personClass;;
根據(jù)上面我們可以把FHLog(@"[Person class] is %p ",[Person class]);替換為
FHLog(@"[Person class] is %p ",&personClass),這樣大家就好理解了;
- 我們可以通過下斷點(diǎn)方式進(jìn)行驗(yàn)證,也可以通過Log方式驗(yàn)證。我采用在關(guān)鍵路徑上添加Log,通過觀察Log來分析函數(shù)的調(diào)用流程。
修改NSObject.mm文件,在相關(guān)方法入口添加日志”
+ (id)alloc {
print_D("self:%p",self);
return _objc_rootAlloc(self);
}
id
_objc_rootAlloc(Class cls)
{
print_D("cls:%p",cls);
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
print_D("cls:%p,checkNil:%d,allocWithZone:%d",cls,checkNil,allocWithZone);
// 下面代碼暫時(shí)省略.....
}
然后我們運(yùn)行項(xiàng)目,觀察日志的輸出情況:

通過log分析,發(fā)現(xiàn)和我們從源碼閱讀分析的不一致,是按照[Person alloc]->objc_alloc->callAlloc->[NSObject alloc]->_objc_rootAlloc->callAlloc順序調(diào)用。
那么問題來了,為什么在[Person alloc]后沒有調(diào)用+ (id)alloc,而先走的是id
objc_alloc(Class cls)?
當(dāng)我們調(diào)用一個(gè)OC方法,實(shí)際上就是發(fā)送一條消息SEL,而在系統(tǒng)會(huì)把SEL和真正的函數(shù)實(shí)現(xiàn)IMP進(jìn)行關(guān)聯(lián)。
由此又引申出其他的問題了,
- 問題3:那么SEL_alloc是在什么時(shí)候和IMP進(jìn)行了綁定?
- 問題4:SEL_alloc如何實(shí)現(xiàn)和IMP_objc_alloc實(shí)現(xiàn)綁定?
通過全局搜索objc_alloc,一個(gè)個(gè)分析,發(fā)現(xiàn)在在objc-runtime-new.mm文件中有如下代碼
fixupMessageRef方法中有一個(gè)判斷
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;
}
...
/**
* 以下代碼省略
*/
}
}
發(fā)現(xiàn)這里有相關(guān)的代碼把SEL_alloc和objc_alloc的函數(shù)地址進(jìn)行了關(guān)聯(lián),于是添加相關(guān)日志,發(fā)現(xiàn)這里沒有打印,說明我們在運(yùn)行程序的時(shí)候并沒有走到這里。
那么可以推斷這里的fixupMessageRef不是在運(yùn)行的時(shí)候執(zhí)行的,可能在程序編譯或者鏈接階段的時(shí)候就執(zhí)行了,從而完成了SEL和IMP的綁定操作。
全局搜索一下這個(gè)函數(shù),發(fā)現(xiàn)這個(gè)函數(shù)的調(diào)用是在objc-runtime-new.mm文件(2624行處)的_read_images里,通過閱讀方法注釋
/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked
* list beginning with headerList.
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
可以知道是在程序鏈接階段執(zhí)行的,所以我們推斷SEL和IMP的綁定應(yīng)該是在程序鏈接階段的時(shí)候就完成了。
用MachOView打開我們剛剛編譯的工程文件可以看到
原因是系統(tǒng)做了符號(hào)綁定,alloc方法會(huì)關(guān)聯(lián)到一個(gè)名稱為''alloc"的SEL(消息),而系統(tǒng)把SEL_alloc和真正的函數(shù)實(shí)現(xiàn)(IMP)&objc_alloc進(jìn)行綁定。

所以[Person alloc]的實(shí)際的流程如下

不管從源碼的分析和我們實(shí)際運(yùn)行得到的流程上來看,目前的關(guān)鍵實(shí)現(xiàn)就在callAlloc函數(shù),通過分析callAlloc源碼我們得到如下流程

我們在修改callAlloc函數(shù)添加相應(yīng)的日志如下:
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
print_D("cls:%p,checkNil:%d,allocWithZone:%d",cls,checkNil,allocWithZone);
if (slowpath(checkNil && !cls)) {
print_D("cls:%p,checkNil:%d,allocWithZone:%d,return nil",cls,checkNil,allocWithZone);
return nil;
};
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
print_D("cls:%p,!cls->ISA()->hasCustomAWZ()) is true",cls);
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
print_D("cls:%p,cls->canAllocFast()) is true",cls);
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
print_D("cls:%p,cls->canAllocFast()) is true,call calloc(1, cls->bits.fastInstanceSize());",cls);
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) {
print_D("cls:%p,obj is null,call callBadAllocHandler(cls)",cls);
return callBadAllocHandler(cls);
};
print_D("cls:%p,obj is not nill,call obj->initInstanceIsa(cls, dtor),then return obj",cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
print_D("cls:%p,cls->canAllocFast() is false,call class_createInstance(cls, 0)",cls);
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) {
print_D("cls:%p,cls->canAllocFast() is false,obj is null,call callBadAllocHandler(cls)",cls);
return callBadAllocHandler(cls);
}
print_D("cls:%p,cls->canAllocFast() is false,obj is not null,return obj",cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) {
print_D("cls:%p,allocWithZone is true,call [cls allocWithZone:nil]",cls);
return [cls allocWithZone:nil];
}
print_D("cls:%p,allocWithZone is false,call [cls alloc]",cls);
return [cls alloc];
}
然后運(yùn)行程序,打印日志如下:

- 通過日志我們可以清晰的看到callAlloc函數(shù)內(nèi)部的執(zhí)行情況
- 第一次執(zhí)行callAlloc的時(shí)候,內(nèi)部只調(diào)用了[cls alloc],從而調(diào)用NSObject的+(id)alloc方法,接下來是_objc_rootAlloc
- _objc_rootAlloc內(nèi)部調(diào)用callAlloc函數(shù),傳入checkNil:false,allocWithZone:true,完成了對(duì)callAlloc函數(shù)的第二次調(diào)用
- 分析callAlloc的第二次調(diào)用日志,調(diào)用了class_createInstance函數(shù)
id obj = class_createInstance(cls, 0);
return obj;
- 到這里說明class_createInstance是創(chuàng)建對(duì)象的關(guān)鍵,從命名上看這個(gè)函數(shù)創(chuàng)建了一個(gè)類的實(shí)例,那么我們繼續(xù)探究class_createInstance內(nèi)部做了什么。
id
class_createInstance(Class cls, size_t extraBytes)
{
print_D("cls:%p",cls);
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
class_createInstance內(nèi)部調(diào)用_class_createInstanceFromZone,_class_createInstanceFromZone顧名思義,從空間創(chuàng)建一個(gè)類的實(shí)例,繼續(xù)看_class_createInstanceFromZone,我在方法里添加了一些注釋
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
// 1.根據(jù)extraBytes計(jì)算對(duì)象的內(nèi)存空間大小
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
// 2.根據(jù)計(jì)算的size為obj申請分配內(nèi)存
obj = (id)calloc(1, size);
if (!obj) return nil;
// 3.初始化對(duì)象的isa指針,obj->initInstanceIsa(cls, hasCxxDtor)<==>initIsa(cls, true, hasCxxDtor);
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
// 2.根據(jù)計(jì)算的size為obj申請分配內(nèi)存
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.
// 3.初始化對(duì)象的isa指針,initIsa(cls)==>initIsa(cls, true, hasCxxDtor)
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
initInstanceIsa的實(shí)現(xiàn)
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
assert(!cls->instancesRequireRawIsa());
assert(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
obj->initIsa(cls)的實(shí)現(xiàn)
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
- 分析_class_createInstanceFromZone源碼,里面主要完成2件事:
1.計(jì)算對(duì)象所占內(nèi)存空間的大小并向系統(tǒng)申請分配內(nèi)存空間
size_t size = cls->instanceSize(extraBytes);
...
obj = (id)calloc(1, size);
2.初始化對(duì)象的isa
obj->initInstanceIsa(cls, hasCxxDtor);
這里我們斷點(diǎn)配合lldb命令來查看一下在執(zhí)行obj->initInstanceIsa前后的變化
obj的description的

可以看到,是obj->initInstanceIsa完成了申請的內(nèi)存空間和類(傳入的class)的關(guān)聯(lián)。
calloc函數(shù)是來自malloc庫,功能是開辟內(nèi)存空間,相較于malloc函數(shù),calloc函數(shù)會(huì)自動(dòng)將內(nèi)存初始化為0。參考百度百科-calloc
現(xiàn)在我們對(duì)前面提到的4個(gè)問題進(jìn)行總結(jié):
問題1:alloc內(nèi)部如何申請開辟內(nèi)存的?
答:通過前面的流程分析我們知道,是在第二次調(diào)用callAlloc函數(shù)的時(shí)候,在callAlloc內(nèi)通過調(diào)用class_createInstance,在class_createInstance內(nèi)部調(diào)用并返回_class_createInstanceFromZone,
在_class_createInstanceFromZone內(nèi)部通過calloc函數(shù)將為我們的結(jié)構(gòu)體指針申請開辟了內(nèi)存。問題2:alloc如何把申請的內(nèi)存空間和類進(jìn)行關(guān)聯(lián)?
答:在_class_createInstanceFromZone函數(shù)內(nèi)部申請開辟內(nèi)存后,通過調(diào)用obj的initInstanceIsa函數(shù),將傳入class和申請到的空間指針關(guān)聯(lián)到一起。問題3:SEL_alloc是在什么時(shí)候和IMP進(jìn)行了綁定?
答:是在程序鏈接階段,讀取鏡像文件的時(shí)候完成了綁定。問題4:SEL_alloc如何實(shí)現(xiàn)和IMP_objc_alloc實(shí)現(xiàn)綁定?
答:在fixupMessageRef內(nèi)部里實(shí)現(xiàn)了綁定。fixupMessageRef內(nèi)部有一個(gè)判斷
if (msg->sel == SEL_alloc) {
msg->imp = (IMP)&objc_alloc;
}
完整的alloc流程如下:

init
init源碼實(shí)現(xiàn):
- (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;
}
- 結(jié)合前面的alloc分析,alloc最終返回obj,再結(jié)合init源碼,init內(nèi)部并沒有做其他的處理,直接把a(bǔ)lloc后的obj返回。
- apple提供這么一個(gè)方法的意義在于,為提供一個(gè)接口讓子類根據(jù)自身情況進(jìn)行相應(yīng)的重寫init,可以理解是一種工廠設(shè)計(jì)。
概括一下alloc,init,New的實(shí)現(xiàn)流程和作用

New
New的實(shí)現(xiàn)如下:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
- 結(jié)合前面的alloc分析,callAlloc最終返回obj,相比[[XXCls alloc] init]少調(diào)用了_objc_rootAlloc和[NSObject alloc]和一次callAlloc,然后調(diào)用再init,減少了一些函數(shù)調(diào)用的開銷,。
- 實(shí)際開發(fā)中考慮到可讀性和編碼規(guī)范,一般不會(huì)采用New的方式,大多采用alloc+init方式。
總結(jié)

- 本文探究了alloc&init&New的內(nèi)部實(shí)現(xiàn)流程進(jìn)行了詳細(xì)的介紹。
- alloc后返回了一個(gè)id類型的obj,這個(gè)就是我們創(chuàng)建的對(duì)象。那么對(duì)象究竟是什么?對(duì)象里有什么?cls->instanceSize是如何計(jì)算對(duì)象所占的內(nèi)存空間的?我會(huì)在下一篇繼續(xù)分享。