前言
我們在開發(fā)過程中,接觸最多的就是[[NSObjec alloc] init]或者[NSObject New]了,因此想要探究OC的底層原理改执,我們先從alloc&init&New入手,看看它們內(nèi)部是如何實現(xiàn)的。
目錄
簡介
我們知道[[NSObejct alloc] init]是創(chuàng)建了一個對象并初始化窘问,即申請為對象開辟申請一段內(nèi)存,初始化對象的一些屬性宜咒。所以我們在開始探究前拋出2個問題惠赫。
- 問題1:alloc內(nèi)部如何申請開辟內(nèi)存的?
- 問題2:alloc如何把申請的內(nèi)存空間指針和類進行關聯(lián)故黑?
探究思路
- 方式一:通過符號斷點跟蹤調(diào)試分析
- 方式二:通過閱讀源碼分析儿咱,因為從官方下載的objc源碼是無法編譯調(diào)試運行的
- 方式三:配置objc源碼庭砍,讓其可以編譯運行,通過運行可編譯的源碼結合demo進行調(diào)試分析
方式一比較麻煩混埠,局限性很大逗威,就不介紹了。
方式二能夠讓我們了解alloc的實現(xiàn)流程岔冀,但是OC底層源碼實現(xiàn)有很多的分支凯旭,具體會走哪些分支我們不確定,也存在一定的局限性使套。
方式三能夠就像我們學習一個三方庫一樣罐呼,通過配合斷點或者日志來快速了解一個功能的實現(xiàn)流程,但是如何配置呢侦高?官方下載的objc源碼不能運行時因為依賴其他庫的相關文件嫉柴,這些文件沒有不在我們下載的objc源碼里,需要我們自己找到依賴文件配置到項目中奉呛。具體配置不在不在這里描述计螺,這里提供了可編譯的objc4源碼:
objc4可編譯調(diào)試源碼項目
本文以objc4-750版本進行分析介紹。
我們可以先簡單大致閱讀一下alloc在objc源碼的實現(xiàn)瞧壮,然后運行demo結合斷點登馒、日志的方式來探究alloc的實現(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)部分支較多咆槽,會根據(jù)情況再調(diào)用不同的函數(shù)陈轿,這個我們先暫時不關注,后面會進行分析秦忿。
但實際是否如此麦射?我們來驗證一下:
- 解釋一下FHLog(@"[Person class] is %p ",[Person class]);里面的%p為什么對應[Person class],而不是&[Person class]灯谣,理解的可以跳過此處潜秋。
我們要打印的是Person類的地址,[Person class]返回的是一個Class胎许,即我們說的類峻呛,根據(jù)源碼
typedef struct objc_class *Class;
Class實際上就是struct objc_class*,是一個結構體指針
相當于把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),這樣大家就好理解了;
- 我們可以通過下斷點方式進行驗證呐萨,也可以通過Log方式驗證杀饵。我采用在關鍵路徑上添加Log,通過觀察Log來分析函數(shù)的調(diào)用流程谬擦。
修改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*/);
}
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
print_D("cls:%p,checkNil:%d,allocWithZone:%d",cls,checkNil,allocWithZone);
// 下面代碼暫時省略.....
}
然后我們運行項目,觀察日志的輸出情況:
通過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)最筒?
當我們調(diào)用一個OC方法,實際上就是發(fā)送一條消息SEL,而在系統(tǒng)會把SEL和真正的函數(shù)實現(xiàn)IMP進行關聯(lián)蔚叨。
由此又引申出其他的問題了床蜘,
- 問題3:那么SEL_alloc是在什么時候和IMP進行了綁定?
- 問題4:SEL_alloc如何實現(xiàn)和IMP_objc_alloc實現(xiàn)綁定蔑水?
通過全局搜索objc_alloc邢锯,一個個分析,發(fā)現(xiàn)在在objc-runtime-new.mm文件中有如下代碼
fixupMessageRef方法中有一個判斷
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)這里有相關的代碼把SEL_alloc和objc_alloc的函數(shù)地址進行了關聯(lián)搀别,于是添加相關日志丹擎,發(fā)現(xiàn)這里沒有打印,說明我們在運行程序的時候并沒有走到這里歇父。
那么可以推斷這里的fixupMessageRef不是在運行的時候執(zhí)行的蒂培,可能在程序編譯或者鏈接階段的時候就執(zhí)行了,從而完成了SEL和IMP的綁定操作榜苫。
全局搜索一下這個函數(shù)护戳,發(fā)現(xiàn)這個函數(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的綁定應該是在程序鏈接階段的時候就完成了灸异。
用MachOView打開我們剛剛編譯的工程文件可以看到
原因是系統(tǒng)做了符號綁定,alloc方法會關聯(lián)到一個名稱為''alloc"的SEL(消息)羔飞,而系統(tǒng)把SEL_alloc和真正的函數(shù)實現(xiàn)(IMP)&objc_alloc進行綁定。
所以[Person alloc]的實際的流程如下
不管從源碼的分析和我們實際運行得到的流程上來看檐春,目前的關鍵實現(xiàn)就在callAlloc函數(shù)逻淌,通過分析callAlloc源碼我們得到如下流程
我們在修改callAlloc函數(shù)添加相應的日志如下:
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];
}
然后運行程序,打印日志如下:
- 通過日志我們可以清晰的看到callAlloc函數(shù)內(nèi)部的執(zhí)行情況
- 第一次執(zhí)行callAlloc的時候疟暖,內(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骨望,完成了對callAlloc函數(shù)的第二次調(diào)用
- 分析callAlloc的第二次調(diào)用日志,調(diào)用了class_createInstance函數(shù)
id obj = class_createInstance(cls, 0);
return obj;
- 到這里說明class_createInstance是創(chuàng)建對象的關鍵欣舵,從命名上看這個函數(shù)創(chuàng)建了一個類的實例擎鸠,那么我們繼續(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)建一個類的實例袜蚕,繼續(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計算對象的內(nèi)存空間大小
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
// 2.根據(jù)計算的size為obj申請分配內(nèi)存
obj = (id)calloc(1, size);
if (!obj) return nil;
// 3.初始化對象的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ù)計算的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.初始化對象的isa指針,initIsa(cls)==>initIsa(cls, true, hasCxxDtor)
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
initInstanceIsa的實現(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)的實現(xiàn)
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
- 分析_class_createInstanceFromZone源碼绢涡,里面主要完成2件事:
1.計算對象所占內(nèi)存空間的大小并向系統(tǒng)申請分配內(nèi)存空間
size_t size = cls->instanceSize(extraBytes);
...
obj = (id)calloc(1, size);
2.初始化對象的isa
obj->initInstanceIsa(cls, hasCxxDtor);
這里我們斷點配合lldb命令來查看一下在執(zhí)行obj->initInstanceIsa前后的變化
obj的description的
可以看到牲剃,是obj->initInstanceIsa完成了申請的內(nèi)存空間和類(傳入的class)的關聯(lián)。
calloc函數(shù)是來自malloc庫雄可,功能是開辟內(nèi)存空間,相較于malloc函數(shù)凿傅,calloc函數(shù)會自動將內(nèi)存初始化為0。參考百度百科-calloc
現(xiàn)在我們對前面提到的4個問題進行總結:
問題1:alloc內(nèi)部如何申請開辟內(nèi)存的数苫?
答:通過前面的流程分析我們知道狭归,是在第二次調(diào)用callAlloc函數(shù)的時候,在callAlloc內(nèi)通過調(diào)用class_createInstance文判,在class_createInstance內(nèi)部調(diào)用并返回_class_createInstanceFromZone过椎,
在_class_createInstanceFromZone內(nèi)部通過calloc函數(shù)將為我們的結構體指針申請開辟了內(nèi)存。問題2:alloc如何把申請的內(nèi)存空間和類進行關聯(lián)戏仓?
答:在_class_createInstanceFromZone函數(shù)內(nèi)部申請開辟內(nèi)存后疚宇,通過調(diào)用obj的initInstanceIsa函數(shù),將傳入class和申請到的空間指針關聯(lián)到一起赏殃。問題3:SEL_alloc是在什么時候和IMP進行了綁定敷待?
答:是在程序鏈接階段,讀取鏡像文件的時候完成了綁定仁热。問題4:SEL_alloc如何實現(xiàn)和IMP_objc_alloc實現(xiàn)綁定榜揖?
答:在fixupMessageRef內(nèi)部里實現(xiàn)了綁定。fixupMessageRef內(nèi)部有一個判斷
if (msg->sel == SEL_alloc) {
msg->imp = (IMP)&objc_alloc;
}
完整的alloc流程如下:
init
init源碼實現(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;
}
- 結合前面的alloc分析抗蠢,alloc最終返回obj举哟,再結合init源碼,init內(nèi)部并沒有做其他的處理迅矛,直接把alloc后的obj返回妨猩。
- apple提供這么一個方法的意義在于,為提供一個接口讓子類根據(jù)自身情況進行相應的重寫init秽褒,可以理解是一種工廠設計壶硅。
概括一下alloc,init,New的實現(xiàn)流程和作用
New
New的實現(xiàn)如下:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
- 結合前面的alloc分析销斟,callAlloc最終返回obj庐椒,相比[[XXCls alloc] init]少調(diào)用了_objc_rootAlloc和[NSObject alloc]和一次callAlloc,然后調(diào)用再init蚂踊,減少了一些函數(shù)調(diào)用的開銷约谈,。
- 實際開發(fā)中考慮到可讀性和編碼規(guī)范,一般不會采用New的方式,大多采用alloc+init方式窗宇。
總結
- 本文探究了alloc&init&New的內(nèi)部實現(xiàn)流程進行了詳細的介紹措伐。
- alloc后返回了一個id類型的obj,這個就是我們創(chuàng)建的對象军俊。那么對象究竟是什么侥加?對象里有什么?cls->instanceSize是如何計算對象所占的內(nèi)存空間的粪躬?我會在下一篇繼續(xù)分享担败。