MJiOS底層筆記--OC對象本質

本文屬筆記性質,主要針對本身理解不太透徹的地方進行記錄。ios

推薦系統直接學習小碼哥iOS底層原理班---MJ老師的課確實不錯,強推一波。數組


OC對象本質

基於C與C++結構體實現緩存


OC語言如何被編譯器編譯:

OC ==> C++ ==> 彙編 ==> 機器語言bash

而在C++中只有struct(結構體)才能容納不一樣類型的內容(好比不一樣屬性)。架構


將Objective-C代碼轉換爲C\C++代碼

  1. clang -rewrite-objc OC源文件 -o 輸出的CPP文件

將源文件轉寫成通用的cpp文件框架

  1. xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 輸出的CPP文件

經過Xcode將源文件轉寫成arm64架構下的iphoneos文件,文件內容比第一種要少iphone

  1. 若是須要連接其餘框架,使用-framework參數。好比-framework UIKit

NSObject的OC與C++定義

  • 在OC中的定義
@interface NSObject <NSObject> {
    Class isa;
}
複製代碼
  • 轉成C++以後的定義
struct NSObject_IMPL {
	Class isa;
};
複製代碼

對於結構體來講,和數組同樣。其第一個成員的地址,即爲結構體對象的地址。 因此一個OC對象的地址,實際上就是其isa指針的地址。函數

而這個isa是指向objc_class結構體的指針佈局

// 指針
typedef struct objc_class *Class;
複製代碼

而一個指針在64位系統中所佔的內存爲8字節學習

因此一個OC對象所佔的內存至少爲8字節


NSObject對象所佔用內存的大小

上面的結論經過class_getInstanceSize函數也能夠佐證:

#import <objc/runtime.h>

/*
得到NSObject實例對象的
`成員變量`
所佔用的大小 >> 8
*/
NSLog(@"%zd", class_getInstanceSize([NSObject class]));


//runtime源碼中
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

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

須要注意這個word_align返回的是內存對齊後的大小,以unalignedInstanceSize(爲對齊的)大小做爲參數。

而對於NSObject *obj指針,咱們有另外一個函數能夠查看其實際被分配的內存大小

#import <malloc/malloc.h>
// 得到obj指針所指向內存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
複製代碼

爲何8字節的結構體會被分配16字節

繼續看runtime

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

id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

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]; } // Replaced by ObjectAlloc + (id)allocWithZone:(struct _NSZone *)zone { return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone); } id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone) { id obj; #if __OBJC2__ // allocWithZone under __OBJC2__ ignores the zone parameter (void)zone; obj = class_createInstance(cls, 0); #else if (!zone) { obj = class_createInstance(cls, 0); } else { obj = class_createInstanceFromZone(cls, 0, zone); } #endif if (slowpath(!obj)) obj = callBadAllocHandler(cls); return obj; } id class_createInstance(Class cls, size_t extraBytes) { return _class_createInstanceFromZone(cls, extraBytes, nil); } 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;
}


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;
}
複製代碼

alloc函數最終會根據instanceSize返回的size,而後使用calloc(1, size);函數去分配內存。

instanceSize函數中,alignedInstanceSize方法爲成員變量所佔內存大小(上面已經貼過一次).extraBytes參數(據我所見)都爲0。

CoreFoundation框架在instanceSize函數中硬性規定不足16字節的內存地址會被補成16位字節。

但實際上,NSObject對象只使用了8字節用來存儲isa指針


Student對象的本質

@interface Student : NSObject
{
    @public
    int _no;
    int _age;
}
@end
複製代碼

重寫成C++以後

struct Student_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _no;
	int _age;
};

struct NSObject_IMPL {
	Class isa;
};


//其實就是
struct Student_IMPL {
	Class isa; //8字節
	int _no; //4字節
	int _age; //4字節
};
複製代碼

因此一個OC對象的本質其實是一個包含了全部父類成員變量+自身成員變量的結構體


Student的內存佈局及大小

能夠經過Debug->Debug workflow->View momory查看指定地址的結構來查證

對於Student實例對象所佔內存地址的大小,咱們一樣能夠經過malloc_size函數來肯定。

結果是16。8字節父類的isa指針、4字節_age的int、4字節_no的int。

固然若是有興趣能夠用memory write (stu地址+8偏移量) 8的方式,經過直接修改內存的方式對成員變量_no的值進行修改。


內存對齊原則下的OC對象內存分配

alignedInstanceSize()函數的內存對齊

alignedInstanceSize()函數會按照全部成員變量中內存最長的一個作內存對齊。好比

@interface Animal: NSObject
{
    int weight;
    int height;
    int age;
}
複製代碼

實際上只須要8+4+4+4=20個字節長度便可,可是內存對其以後會返回8*3=24

malloc()/calloc()函數的內存對齊

在對象實際建立時,先以alignedInstanceSize()返回的大小做爲參考。 而後calloc在實際分配內存時爲了內存對齊,最終將會根據bucket進行分配。這個bucket是16的整數倍。

#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */
複製代碼

因此Animal的實例對象實際上會被分配32個字節長度的內存地址。


sizeOf 與 class_getInstanceSize

返回一個參數對象所佔的內存大小

sizeOf

sizeOf是運算符,在程序編譯階段將會直接替換成參數類型所佔內存具體的常數值。

因爲在編譯階段替換,因此有如下這種特性:

MJPerson *p = [[MJPerson alloc] init];
NSLog(@"%zd", sizeof(p)); // 8
複製代碼

p在編譯時將會被認爲成指針,返回8字節的指針內存長度。而不是MJPerson類型的內存長度。

class_getInstanceSize

class_getInstanceSize是一個方法,在程序運行階段將會進行計算。

他能夠在運行階段計算某個類所需內存大小

class_getInstanceSize([p class]) //24
複製代碼

objc_class

runtime.h

OC2.0之前的類結構體。在2.0以後只剩下頭文件,而且已經標記成了OBJC2_UNAVAILABLE的棄用狀態。

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
複製代碼

objc_runtime_new.h

最新的runtime源碼中,優化了類的結構,內部分工更加明確。

在一級結構體中,只保留了isasuperclasscache三個經常使用的成員

其他信息均轉移到了class_data_bits_t這個二級結構體上

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // 方法緩存
    class_data_bits_t bits;    // 具體的類信息

    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ...
}
複製代碼

class_data_bits_t(類信息列表)內部,還保存着class_rw_t(可讀寫信息列表),這些信息是能夠動態修改的

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods; //方法
    property_array_t properties; //屬性
    protocol_array_t protocols; //協議

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}
複製代碼

class_rw_t(可讀寫信息列表)內部,還保存着class_ro_t(不可變信息列表),保存着類加載進內存時就須要肯定的信息

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; //實例對象所佔內存大小
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name; //類名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; //成員變量

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};
複製代碼

參考資料

小碼哥iOS底層原理班

iOS複習筆記:OC對象內存大小問題

相關文章
相關標籤/搜索