alloc&init底層重識(shí)

    Person * p1 = [Person alloc];
    Person * p2 = [p1 init];
    Person * p3 = [p1 init];
    
    NSLog(@"對(duì)象%@ - 指針指向的地址%p - 指針地址%p",p1,p1,&p1);
    NSLog(@"對(duì)象%@ - 指針指向的地址%p - 指針地址%p",p2,p2,&p2);
    NSLog(@"對(duì)象%@ - 指針指向的地址%p - 指針地址%p",p3,p3,&p3);

打印結(jié)果

2021-03-24 13:41:24.665807+0800 111[606:63324] 對(duì)象<Person: 0x28291c080> - 指針指向的地址0x28291c080 - 指針地址0x16f91d178
2021-03-24 13:41:24.666031+0800 111[606:63324] 對(duì)象<Person: 0x28291c080> - 指針指向的地址0x28291c080 - 指針地址0x16f91d170
2021-03-24 13:41:24.666153+0800 111[606:63324] 對(duì)象<Person: 0x28291c080> - 指針指向的地址0x28291c080 - 指針地址0x16f91d168

三個(gè)對(duì)象的內(nèi)存地址都指向了同一個(gè) 0x28291c080,但使用了3個(gè)內(nèi)存指針指向了這個(gè)相同的內(nèi)存地址。
(猜 因?yàn)槿齻€(gè)對(duì)象 有三個(gè)isa指針?)

先下載oc的源碼,在該庫(kù)的readme中,作者也探索了alloc的流程,也做了對(duì)應(yīng)的分析和標(biāo)注。

在源碼中 master / objc_debug-master / objc-781 / objc.xcodeproj項(xiàng)目,雙擊運(yùn)行
打開(kāi)項(xiàng)目后,在Public Headers可以看到NSObject.h系統(tǒng)類(lèi)NSObject的頭文件的所有屬性,對(duì)象方法,類(lèi)方法。

頭文件.jpg

找到alloc方法后,先選中alloc,然后按住Command鍵,再右鍵即可跳轉(zhuǎn)(跟蹤)到NSObject.mm文件,文件在/Source中可以找到。

alloc方法

+ (id)alloc {
    return _objc_rootAlloc(self);
}

可以看到方法中調(diào)用了_objc_rootAlloc函數(shù),并傳入了self自己本身

再跟到_objc_rootAlloc函數(shù)可以看到

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

注釋中寫(xiě)到基本點(diǎn)的類(lèi)實(shí)現(xiàn)通過(guò) 類(lèi)方法(+)調(diào)用 alloc,并且cls(類(lèi))不能為nil

在代碼里又調(diào)用了 callAlloc方法
參數(shù)分別為:
1.cls 類(lèi)
2.是否檢查為nil,默認(rèn)為false
3.是否allocWithZone 傳入的是true

再跟進(jìn)到callAlloc函數(shù)里

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

注釋中寫(xiě)到通過(guò)[類(lèi) alloc] 或者[類(lèi) allocWithZone:nil]方法來(lái)調(diào)用得到,接著是簡(jiǎn)化優(yōu)化
OBJC2 是判斷oc 2.0可用
其中用到了兩個(gè)宏,宏寫(xiě)在了項(xiàng)目的/Project Headers/objc-os.hline.151、152

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

具體的解釋可以參考這篇__builtin_expect 說(shuō)明
__builtin_expect(bool(x), 1)的意思是x的值為真的可能性最大,編譯器會(huì)更大的可能編譯 if(__builtin_expect(bool(x), 1))里的代碼。
相反__builtin_expect(bool(x), 0)的意思是x的值為假的可能性最大,編譯器會(huì)最大的可能性不走判斷該方法的代碼,會(huì)走else里方法。
所以

//最大可能性為真  編譯器最大可能走這個(gè)判斷
#define fastpath(x) (__builtin_expect(bool(x), 1)) 
//最大可能性為假,編譯器最大的可能不走這個(gè)判斷,會(huì)走else
#define slowpath(x) (__builtin_expect(bool(x), 0))

借用__builtin_expect 說(shuō)明的舉例

int x, y;
 if(slowpath(x > 0))
    y = 1; 
else 
    y = -1;

代碼里,編譯器會(huì)優(yōu)先執(zhí)行y = -1,因?yàn)榕袛嗬锏淖畲罂赡苄詾榧?,直接?strong>else。
通過(guò)這種方式,編譯器在編譯過(guò)程中,會(huì)將可能性更大的代碼緊跟著執(zhí)行代碼,從而減少指令跳轉(zhuǎn)帶來(lái)的性能上的下降。

回頭看代碼

截圖.png

fastpathcls->ISA()->hasCustomAWZ())判斷一個(gè)類(lèi)是否有自定義的+allocWithZone實(shí)現(xiàn),AWZ就是AllocWithZone的縮寫(xiě)。所以fastpath(!cls->ISA()->hasCustomAWZ())表示的是這個(gè)類(lèi)沒(méi)有自定義的+allocWithZone時(shí),走if里的代碼。
代碼中又接著調(diào)用了 _objc_rootAllocWithZone,傳入cls(類(lèi)),和一個(gè)nil (接收參數(shù)為 malloc_zone_t zone __unused)

等等!~
_objc_rootAllocWithZone是不是有點(diǎn)眼熟?

截圖.png

alloc下方,allocWithZone方法中直接調(diào)用了_objc_rootAllocWithZone,這個(gè)等重學(xué)allocWithZone的時(shí)候再細(xì)究

再跟到_objc_rootAllocWithZone

_objc_rootAllocWithZone

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    //allocWithZone 在 oc 2.0之后忽略zone的參數(shù)
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

_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:需要開(kāi)辟的內(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)申請(qǐ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)到相應(yīng)的類(lèi)
    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);
}

這個(gè)方法主要的三個(gè)步驟
1.計(jì)算出需要開(kāi)辟的內(nèi)存空間大小
2.根據(jù)需要開(kāi)辟的內(nèi)存大小向系統(tǒng)申請(qǐng)內(nèi)存,返回地址指針
3.創(chuàng)建類(lèi)的isa 與 地址指針綁定

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

instanceSize

計(jì)算需要開(kāi)辟的內(nèi)存空間大小是通過(guò) size = cls->instanceSize(extraBytes);內(nèi)部實(shí)現(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;
}

在最后的時(shí)候,判斷了 size是否小于16,如果是則 size = 16的操作。
在判斷代碼里cache.hasFastInstanceSize(extraBytes)是判斷cache里是否有實(shí)例的內(nèi)存大小,傳入的是extraBytes 值是0。前面是fastpath最大可能性為真(前面說(shuō)過(guò))。
接著通過(guò)cache.fastInstanceSize(extraBytes)來(lái)計(jì)算最終的實(shí)例大小。

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

代碼中_flags & FAST_CACHE_ALLOC_MASK16可以通過(guò)位與運(yùn)算快速提取舍入到下一個(gè)16字節(jié)邊界的實(shí)例大小 得到計(jì)算后的size
接著將size 加上開(kāi)辟的內(nèi)存大小extra 減去 FAST_CACHE_ALLOC_DELTA16(值為0x0008)。理解為實(shí)例大小加上內(nèi)存大小,在進(jìn)行8位的偏移。
在代碼的最后,返回了align16方法的返回值,字面意思就是16對(duì)齊

align16

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

代碼中可以看到 (x + size_t(15)) & ~size_t(15),說(shuō)實(shí)話我一開(kāi)始是沒(méi)懂!也是翻了教程來(lái)學(xué),好多種解釋。
引用了一個(gè)博主的解釋

16字節(jié)對(duì)齊運(yùn)算

其中X為計(jì)算出的實(shí)例大小 比如為8,size_t(15)為什么是15?寫(xiě)死?
x +15 = 23,
二進(jìn)制為 0000 0000 0001 0111

15
二進(jìn)制為0000 0000 0000 1111

接著將15的二進(jìn)制取反 (~)取反操作
15取反
二進(jìn)制為1111 1111 1111 0000

之后將23的二進(jìn)制與 取反后的二進(jìn)制進(jìn)行 與 運(yùn)算 相同為1 不同為0
與后二進(jìn)制為 0000 0000 0001 0000 值為16,非常巧妙的計(jì)算!

16對(duì)齊的目的

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

回過(guò)頭來(lái)看 size = cls->instanceSize(extraBytes),此時(shí)size的內(nèi)存大小就為16,哪怕Int類(lèi)型為4個(gè)字節(jié),那也會(huì)開(kāi)辟出16大小的內(nèi)存(8個(gè)字節(jié))。如果一個(gè)類(lèi)里只有兩個(gè)int類(lèi)型的屬性,那么這兩個(gè)int類(lèi)型的屬性會(huì)共用一個(gè)16大小的內(nèi)存

isa

isa本身的結(jié)構(gòu)體內(nèi)部只有一個(gè)指針,且isa指針占用內(nèi)存8個(gè)字節(jié)。

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

可以用runtimeclass_getInstanceSize來(lái)獲取實(shí)例大小

打印isa大小及內(nèi)存大小

可以看到Person的類(lèi)isa大小只有8 但是系統(tǒng)給開(kāi)辟的空間卻有16

現(xiàn)在給Person加三個(gè)字段,一個(gè)Int,一個(gè)NSString,一個(gè)Int。順序很重要!

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
@property(nonatomic,assign) NSInteger age;
@property(nonatomic,strong) NSString * name;
@property(nonatomic,assign) NSInteger height;
@end

內(nèi)存占用說(shuō)明.png

圖中可看出,蘋(píng)果的確是做了16字節(jié)對(duì)齊的操作,Int本該4個(gè)字節(jié)卻開(kāi)辟了8個(gè)字節(jié)的空間。

申請(qǐng)內(nèi)存,返回地址指針

由于在調(diào)用_class_createInstanceFromZone時(shí),第三個(gè)參數(shù)傳入的是nil,接收的是zone,而iOS8 以后廢棄了用zone來(lái)開(kāi)辟內(nèi)存的方式,所以直接調(diào)用了 obj = (id)calloc(1, size)方法來(lái)申請(qǐng)內(nèi)存。

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);
}
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
。。。。。。。。
。。。。。。。。
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }

此時(shí)打印obj ,po obj出來(lái)的只有 )0x開(kāi)頭的地址,并不是<Person 0xxxxxxxxx>這類(lèi)的信息,因?yàn)檫€沒(méi)有將類(lèi)與該類(lèi)的內(nèi)存地址進(jìn)行關(guān)聯(lián)。
calloc只是將拿到的內(nèi)存大小size去申請(qǐng)內(nèi)存空間。

關(guān)聯(lián)相應(yīng)的類(lèi)

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

//initInstanceIsa的實(shí)現(xiàn)
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

代碼中將類(lèi) 和 hasCxxDtor傳入initInstanceIsa方法,在initInstanceIsa方法中調(diào)用了initIsa方法也傳入了cls類(lèi).
這些操作后就將類(lèi)類(lèi)的內(nèi)存地址*相關(guān)聯(lián)起來(lái)了。

(lldb) po p1
<Person: 0x60000048b740>

此時(shí)類(lèi)的alloc結(jié)束!
總結(jié)alloc的作用1.計(jì)算類(lèi)大小,進(jìn)行16字節(jié)對(duì)齊。2.拿到類(lèi)大小去申請(qǐng)開(kāi)辟內(nèi)存。3.將類(lèi)與內(nèi)存進(jìn)行關(guān)聯(lián)
引用 K哥的賊船圖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;
}
  • 代碼里有兩個(gè)init方法,一個(gè)是類(lèi)方法,一個(gè)是對(duì)象方法,都是反悔了對(duì)象本身。
  • 類(lèi)方法返回的是一個(gè)id任意類(lèi)型的self,是為了給開(kāi)發(fā)者自定義構(gòu)造方法的入口,如重寫(xiě)類(lèi)方法來(lái)設(shè)計(jì)工廠模式

FruitFactory.h

#import <Foundation/Foundation.h>
#import "FruitProtocol.h"

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger,FruitType){
    FruitTypeApple,
    FruitTypeOrange,
};

@interface FruitFactory : NSObject

+(id<FruitProtocol>)initWithType:(FruitType)type;

@end

NS_ASSUME_NONNULL_END   

FruitFactory.m

#import "FruitFactory.h"
#import "AppleFruit.h"
#import "OrangeFruit.h"

@implementation FruitFactory

+(id<FruitProtocol>)initWithType:(FruitType)type{
    id<FruitProtocol> factory = nil;
    switch (type) {
        case FruitTypeApple:
            factory = [[AppleFruit alloc] init];
            break;
        case FruitTypeOrange:
            factory = [[OrangeFruit alloc] init];
            break;
            
        default:
            break;
    }
    
    return factory;
}

@end

調(diào)用工廠方法時(shí)就是init開(kāi)頭,但返回的可能是不同的類(lèi)。所以要拿id來(lái)接收

#import "ViewController.h"
#import "FruitFactory.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    id<FruitProtocol> factory1 = [FruitFactory initWithType:FruitTypeApple];
    factory1.name = @"蘋(píng)果";
    [factory1 createProduct];
    
    
    id<FruitProtocol> factory2 = [FruitFactory initWithType:FruitTypeOrange];
    factory2.name = @"橘子";
    [factory2 createProduct];
    
}

@end

new

嘗嘗用new的機(jī)會(huì)并不多,但new的源碼與alloc + init的本質(zhì)無(wú)區(qū)別。

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

new操作無(wú)法對(duì)工廠設(shè)計(jì)重載init方法時(shí)做的業(yè)務(wù)操作進(jìn)行執(zhí)行。如果不需要重載自己寫(xiě)的方法時(shí),則不需要考慮。
如:

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

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

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