iOS的OC源碼分析之類的結構分析

前言

想成爲一名優秀的iOS開發者,對底層的原理學習是必不可少的,筆者整理了一系列有關OC的底層文章,但願能夠幫助到你。這篇文章主要講解的是類的底層結構分析編程

1.iOS的OC對象建立的alloc原理數組

2.iOS的OC對象的內存對齊緩存

3.iOS的OC的isa的底層原理bash

開始介紹類的結構以前,請問一下,你在開發的過程當中有沒有想過一個問題。就是建立多個相同類型的對象的時候,那麼這個對象的類是否是多個呢?帶着這個問題,有了以下不一樣形式獲取到類的代碼markdown

Class class1 = [TestJason class];
Class class2 = [TestJason alloc].class;
Class class3 = object_getClass([TestJason alloc]);
NSLog(@"%p====%p===%p",class1,class2,class3);

===========運行的結果===========
LGTest[1541:33345] 0x100002848====0x100002848===0x100002848
複製代碼

從上面的運行結果能夠知道,類在內存裏面只存在一份less

1.類結構初探

仍是使用蘋果的objc4-756.2源碼來學習的,具體能夠看iOS的OC對象建立的alloc原理這篇文章有介紹。經過Class的源碼post

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *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

    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ....
    
    
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
複製代碼

從源碼能夠知道Class是一個objc_class的結構體,而objc_class是繼承自objc_object的,由於在面向對象編程中萬物皆對象,從這裏也能夠知道,其實類也是對象的。從中能夠知道類裏面分別有從父類繼承的isasuperclass,cache_tclass_data_bits_t類型的bits。由於在底層中最終是編譯成結構體的形式,因此你是否是很好奇大部分的基類NSObject在底層是怎樣的形式呢?學習

OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
複製代碼

經過源碼能夠知道,NSObject的底層是與objc_object是同樣的。在平常開發中,定義類的時候都是會有屬性,變量和方法的,你是否是很好奇屬性,成員變量和方法在類的底層中儲存在哪裏?接下來,我都會一一介紹。ui

2.class_data_bits_t存放屬性和實例方法的地方

爲了方便介紹接下來的內容定義了一個TestObject的類,下面是這個類的定義和實現,而且經過lldb的指令來介紹。this

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TestObject : NSObject{
    NSString *nickName;
}
@property(nonatomic,copy) NSString *name;

-(void)sayName;
+(void)sayNickName;

@end

NS_ASSUME_NONNULL_END

//實現的代碼
TestObject *testObject = [TestObject alloc];
Class tClass = object_getClass(testObject);
NSLog(@"%@===%p",testObject,tClass);
複製代碼

經過lldb的指令獲得以下:

經過上面的結果能夠知道 0x001d800100001659isa的內存值, 0x0000000100b37140superclass的, 0x00000001003da290cache_t的,那麼 0x0000000000000000就是 bits了嗎?這幾個值從字面意思能夠知道 superclass是存放父類的, cache_t是存放一些緩存的東西(這塊內存後續會出一篇文章介紹),那麼 bits應該就是存放咱們須要的屬性和實例方法的了。下面是 class_rw_t的源碼

class_rw_t *data() { 
    return bits.data();
}
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;
複製代碼

從上面的lldb中打印出來的0x0000000000000000是0的這樣的話就是否是說直接打印不出來呢?並非的,咱們能夠經過分析類裏面的各個屬性佔的字節大小而後經過內存偏移來找到最終bits的內存值。

2.1.objc_class的各個指向值的大小

經過源碼能夠知道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
    ···
}
    
struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
···
 }
 
 typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

複製代碼

經過前面的文章能夠知道,isa是一個聯合體(union)佔8個字節,superclass由於是一個類因此也佔8個字節,由於cache是一個結構體類型(struct),這個大小是根據它裏面的屬性的容量來決定大小的,由於_buckets是一個結構體指針佔8字節,mask_t經過源碼能夠知道佔4字節,因此cache共佔16字節。由isacache一共是32字節,轉爲16進制就是0x20,由上圖能夠知道,tClass類的內存值的起始位置是0x100001680經過內存偏移0x20能夠獲得0x1000016a0,那麼這個值就是bits的內存值。也能夠直接用x/5gx tClass來打印出來,獲得的內存值也是0x1000016a0

由於 bits裏面的 data()能夠獲得 class_rw_t,而 class_rw_t能夠獲得屬性的值,因此經過 lldb的指令能夠打印 class_rw_t裏面所指向的值。如下是內存偏移到 0x1000016a0打印出來的結果

因此屬性是放在 class_rw_tproperties裏面,能夠經過 properties裏面的 list獲得 property_list_t的數組,最終打印了屬性 name

可是再用 $7這個數組來找的時候,發現是找不到 TestObject類的 nickName這個成員變量的,這時候能夠看一下 class_rw_t裏面的 const class_ro_t *ro

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

這個ro裏面也有method_list_t,protocol_list_tproperty_list_t,其中多了ivar_list_t。經過lldb的指令最終找到了成員變量nickName

從中能夠知道類 TestObject中有成員變量的數量爲2個,其中第一個是 nickName,這時候就有點奇怪了,咱們不是隻在 TestObject中只定義了一個 nickName這個成員變量嗎?多出來的一個是什麼呢?原來在類中定義的屬性也是會被定義爲帶下劃線的成員變量的,經過 lldb能夠看到

而且在rw中的method_array_tro中的method_list_t均可以經過獲得TestObject的方法,以下是經過lldb的指令來獲取rw中的方法

從中能夠知道類TestObject有方法數爲4個,而且第一個的方法是cxx_destruct,另外的兩個方法會不會是咱們在類中定義的實例方法sayName和類方法sayNickName,那麼仍是多出來了一個方法數量,經過lldb打印出來的

其實就是定義的屬性name的getter和setter方法的兩個方法和sayName的實例方法以及一個系統的cxx_destruct方法,並無sayNickName這個類方法。這是爲何呢?

注意:若是實例方法只是在類的`.h`文件聲明瞭,可是並無在`.m`文件中實現的話,是不在`rw`和`ro`裏面的

思考一個問題:爲何成員變量會存在class_ro_t裏面不存在class_rw_t裏面呢?class_rw_tclass_ro_t有什麼不同呢?

2.3 類方法

經過上面的能夠知道類方法是不存在類裏面的,是存在元類裏面的,能夠經過lldb指令來查找,先找到當前的類的isa,再經過isa&ISA_MASK能夠獲得元類

從中能夠知道 0x0000000100002700就是 元類的內存值,再經過 x/5gx能夠獲得 元類中的 class_data_bits_t,此時就至關於走一遍上面介紹過的查找 bits裏面的方法的流程

最終再元類中的 bits的方法中能夠查找到 sayNickName這個類方法。

3.最後

從上面的介紹能夠知道類在底層中的結構分別是isa,superclass,cache_tbits,其中它們所佔的大小是不同的。bits裏面的rw有保存着實例方法屬性,可是並無包含成員變量rw中包含着roro中保存着實例方法屬性成員變量成員變量屬性都存在ivar裏面,而且屬性是如下劃線_的形式存在的。類的類方法是存在元類的bits的方法裏面。至此,有關類的結構分析就介紹完畢了。

相關文章
相關標籤/搜索