iOS底層之a(chǎn)lloc、init探究

我們先從下面案例看看alloc和init分別做了什么事?

    BKPerson *p1 = [BKPerson alloc];
    BKPerson *p2 = [p1 init];
    BKPerson *p3 = [p1 init];
    BKNSLog(@"%@ - %p - %p",p1,p1,&p1);
    BKNSLog(@"%@ - %p - %p",p2,p2,&p2);
    BKNSLog(@"%@ - %p - %p",p3,p3,&p3);  

輸出打印結(jié)果


打印的分別是對象描述、指針指向的地址(即當前指針存放的對象的地址)、當前指針地址(當前指針被存儲在哪里)。
可以看出p1,p2,p3指向了同一個內(nèi)存地址0x600001138790,而分別使用了3個指針保存這個內(nèi)存地址的值。

猜想:在alloc一步就已經(jīng)創(chuàng)建出了一個對象??
我們再通過objc源碼來驗證這個猜想

源碼探究

一步一步跟進alloc的源碼

  1. alloc ——> _objc_rootAlloc
+ (id)alloc {
    return _objc_rootAlloc(self);
}`
  1. _objc_rootAlloc——> callAlloc
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
  1. callAlloc——> _objc_rootAllocWithZone
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));
}

這里面系統(tǒng)用到了兩個宏定義

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
  • __builtin_expect這個指令是gcc引入的,作用是允許程序員將最有可能執(zhí)行的分支告訴編譯器。這個指令的寫法為:__builtin_expect(EXP, N)。意思是:EXP==N的概率很大。
  • __builtin_expect()是 GCC (version >= 2.96)提供給程序員使用的,目的是將“分支轉(zhuǎn)移”的信息提供給編譯器,這樣編譯器可以對代碼進行優(yōu)化,以減少指令跳轉(zhuǎn)帶來的性能下降。
  • 也就是說fastpath(x)告訴編譯器x值為真的可能性最大。編譯器會更大可能編譯fastpath條件分支里的指令,也就是告訴編譯器很大概率走這條分支。
  • slowpath(x)告訴編譯器x值為假的可能性最大,也就是x為真的可能性很小,當x==0為真才執(zhí)行這個條件分支下的語句。編譯器會更大可能編譯else部分的指令,也就是告訴編譯器很小概率會走if slowpath(x)這條分支的指令。
  • 通過這種方式,編譯器在編譯過程中,會將可能性更大的代碼緊跟著前面的代碼,從而減少指令跳轉(zhuǎn)帶來的性能上的下降。

在回到callAlloc方法里,由于這里有幾個分支,可以通過斷點調(diào)試跟方法,看是走的哪一個分支。
if (fastpath(!cls->ISA()->hasCustomAWZ()))中,cls->ISA()->hasCustomAWZ())判斷一個類是否有自定義的 +allocWithZone 實現(xiàn),所以fastpath(!cls->ISA()->hasCustomAWZ())表示這個類沒有自定義的+allocWithZone 時走這部分代碼。所以執(zhí)行了_objc_rootAllocWithZone(cls, nil);

  1. _objc_rootAllocWithZone——> _class_createInstanceFromZone
NEVER_INLINE
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);
}
  1. _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;
    // 1:需要開辟的內(nèi)存大小,可以看到外部傳入的extraBytes為0
    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;
    }

    // 3: 關(guān)聯(lián)到相應的類
    if (!zone && fast) {
        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);
}

這個方法主要做了三個事情:計算需要的內(nèi)存大小、申請內(nèi)存返回地址指針、關(guān)聯(lián)到對應的類。下面分析這個三個方法都做了哪些工作

alloc核心方法

1.計算內(nèi)存大小

計算需要開辟的內(nèi)存空間大小是通過size = cls->instanceSize(extraBytes);內(nèi)部實現(xiàn)過程如下

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

在這里可以發(fā)現(xiàn),if (size < 16) size = 16;不夠16個字節(jié),會手動分配16個字節(jié)。之后調(diào)試跟進走的是cache.fastInstanceSize快速計算實例大小的方法。

fastInstanceSize中會執(zhí)行到align16

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

align16的實現(xiàn),可以看到當前使用的是16字節(jié)對齊的方式。

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

圖示16字節(jié)對齊運算


16字節(jié)對齊運算

16字節(jié)對齊的目的

  • 提高性能,加快存取速度:通常內(nèi)存是由一個個字節(jié)組成的,cpu在存取數(shù)據(jù)時,并不是以字節(jié)為單位存儲,而是以塊為單位存取。頻繁存取字節(jié)未對齊的數(shù)據(jù),會極大降低cpu的性能。固定16字節(jié)的存取長度,可以更快存取數(shù)據(jù)。
  • 更安全:蘋果如今采用16字節(jié)對齊,由于在一個對象中,isa占8字節(jié),而對象每個屬性也占8字節(jié),當對象無屬性時,會預留8字節(jié),即16字節(jié)對齊,如果不預留,CPU存取時以16字節(jié)長度會導致訪問到相鄰的其他對象,造成訪問混亂。

在執(zhí)行完cls->instanceSize(extraBytes)就可以打印出計算出的內(nèi)存大小size為16


我們知道OC代碼底層是用C/C++實現(xiàn)的,將OC轉(zhuǎn)成C/C++代碼可以發(fā)現(xiàn),NSObject實際是一個結(jié)構(gòu)體,結(jié)構(gòu)體里只有個isa指針,既然是指針,在64位機環(huán)境下占了8個字節(jié),在32位環(huán)境下占了4個字節(jié)。而NSObject這個結(jié)構(gòu)體內(nèi)部只有這么一個指針,那NSObject本身也是占用8字節(jié)。

struct NSObject_IMPL {
    Class isa;
};

可以用runtime的一個函數(shù),獲取類的實例大小

class_getInstanceSize(Class _Nullable cls)

需要引入#import <objc/runtime.h>,打印結(jié)果果然是8個字節(jié)

NSLog(@"InstanceSize:%zd",class_getInstanceSize([NSObject class]));

結(jié)果:InstanceSize:8

我們還能通過導入#import <malloc/malloc.h>,查看到這個函數(shù)

extern size_t malloc_size(const void *ptr);

使用這個函數(shù)打印

NSLog(@"%zd",malloc_size((__bridge const void *)(obejct)));

結(jié)果是 16

由此看出對象本身占用8個字節(jié),而系統(tǒng)開辟了16個字節(jié)用來保存這個對象。

進一步驗證

上面只是NSObject類的情況,而通常類都會有相關(guān)屬性。
BKPerson類增加4個屬性,兩個字符串類型,兩個int類型

@interface BKPerson : NSObject

@property (nonatomic,strong) NSString *name;     // Dog
@property (nonatomic,strong) NSString *nick; // KK
@property (nonatomic) int age;
@property (nonatomic) int hobby;

@end

運行調(diào)試



查看對象內(nèi)存地址


可以看到對象isa指針占據(jù)了8字節(jié),兩個string屬性每個占據(jù)8字節(jié),一共8+8+8=24個字節(jié),但是系統(tǒng)開辟了32字節(jié),也就是有8個字節(jié)為空。

再賦值兩個int屬性,而64位機下int類型占據(jù)了4個字節(jié),可以看到之前空的8個字節(jié),剛好放了age和hobby兩個屬性的值。

結(jié)合以上16字節(jié)對齊分配空間法則,進一步得出結(jié)論:
類的實例占用8字節(jié),而每一次申請的內(nèi)存空間是16字節(jié)。類的實例在第一次申請內(nèi)存空間就申請了包括isa和所有屬性的內(nèi)存空間大小,跟屬性有沒有賦值無關(guān)

2.申請內(nèi)存,返回地址指針

return _class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC);從第三個參數(shù)可以看到傳入的zone為nil,由于iOS 8以后廢除了用zone開辟內(nèi)存的方式,所以是用obj = (id)calloc(1, size);的方式申請內(nèi)存。這里面size就是我們上面字節(jié)對齊算法得出的內(nèi)存大小。

打印執(zhí)行calloc之后的地址


而這里只打印出了開辟的內(nèi)存地址,沒有類的信息例如<BKPerson: 0x101019160>,驗證了calloc這一步只是向系統(tǒng)申請內(nèi)存空間。

3.關(guān)聯(lián)相應的類

將地址指針與類相關(guān)聯(lián)

obj->initInstanceIsa(cls, hasCxxDtor);
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

這一步會init一個isa指針,與類關(guān)聯(lián)起來

打印指針描述,可以看到已經(jīng)關(guān)聯(lián)上類了


總結(jié):以上對alloc源碼的探究,可以得知alloc的主要作用就是使用16字節(jié)對齊算法計算內(nèi)存,開辟內(nèi)存,關(guān)聯(lián)類。

alloc的整體流程圖示


那么init幫我們做了什么事呢?

+ (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;
}
  • 由上面源碼可以知曉init的類方法和對象方法返回的都是對象本身。
  • 不同的是類方法返回了一個id類型的self,這是為了可以給開發(fā)者提供自定義構(gòu)造方法的入口,通過id強轉(zhuǎn)類型實現(xiàn)工廠設計,返回我們定義的類型。

new

我們習慣于用new一個對象,可以更省略代碼,通過源碼可以知道它跟alloc+init的方式本質(zhì)并沒有區(qū)別

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

但是一般開發(fā)中并不建議使用new,主要是工廠設計重載init方法我們?nèi)绻鲆恍I(yè)務的操作,用new初始化則無法執(zhí)行到里面去。

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

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