iOS Objective-C底層 part2:born

以下內(nèi)容以至少你已經(jīng)理解OC內(nèi)萬物皆對象的概念為基礎(chǔ),當(dāng)然你還得有一份可以跑得objc源碼

1. Obj before born

在我們還沒有書寫代碼創(chuàng)建對象時,內(nèi)存內(nèi)已經(jīng)滿是對象(類,元類)了.

2. Obj 誕生 alloc

2.1 申請堆空間
//C code
typedef struct{
    char name[21];
    char age;
}CustomStruct;

typedef CustomStruct * CustomStructPointer;

int main(int argc, const char * argv[]) {
    CustomStructPointer stu = (CustomStructPointer)malloc(sizeof(CustomStruct));
    stu->age = 10;
    strcpy(stu->name, "pogong");
    printf("stack address %p\n",&stu);
    printf("heap address %p\n",stu);
    free(stu);
    return 0;
}

打印:
stack address 0x7fff5fbff708
heap address 0x100403ff0
C_memory_map.jpeg
//OC code

//PGCustomClass.h
@interface PGCustomClass : NSObject
@property(nonatomic,copy)NSString * name;
@property(nonatomic,assign)int age;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        PGCustomClass * obj = [[PGCustomClass alloc]init];
        NSLog(@"stack address %p",&obj);
        NSLog(@"heap address %p",obj);
    }
    return 0;
}

打印:
stack address 0x7fff5fbff728
heap address 0x101a02bc0
OC_memory_map.jpeg

以上是C語言的一個棧上的結(jié)構(gòu)體指針指向堆上的結(jié)構(gòu)體實例的代碼+內(nèi)存示意圖和OC的一個棧上的對象指針指向堆上的對象實例的代碼+內(nèi)存示意圖.
因為OC的對象說到底還是個結(jié)構(gòu)體實例,所以O(shè)C的對象生成的結(jié)果和C語言生成結(jié)構(gòu)體指針指向結(jié)構(gòu)體實例的結(jié)果是一樣的.當(dāng)然OC的對象生成過程會比較復(fù)雜,因為OC可是優(yōu)雅的動態(tài)語言誒!以下就是曲折的誕生過程:

alloc調(diào)用堆棧.png

alloc像內(nèi)的調(diào)用棧大概如上圖所示,看代碼的捋很久,也不需要全都記住,主要知道幾個關(guān)鍵參數(shù),關(guān)鍵條件和關(guān)鍵實現(xiàn)就可以了.

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;
    
#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // 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())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif
    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

1.hasCustomAWZ存在于類的元類中,標(biāo)識這個類有沒有復(fù)寫alloc/allocWithZone:;
2.canAllocFast是否支持快速創(chuàng)建.

可以看出最終都調(diào)用了_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();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            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.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}

幾大判斷條件:
hasCxxCtor:類及父類是否有自己的構(gòu)造函數(shù);
hasCxxDtor:類及父類是否有自己的析構(gòu)函數(shù)(這個條件在后面講對象dealloc的時候也會說到,與對象是否有實例變量有關(guān),這條件會記錄在對象的isa內(nèi));
fast:類是否用了是優(yōu)化的isa;

canAllocNonpointer and SUPPORT_NONPOINTER_ISA
兩個都帶nonpointer,
SUPPORT_NONPOINTER_ISA是來標(biāo)識當(dāng)前平臺是否支持優(yōu)化的isa,但即使平臺支持,具體到某一個類卻是不一定的,要具體的去驗證類的元類的信息.不過可以放心大多系統(tǒng)的類的isa都是支持優(yōu)化的,我們自定義的類的isa也是支持優(yōu)化的.
canAllocNonpointer則是具體標(biāo)記某個類是否支持優(yōu)化的isa.
在閱讀源碼時還有會各種帶nonpointer字樣的針對優(yōu)化isa的標(biāo)記,除SUPPORT_NONPOINTER_ISA外,全是針對某個類而言的.
優(yōu)化的isa是什么?接下來會說.

zone:老版本中要先去看看zone是否有空間,在OBJC2下,忽略zone參數(shù).

size:這由外圍傳入,size存儲在類的元類內(nèi)

// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
    assert(isRealized());
    return data()->ro->instanceSize;
}

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}

size_t size = cls->instanceSize(extraBytes);    

data()->ro->instanceSize;上方的注解May be unaligned depending on class's ivars..類實例的instanceSize取決于類的中成員變量的個數(shù):

instanceSize.png

再看看最后的調(diào)用:calloc或者malloc_zone_calloc就和C語言在堆中申請空間如出一轍了.

2.2 isa init
  • isa沒那么簡單,因為優(yōu)化了

在part1內(nèi)已經(jīng)提過了多遍的isa,當(dāng)然只要知道OC內(nèi)萬物皆對象,也肯定知道類實例->類->元類用isa串聯(lián)起來的關(guān)系:

simple_isa.png

但具體到真實的應(yīng)用場景下,isa的串聯(lián)會比上圖描繪更復(fù)雜更具體一些,特別是在64位系統(tǒng)上.所有用了64位系統(tǒng)的電子產(chǎn)品都沒有用全64位來表示地址.
因為這不現(xiàn)實:32位==>4G內(nèi)存,64位==>你算算看.

64位不全拿來表示地址,這就給64位的isa留下了很大的優(yōu)化空間(32位時對象的isa只是指向類而已).
我們截取類實例到類的過程來說明優(yōu)化的isa

instance_point_to_class_simple.png

先找到關(guān)于id的定義:

typedef struct objc_object *id;

然后objc_object又是什么:

struct objc_object {
private:
    isa_t isa;
}

然后再看isa_t是什么(這里只看arm64的):

union isa_t 
{
Class cls;
uintptr_t bits;
struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33;
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
}

這個union isa_t新奇了,聯(lián)合體少見吧!更奇怪的是聯(lián)合體還嵌套了結(jié)構(gòu)體,有不明白的請戳.
簡單的說就是:Class cls+uintptr_t bits+struct{......}共用一塊64位的內(nèi)存空間,當(dāng)然只有一個有效,在SUPPORT_NONPOINTER_ISA為1的情況下,仍然有一些類不支持優(yōu)化的isa,所以這樣的union isa_t就支持多用:

Class cls->為未優(yōu)化版的isa指向一個類

uintptr_t bit+struct{......}
uintptr_t bit用于對64位統(tǒng)一賦值,
struct{......}做細化讀取與細化賦值

請注意這聯(lián)合體內(nèi)結(jié)構(gòu)體內(nèi)的這個字段shiftcls,shiftcls=shift class,短的類地址.union isa_t共計64位,shiftcls占33位.這就是一個操作系統(tǒng)地址變量優(yōu)化的細節(jié).在64位iPhone上只拿33位表示地址的,也就是說這的shiftcls就存儲了類實例歸屬的類的地址.如圖:

instance_point_to_class_real.png

當(dāng)然類對象指向元類對象也是一樣的道理.
除了shiftcls之外,isa_t內(nèi)的各個字段均有用處,這些也就是64位的isa具體優(yōu)化的地方:

nonpointer:1->表示使用優(yōu)化的isa指針
has_assoc:1->是否包含關(guān)聯(lián)對象
has_cxx_dtor:1->是否包含析構(gòu)函數(shù)
shiftcls:33->類的指針
magic:6->固定值,用于判斷是否完成初始化
weakly_referenced:1->對象是否指向一個弱引用對象
deallocating:1->對象是否正在銷毀
has_sidetable_rc:1->在extra_rc存儲引用計數(shù)將要溢出的時候,借助sidetable(散列表)存儲引用計數(shù),has_sidetable_rc設(shè)置成1
extra_rc:19->存儲引用計數(shù)

后面章節(jié)的文章會細說關(guān)于這些字段所實現(xiàn)和優(yōu)化的功能.

  • 初始化對象的isa

初始化對象的isa要么initInstanceIsa->initIsa,要么直接調(diào)用initIsa->initIsa.

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

    initIsa(cls, true, hasCxxDtor);
}
inline void 
objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}
inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        assert(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif

        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don't want to hurt instantiation
        isa = newisa;
    }
}

SUPPORT_NONPOINTER_ISA前面已經(jīng)說過,而SUPPORT_INDEXED_ISA 為 1是另外一種優(yōu)化,用isa內(nèi)indexcls存儲著類在類列表內(nèi)的索引,這個用在watch上,手機和電腦上沒有這么用.

所以再看objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)的實現(xiàn)就簡單多了.

不支持nonpointer的,isa.cls = cls;

支持nonpointer的,
newisa.bits賦值,即對isa的64位統(tǒng)一初始化賦值,(統(tǒng)一初始化賦值)
newisa.has_cxx_dtor記錄傳入的has_cxx_dtor,(細化賦值)
newisa.shiftcls記錄下cls的地址.(細化賦值)

newisa.shiftcls = (uintptr_t)cls >> 3;(為什么右移3位?)
拿手機舉例子:shiftcls:33;(shiftcls會分配到33位),在64位的手機上拿33位保存類的地址,但因為位對齊的緣故,所有地址都是8的倍數(shù),所有地址書寫的成二進制數(shù)最后3位全是0,所以才如上見到:cls >> 3;(消除了3個沒有影響的0)

3. Obj 裝扮 init

- (id)init {
    return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
    return obj;
}

在沒有復(fù)寫init方法的情況下,init的實現(xiàn)特別簡單.

- (instancetype)init
{
    self = [super init];
    if (self) {
        _name = @"pogong";
        _age = 28;
    }
    return self;
}

復(fù)寫init的情況下能做的也只是對類實例成員變量的初始化裝扮.

init.png

當(dāng)然這樣的工作不在init內(nèi)部也能完成.

born.png

4. Obj 怪胎 Tagged Pointer

事情是是要從32位系統(tǒng)轉(zhuǎn)向64系統(tǒng)說起.
32位系統(tǒng)下:

NSNumber * num = [[NSNumber alloc]initWithInt:1];

棧上4個字節(jié)的對象指針指向堆上8個字節(jié)(存儲isa4個字節(jié)+存儲值4個字節(jié))的對象實例,共計12個字節(jié).

64位系統(tǒng)下:

NSNumber * num = [[NSNumber alloc]initWithInt:1];

棧上8個字節(jié)的對象指針指向堆上16個字節(jié)(存儲isa8個字節(jié)+存儲值8個字節(jié))的對象實例,共計24個字節(jié).
保存一個int要用8個字節(jié),包裝成對象要24字節(jié),有點太浪費了.
所以Tagged Pointer應(yīng)運而生,

NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberFFFF = @(0xFFFF);

NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"numberffff pointer is %p", numberFFFF);

打印:
number1 pointer is 0xb000000000000012
number2 pointer is 0xb000000000000022
number3 pointer is 0xb000000000000032
numberffff pointer is 0xb0000000000ffff2

我們前面已經(jīng)講過,因為64位系統(tǒng)上8位對齊,16進制打印出的地址最后一位不是8就是0(2進制打印后三位全是0),而這里最后一位是2,很怪異,這就是對Tagged Pointer的標(biāo)記.再將標(biāo)記位的前面的數(shù)值和對象本身的值進行比較一模一樣.
Tagged Pointer就是將Tagged Pointer的標(biāo)記混在一塊64位的內(nèi)存內(nèi).看上去是對象,但卻沒有isa(一個沒有靈魂的對象==>Tagged Pointer).但索性現(xiàn)在的isa也不能直接被調(diào)用,所以不會造成什么不便.
棧上8個字節(jié)的對象指針指向堆上8個字節(jié)(Tagged Pointer)的對象實例,共計16個字節(jié).
Tagged Pointer的引入,節(jié)約了64位系統(tǒng)的內(nèi)存,提高了運行效率.
NSNumber外,NSDate,NSString都應(yīng)用到Tagged Pointer.

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

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

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