alloc&init 探索

alloc&init 探索

  • 首先要明確alloc做了什么,init做了什么。
#import "ViewController.h"
#import "Person.h"


@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    
    Person *p1 = [Person alloc];
    Person *p2 = [p1 init];
    Person *p3 = [p1 init];
    
    SSNSLog(@"%@",p1);
    SSNSLog(@"%@",p2);
    SSNSLog(@"%@",p3);
}


@end

上方的p1/p2/p3經(jīng)打印是一模一樣的

截屏2021-03-22 下午2.15.39.png

分別打印p1/p2/p3的地址是否相同?

截屏2021-03-22 下午2.25.53.png

分別打印&p1/&p2/&p3的地址是否相同?

截屏2021-03-22 下午2.26.54.png
  • 總結(jié):為什么會(huì)出現(xiàn)這種情況?
    因?yàn)閕nit并沒有對(duì)alloc出來的內(nèi)存空間的指針進(jìn)行任何修改,p2/p3的指針都是指向這塊內(nèi)存空間,這是為什么第二個(gè)是相等的。其實(shí)<Person: 0x600000b94040> 中冒號(hào)后面的0x600000b94040 就是指向的地址。
    下圖示意:


    截屏2021-03-22 下午2.36.02.png
  • 那么alloc是怎么開辟內(nèi)存空間的呢,這里就需要看底層的實(shí)現(xiàn)了。這里記錄三種方法去看

1.下符號(hào)斷點(diǎn)的形式(Symbolic BreakPoint)跟流程,配合objc源碼看源碼;

2.通過摁住control-step into跟流程,配合objc源碼;

3.匯編(Denug->Debug Workflow->Always Show Disassembly)查看跟流程。(最常用)

這里貼出蘋果開源源碼地址: https://opensource.apple.com

這個(gè)地址?的更直接 https://opensource.apple.com/tarballs/

  • 我們可以在源碼地址直接下載源碼工程直接設(shè)置一個(gè)走讀的工程,這樣就可以直接打斷點(diǎn)看源碼了。這塊的資料有很多就不再寫了。我這里在github上防止一個(gè)配置好的objc4-781的工程,有需要的可以下載。
+ (id)alloc {
    return _objc_rootAlloc(self);
}

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

// Replaced by CF (throws an NSException)
+ (id)init {
    return (id)self;
}

上面可以看到alloc調(diào)用的 _objc_rootAlloc 方法,接著往下找_objc_rootAlloc方法。

// 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*/);
}

可以看到_objc_rootAlloc去調(diào)用了callAlloc方法。接著向下看

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
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));
}

上面我們看到callAlloc可能走兩個(gè)方法:_objc_rootAllocWithZone 和 objc_msgSend。我們先看_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);
}

可以看到_objc_rootAllocWithZone直接調(diào)用的_class_createInstanceFromZone,我們已經(jīng)走到了關(guān)鍵點(diǎ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)
{
    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;

    //1.計(jì)算需要的內(nèi)存空間大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 2.向系統(tǒng)申請開辟內(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) {
        //3.關(guān)聯(lián)到相應(yīng)的類
        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);
}

這里_class_createInstanceFromZone 有三個(gè)關(guān)鍵的步驟instanceSize 和calloc和initInstanceIsa 作用備注了一下

首先看instanceSize方法

    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;
    }

這面是cache.fastInstanceSize

  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);
          //                16      0         8
        }
    }

上面size=16 ;extra = 0 ; FAST_CACHE_ALLOC_DELTA16 = 8 最后16+0-8 = 8

#define FAST_CACHE_ALLOC_MASK         0x1ff8
#define FAST_CACHE_ALLOC_MASK16       0x1ff0
#define FAST_CACHE_ALLOC_DELTA16      0x0008

再看下align16的實(shí)現(xiàn)

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15); //16字節(jié)對(duì)齊
}

這個(gè)函數(shù)明顯是16字節(jié)對(duì)齊

    0000 0000  0001 0111       8+15 = 23
&  1111   1111     1111   0000   ->15取反 

相與后得到:  0000 0000  0001   0000   即16后面的都抹零,最后是16的倍數(shù)

  • 所以,instanceSize作用是計(jì)算需要的內(nèi)存空間大小。

現(xiàn)在去看calloc的實(shí)現(xiàn),打斷點(diǎn)po一下obj,打印出來是nil

截屏2021-03-22 下午5.44.11.png

走完calloc的實(shí)現(xiàn),打斷點(diǎn)po一下obj,打印出來是一個(gè)地址,現(xiàn)在已經(jīng)開辟了內(nèi)存了。但是只是一個(gè)指針,還沒有關(guān)聯(lián)到Person類

截屏2021-03-22 下午5.48.09.png

下面走到initInstanceIsa,把指針和類關(guān)聯(lián)起來

截屏2021-03-22 下午5.53.09.png
  • 所以alloc的流程圖為:
截屏2021-03-22 下午6.08.03.png
  • 從上面的結(jié)論看alloc已經(jīng)完成了開辟空間并與Person類關(guān)聯(lián)的工作,那么init的作用是什么呢?貼一下init的源碼實(shí)現(xiàn)
+ (id)init {
    return (id)self;
}

這是重寫init的構(gòu)造方法,工廠設(shè)計(jì),給用戶提供相應(yīng)的入口

  • 然后new的作用是什么呢?
+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

可以看到調(diào)用了alloc流程中的callAlloc流程,它把類對(duì)象也傳了進(jìn)去(Self),相當(dāng)于調(diào)用了alloc+init

  • 在這里回頭看下_class_createInstanceFromZone 中的 size = cls->instanceSize(extraBytes); 當(dāng)時(shí)走到這里的時(shí)候,看到大小是16,那么什么可以影響這個(gè)size大小呢?再把源碼貼過來
    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);
          //                16      0         8
        }
    }

在這里我們先給Person增加一個(gè)name屬性,再次走到這個(gè)方法看下size大小,可以看到仍然是16


截屏2021-03-23 上午10.29.42.png

然后我們再給Person增加一個(gè)nickName屬性,再次走到這個(gè)方法看下size大小,可以看到已經(jīng)變成了32


截屏2021-03-23 上午10.33.48.png
  • 這里是字節(jié)對(duì)齊,所以對(duì)對(duì)象來說影響size大小的因素是屬性,這里 8+8+8 = 24 然后字節(jié)對(duì)齊后是 32
  • 今天聽說[NSObject alloc]的流程的問題,跟了一下,附在這個(gè)后面,我是這樣做的,在791的源碼工程里main.m文件里
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSObject *objc1 = [NSObject alloc];
        Person *objc2 = [Person alloc];
     
   
        NSLog(@"Hello, World!  %@",objc2);
    }
    return 0;
}

打上斷點(diǎn),下圖:


截屏2021-03-24 上午11.00.18.png
截屏2021-03-24 上午11.00.30.png

結(jié)果發(fā)生了,在main函數(shù)的14行直接走了第15行,并沒有走+ (id)alloc。然后發(fā)現(xiàn)走的是objc_alloc函數(shù)

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

然后進(jìn)入callAlloc 走 _objc_rootAllocWithZone 函數(shù)

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));
}
  • 而[Person alloc] 的流程不是這樣的,而是先后走了兩次 callAlloc 函數(shù)
    下面寫下順序:
  1. 先走objc_alloc函數(shù)
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
  1. 在callAlloc 函數(shù)中走的最后一行

return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))

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));
}

這句去調(diào)用了alloc 函數(shù),之后走的時(shí)候上面的alloc的流程。這才是一個(gè)最完整的流程

+ (id)alloc {
    return _objc_rootAlloc(self);
}
  • 所以為什么走兩次呢?這里的分歧點(diǎn)在callAlloc 函數(shù)里 !cls->ISA()->hasCustomAWZ() 結(jié)果的不同導(dǎo)致的,在第一次進(jìn)去callAlloc時(shí),取true調(diào)用_objc_rootAllocWithZone,第二次取得值為false,走的最后((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)), 去調(diào)用了alloc。這里大部分都寫的是系統(tǒng)自己走的第一遍,沒有過多研究。其實(shí)這里跟isa密切相關(guān)。

首先研究下ISA()這個(gè)函數(shù),貼出源碼

inline Class 
objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

這里我們打斷點(diǎn)走一下,在開始開始打印下isa 可以看到

截屏2021-03-24 下午1.35.17.png

然后向下走發(fā)現(xiàn)走的是最后一句return (Class)(isa.bits & ISA_MASK); 這句返回的是什么呢?ISA_MASK 查看宏定義是 0x00007ffffffffff8ULL , 這里我特別看了下NSobject 和 Person的不同,如果是NSobject 打印的是

截屏2021-03-24 下午1.43.28.png

但是Person打印的是 :


截屏2021-03-24 下午1.47.51.png
  • 所以總結(jié):Person 在第一次進(jìn)入callAlloc里 !cls->ISA()->hasCustomAWZ()取得false(在ISA()函數(shù)里取得Person類,然后去獲取hasCustomAWZ()取得false),取走的最后一句發(fā)消息調(diào)用alloc,而后第二次進(jìn)去callAlloc,這時(shí)!cls->ISA()->hasCustomAWZ()拿到的true,才去開辟空間并關(guān)聯(lián)了類。而NSobject的alloc流程早就在系統(tǒng)層面走完了。系統(tǒng)層面去研究就要看LLVM的源碼了
最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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