分析oc物件的記憶體結構及其建立過程

lyuf發表於2019-03-10

物件導向的程式語言中有經典的話:萬物皆物件。objective-c就是一門物件導向的語言,那麼在oc的程式設計中就離不開物件的建立,下面分析oc物件的記憶體結構及其建立的過程

記憶體結構

首先分析物件的記憶體結構,其實我們知道oc的物件指標其實就是結構體指標,也就是說oc的物件轉成c++程式碼後其實就是一個結構體。定義一個簡單的類程式碼如下:

@interface Person : NSObject
@property (nonatomic,assign) NSUInteger age;
@property (nonatomic,copy) NSString *name;
-(void)say;
@end

@implementation Person
-(void)say{
    NSLog(@"person say");
}
@end
複製程式碼

利用clang編譯器吧這個類轉成c++程式碼後可以發現對應的Person類其實就是一個結構體,程式碼如下:

struct Person_IMPL {
	Class isa;
	NSUInteger _age;
	NSString * _Nonnull _name;
};

複製程式碼

先忽略say方法的存在(方法跟結構體的isa指標相關,稍後再分析),可以看出來 struct Person_IMPL的結構體定義跟Person類的屬性定義是吻合的。所以,其實我們平常 建立物件其實就是給類的對應的結構體在堆上開闢一塊合適的空間,並返回這塊空間的指標給使用者,這個指標就是我們平時操作物件(包括物件方法的呼叫,物件屬相的更改)的指標,只是oc把這個結構體指標包裝成一個oc型別的指標(Person *)而已

接下來利用指標強轉把oc物件類似指標轉換為c語言結構體指標來驗證一下oc類其實底層就是c語言的結構體。

執行程式碼:

Person *p = [[Person alloc] init];
p.age = 15;
p.name = @"Mike";
//指標強轉
struct Person_IMPL *sp = (__bridge struct Person_IMPL *)(p);
NSLog(@"通過oc物件型別指標轉為結構體指標後訪問的結構體Person_IMPL值 : _age = %zd , _name = %@" , sp->_age , sp->_name);
複製程式碼

列印結果:

通過oc物件型別指標轉為結構體指標後訪問的結構體Person_IMPL值 : _age = 15 , _name = Mike
複製程式碼

驗證結果符合預期。

既然oc物件的底層資料結構是c語言的結構體,那麼物件的屬性或成員的存取其實跟c語言結構體的成員變數的存取原理其實是一樣的:通過指標的偏移操作記憶體的資料:用一個通俗一點的公式可以表達為 propertyValue(物件的成員變數值) = objcPointer(物件指標) + offset(偏移量)。 物件記憶體及其指標的關係用下圖表示:

分析oc物件的記憶體結構及其建立過程

我們可以用程式碼驗證通過指標的操作能否訪問到oc物件的記憶體資料 程式碼如下:

Person *p = [[Person alloc] init];
p.age = 15;
p.name = @"Mike";
struct Person_IMPL *sp = (__bridge struct Person_IMPL *)(p);
//通過指標的偏移操作 , 或的結構體內部的成員地址,也是oc物件指標(Person * p)的物件成員地址。
long long ageAdress =  (long long)((char *)sp+8);
long long nameAdress = (long long)((char *)sp+16);
//打斷點通過lldb指令除錯驗證
NSLog(@"===");
複製程式碼

在NSLog處打斷點後,通過lldb列印相關指令檢視到p或sp指標向上偏移8位元組可以獲取到時成員變數age的值,如圖所示

分析oc物件的記憶體結構及其建立過程

p或sp指標向上偏移8位元組可以獲取到時成員變數name

分析oc物件的記憶體結構及其建立過程
的值,如圖所示

物件的建立過程

研究類的初始化過程肯定是通過objc官方原始碼分析 , 本人用的是objc4-750的版本進行分析。

通過我們建立物件都是呼叫+alloc方法進行建立的,此方法呼叫到了下面兩個方法,我把該方法的簡化如下

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    //此函式呼叫到下面的 _class_createInstanceFromZone 
    id obj = class_createInstance(cls, 0); 
    return obj;
}

//class_createInstance 呼叫到此方法
static __attribute__((always_inline))  id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    bool hasCxxDtor = cls->hasCxxDtor();
    
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    obj = (id)calloc(1, size);
    if (!obj) return nil;
    obj->initInstanceIsa(cls, hasCxxDtor);
    
    return obj;
}


複製程式碼

上面函式的作用有兩個

  1. 獲取建立的物件所需的空間,並分配相應的空間(在獲取空間大小的時候內部邏輯會判斷 size >= 16,並且對齊二進位制後面3位為0)
  2. 把分配好的空間記憶體指標轉為(struct objc_object * 就是我們的id指標,這也反映了NSObjcet * 對應struct objc_object *,下面會分析)
  3. 把初始化完isa的指標作為物件指標返回給呼叫者

物件定義分析

在分析初始化isa指標前先弄清楚 oc物件指標(NSObjcet * , id)對應在objc原始碼中那些結構體的關係可能會容易理解一點

// NSObject 定義
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//struct objc_object 定義
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

//struct objc_class 定義
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

//id 指標的定義
typedef struct objc_object *id;

//Class 定義
typedef struct objc_class *Class;

複製程式碼

通過上面的程式碼可以發現,我們平常使用的 NSObject *或者 id 指標其實底層是 struct objc_object 指標,而平常使用的Class型別其實底層就是struct objc_class指標,而且還可以發現Class型別(objc_class *)其實是繼承自objc_class,就是說我們Class型別其實也是一個物件。

在oc中物件(object),物件的父類(SuperClass),物件的類(Class),物件的元類(MetaClass)都是通過指標來進行關聯的。 SuperClass 對應的是objc_classsuperclass指標 , Class 對應的是objc_classisa指標(OBJC2中的isa指標已經不是直接指向Class的地址了,而是用來位域的技術儲存了Class的地址外還有其他一些額外的資訊)。

isa指標

首先我們開看下isa結構的定義(objc-private.h + isa.h) 這裡緊列舉__x86__64__架構的情況進行分析

#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    struct {
      uintptr_t nonpointer        : 1;//代表是否有開啟指標isa指標優化(位域技術儲存更多資訊)                                       
      uintptr_t has_assoc         : 1;//是否有設定關聯物件                                        
      uintptr_t has_cxx_dtor      : 1;//是否有c++解構函式                                        
      uintptr_t shiftcls          : 44;//儲存Class或MetaClass的記憶體地址資訊
      uintptr_t magic             : 6;//驗證物件是否初始化完成                                         
      uintptr_t weakly_referenced : 1;//是否有被弱引用指標指向                                         
      uintptr_t deallocating      : 1;//物件是否正在釋放                                         
      uintptr_t has_sidetable_rc  : 1;//extra_rc無法儲存過大的數值時,次標誌位為1,把extra_rc部分的值儲存到一個全域性的SideTable中                                    
      uintptr_t extra_rc          : 8//儲存引用計數儲存 (引用值 = 儲存值 - 1)
    };

};
複製程式碼

可以看isa_t 其實是一個共用體union : 一個8位元組指標(64位) = cls = bits = 使用位域的struct

瞭解了isa_t的結構後我們看下struct objc_object初始化isa的方法實現

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) { // Taggedpointer
        isa.cls = cls;
    } else { // 非Taggedpointer , 平時我們經常使用的物件
      
        isa_t newisa(0);
        
         //對isa的 index 、magic 初始化
        newisa.bits = ISA_MAGIC_VALUE;
        
        //對isa的 has_cxx_dtor 初始化
        newisa.has_cxx_dtor = hasCxxDtor;
        
        //把傳進來的Class指標值右移3位賦值給shiftcls
        newisa.shiftcls = (uintptr_t)cls >> 3;
        
        //更新objc_object的isa指標
         isa = newisa;
    }
}
複製程式碼

在複製Class的指標值是為什麼要右移三位在賦值,其實原因可以在從上面獲取記憶體大小時進行的對齊規則可以看出Class的地址轉成64位二進位制時指標的後三位都是0,右移3位後再存進isa的47位的shiftcls,這樣節省可記憶體的空間。通過列印Class的地址值可以看出47位的記憶體是可以存放的下一個右移3位的Class的地址值的,並不一定要64d的的儲存空間。

物件、父類、類、元類間的關係

一個物件呼叫它的例項方法,其實是先通過isa指標找到類物件的記憶體地址,通過訪問其成員 class_data_bits_t bits獲取到例項方法。類物件呼叫的類方法其實與例項方法的原理是一樣的通過isa找到元類(MetaClass)的記憶體地址,通過訪問MetaClassclass_data_bits_t bits獲取類方法進行呼叫。兩者的方法查詢都是在當前類中如果找不到物件的方法就會沿著superClass指標往父類的方法裡面查詢,直到找到位置,如果找不到就會進行方法的動態解析或者訊息的轉發,還沒解決就會丟擲找不到方法的錯誤。下面的圖片很好的展示了這例項物件(objc)與其 類物件(Class)、元類物件(MetaClass)、父類(SuperClass)之間的關係。

分析oc物件的記憶體結構及其建立過程

細心觀察上面的圖片,其實可以發現幾個注意點

  1. 根類物件(圖中的RootClass)的superclass指標最終指向nil , 其isa指向根元類(圖中的Root MetaClass)
  2. 根元類(圖中的Root MetaClass)的isa指標指向的是其本身,superclass指標指向根類(圖中的RootClass) 3.所有的MetaClass的isa都是指向同一個物件,那就是RootMetaClass

那麼分析清楚了這幾個物件間的關係後,接下來開始分析物件例項方法究竟是如何初始化的。

class_data_bits_t 分析

原始碼定義

struct class_data_bits_t {

    uintptr_t bits;
    
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}
複製程式碼

其實 class_data_bits_t bit 就是一個指標而已。真正的方法儲存在data()返回的指標指向的那塊記憶體中。該記憶體其實是一個class_rw_t的型別值。繼續分析返回的class_rw_t *型別值

struct class_rw_t {

    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;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
}

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

通過查閱資料及原始碼,加上實踐驗證可以知道,我們平是定義的類屬性或者方法,經過編譯器的處理轉成C或C++程式碼其實底層由多種結構體和函式共同協作生成包含只讀方法和屬性的struct class_ro_t型別變數。就用上面的Person類作為例子。通過clang編譯器指令轉成c++程式碼後我摘取一些重要片段


//包含物件屬性資訊的變數 , 用於初始化 _class_ro_t 變數
static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	2,
	{{"age","TQ,N,V_age"},
	{"name","T@\"NSString\",C,N,V_name"}}
};

//包含物件成員變數資訊的變數 , 用於初始化 _class_ro_t 變數
static struct /*_ivar_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count;
	struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_ivar_t),
	2,
	{{(unsigned long int *)&OBJC_IVAR_$_Person$_age, "_age", "Q", 3, 8},
	 {(unsigned long int *)&OBJC_IVAR_$_Person$_name, "_name", "@\"NSString\"", 3, 8}}
};

//包含物件方法資訊的變數 , 用於初始化 _class_ro_t 變數
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	5,
	{{(struct objc_selector *)"say", "v16@0:8", (void *)_I_Person_say/**方法對應的函式指標*/},
	{(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
	{(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_}}
};

// _class_ro_t 型別變數
tatic struct _class_ro_t _OBJC_CLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	0, __OFFSETOFIVAR__(struct Person, _age), sizeof(struct Person_IMPL), 
	0, 
	"Person",
	(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Person,
	0, 
	(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Person,
	0, 
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person,
};

//下面幾個方法都是為初始化 Person 類做準備工作
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
	0, // &OBJC_METACLASS_$_Person,
	0, // &OBJC_CLASS_$_NSObject,
	0, // (void *)&_objc_empty_cache,
	0, // unused, was (void *)&_objc_empty_vtable,
	&_OBJC_CLASS_RO_$_Person,
};
static void OBJC_CLASS_SETUP_$_Person(void ) {
	OBJC_METACLASS_$_Person.isa = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_Person.superclass = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_Person.cache = &_objc_empty_cache;
	OBJC_CLASS_$_Person.isa = &OBJC_METACLASS_$_Person;
	OBJC_CLASS_$_Person.superclass = &OBJC_CLASS_$_NSObject;
	OBJC_CLASS_$_Person.cache = &_objc_empty_cache;
}

static void OBJC_CLASS_SETUP_$_Person(void ) {
	OBJC_METACLASS_$_Person.isa = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_Person.superclass = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_Person.cache = &_objc_empty_cache;
	OBJC_CLASS_$_Person.isa = &OBJC_METACLASS_$_Person;
	OBJC_CLASS_$_Person.superclass = &OBJC_CLASS_$_NSObject;
	OBJC_CLASS_$_Person.cache = &_objc_empty_cache;
}

複製程式碼

從上面的原始碼可以看出在程式編譯完成後類的資訊已經被編譯器處理完了大部分的工作,剩下小部分工作是通過runtime機制來處理的。

runtime機制處理類資訊

objc原始碼中有一個函式realizeClass,負責處理編譯資訊及執行時資訊的轉接返回類的真實結構體。我簡化下函式留下處理 _class_ro_tclass_rw_t關係的原始碼

static Class realizeClass(Class cls){
    
    ro = (const class_ro_t *)cls->data();
    rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
    rw->ro = ro;
    rw->flags = RW_REALIZED|RW_REALIZING;
    cls->setData(rw);
    
    return cls;
}
複製程式碼

從上面的原始碼可以看出,類在未經過呼叫函式realizeClass(Class cls) 前,objc_class結構體方法呼叫的class_rw_t *data()方法返回的其實是class_ro_t型別的指標,在經過realizeClass處理後才把class_rw_t型別變數建立好,並把原來的class_ro_t指標賦值給class_rw_t變數的ro成員變數,並賦值給cls。

接下來我們通過objc原始碼驗證一下

在除錯是先獲取[Person Class]的地址,接著realizeClass開始前打斷條件斷點(cls == Person地址值)配合lldb指令,通過指標的偏移獲得class_data_bits_t的值,再通過其呼叫data()方法獲得對應的指標,通過把改指標強轉為class_ro_t 型別列印出來的值符合之前定義Person類的資訊。除錯過程如下圖

分析oc物件的記憶體結構及其建立過程

邏輯分析可能有點亂,見諒 ^__^ !

相關文章