《OC底層系列一》-從alloc&init&New開始

前言

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

目錄

image.png

簡介

我們知道[[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)用流程如下:

image.png

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)目,觀察日志的輸出情況:


image.png

通過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)行綁定。


image.png

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


image.png

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

image.png

我們在修改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)行程序,打印日志如下:

image.png
  • 通過日志我們可以清晰的看到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的


isa&cls.gif
  • 可以看到,是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流程如下:


image.png

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)流程和作用

image.png

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é)

image.png
  • 本文探究了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ù)分享。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容