iOS底層原理:類&類的結(jié)構(gòu)分析

在分析類的結(jié)構(gòu)之前义桂,我們需要先搞清楚類的內(nèi)存分布情況耿戚。

類的內(nèi)存分布

首先創(chuàng)建一個(gè)Person類:

// Person 類
@interface Person : NSObject

- (void)sayHello;
+ (void)sayHi;

@end

@implementation Person

- (void)sayHello {
    NSLog(@"Say Hello");
}

+ (void)sayHi {
    NSLog(@"Say Hi");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [[Person alloc] init];
        
        NSLog(@"%@",person);
    }
    return 0;
}
類的lldb調(diào)試分析

通過上圖中可以看出 0x00000001000012e80x00000001000012c0 為什么打印出的結(jié)果都是 Person 呢粟誓?這個(gè)東西其實(shí)就是元類

那么什么是元類呢?

元類

每一個(gè)對象都對應(yīng)一個(gè)類。Person 類就是 person 對象的類澳盐,換句話說就是 person 對象的isa指針指向 Person 對應(yīng)的結(jié)構(gòu)體;[Person class] 也是對象令宿,描述它的類就是元類叼耙,換句話說[Person class] 對象的isa指向的就是元類

元類的定義和創(chuàng)建都是由編譯器自動(dòng)完成。

元類保存了類方法的列表粒没。當(dāng)一個(gè)類方法被調(diào)用時(shí)筛婉,元類會(huì)首先查找它本身是否有該類的方法的實(shí)現(xiàn),如果沒有則該類會(huì)向它的父類查找該方法癞松,知道一直找到繼承鏈的頭爽撒。

元類分析
元類跟蹤

從上圖中我們可以看到,我們通過isa可以一直找到NSObject類响蓉。通過 person對象的isa指針可以找到Person類硕勿,通過Person的isa指針可以找到元類Person,而通過元類Person的isa指針又可以找元類Person的元類NSObject枫甲,通過元類NSObject的isa指針找到的還是NSObject源武,其實(shí)這個(gè)時(shí)候的NSObject叫做根元類

但是NSObject的類和 NSObject.class 的指針地址不一樣呢?是NSObject類會(huì)在內(nèi)存中存在多份嗎想幻?

我們通過如下代碼打印來看下:

void getClassInfo() {
    Class class1 = [Person class];
    Class class2 = [Person alloc].class;
    Class class3 = object_getClass([Person alloc]);
    
    NSLog(@"\n%p\n%p\n%p\n",class1,class2,class3);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        getClassInfo();
    }
    return 0;
}

輸出結(jié)果如下:

2020-09-15 20:49:21.406345+0800 ObjectAnalysis[34085:811450] 
0x1000012f0
0x1000012f0
0x1000012f0
Program ended with exit code: 0

可以看出其實(shí)類對象在整個(gè)內(nèi)存中只有一份粱栖。

因此我們可以得出一個(gè)著名的isa的走位圖。

isa的走位圖&繼承圖

走位圖

isa查找流程

  • 實(shí)例對象的isa指針類對象
  • 類對象的isa指向元類對象
  • 元類對象的isa指向根元類
  • 根元類的isa指向它自己本身脏毯,從而形成了閉環(huán)

繼承關(guān)系

類對象的繼承關(guān)系

  • 繼承于它的父類
  • 父類繼承它的父類
    ...
  • 直到找到根類闹究,也就是NSObject
  • NSObject 則繼承于nil,這也就是所有的根源抄沮,即無中生有

元類的繼承關(guān)系

  • 子類的元類繼承與 父類的元類
  • 父類的元類繼承它的父類的元類
    ...
  • 直到找到根元類
  • 根元類則是繼承于NSObject

tips:實(shí)例對象是沒有繼承關(guān)系的跋核,只有才有繼承關(guān)系

對象的本質(zhì)

對象的本質(zhì)其實(shí)就是結(jié)構(gòu)體岖瑰,而編譯到底層都是繼承自objc_class的結(jié)構(gòu)體

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() const {
        return bits.data();
    }

    // 代碼過多,自動(dòng)省略
    ...
};

// objc_object
struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
    Class rawISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();

    // 代碼過多砂代,自動(dòng)省略
    ...
};

通過源碼可以看出 objc_class 其實(shí)就是繼承自 objc_object蹋订。

所以我們可以看出所有的 對象元類都有isa指針刻伊,是因?yàn)槎祭^承自 objc_object露戒,而 objc_object中有個(gè)私有變量isa

objc_object捶箱、objc_class智什、isa、object丁屎、NSObject的關(guān)系如下圖所示:

關(guān)系圖

我們接下來重點(diǎn)就是分析類的結(jié)構(gòu)了

類的結(jié)構(gòu)分析

再開始前荠锭,先分析下objc_class的結(jié)構(gòu)圖

objc_class

  • isa 是指向元類的指針
  • superclass 是指向當(dāng)前類的父類
  • cache 是用于緩存方法的,用于加速方法的調(diào)用
  • bits 是存儲(chǔ)類的方法晨川、屬性证九、協(xié)議等信息的地方

一般我們獲取到bits,然后才能獲取到我們需要的信息共虑,那么我們就需要進(jìn)行32位指針的偏移愧怜,為什么是32位指針的偏移,請看下面的代碼段分析:

struct objc_class : objc_object {
    // Class ISA;  // 指針占 8 字節(jié)
    Class superclass; // 指針占 8 字節(jié)
    cache_t cache;   // 占 16 字節(jié)妈拌,具體分析拥坛,看cache_t 結(jié)構(gòu)體的分析
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }

    // 代碼過多,自動(dòng)省略
    ...
};

// mask_t 的定義
#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

// 結(jié)構(gòu)體指針字節(jié)大小尘分,看里面的成員變量猜惋,而大部分都是 static 和方法都不存在結(jié)構(gòu)體里面
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;  // 結(jié)構(gòu)體指針 8 字節(jié)
    explicit_atomic<mask_t> _mask;  // uint32_t 占 4 字節(jié)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#if __LP64__
    uint16_t _flags; // 占 2 字節(jié)
#endif
    uint16_t _occupied; // 占 2 字節(jié)

    // 方法代碼過多,自動(dòng)省略
    ...
};

class_data_bits_t

objc_class結(jié)構(gòu)體中的注釋中已經(jīng)寫到class_data_bits_t相當(dāng)于class_rw_t指針加上rr/alloc的標(biāo)志音诫。

class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

而在class_data_bits_t結(jié)構(gòu)體中為我們提供了一個(gè)用于返回其中的class_rw_t *的方法

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;

     // 代碼過多惨奕,自動(dòng)省略
    ...
public:

    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    // 代碼過多,自動(dòng)省略
    ...
};

class_rw_t

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif

    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;

    // 省略過多的代碼  
    ...

    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>()->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
        }
    }
};

可以看出竭钝,我們可以通過class_rw_t結(jié)構(gòu)體中提供的methods()properties()雹洗、protocols()獲取到對應(yīng)的方法香罐、屬性和協(xié)議。

那么接下來我們通過lldb調(diào)試來查找對應(yīng)的class_data_bist_t bits时肿,查看相應(yīng)的信息庇茫。

class_rw_t

當(dāng)我們獲取到class_rw_t結(jié)構(gòu)體信息后,接下來想要獲取到方法螃成、屬性或者協(xié)議旦签,就需要通過調(diào)用其提供的方法來進(jìn)行獲取查坪。

屬性列表 properties()

屬性信息
  • 1、通過properties()可以獲取到對應(yīng)的屬性列表
  • 2宁炫、通過get方法訪問第一個(gè)屬性偿曙,可以發(fā)現(xiàn)是我們定義的name屬性

【error】但是嘗試去獲取第二個(gè)屬性的時(shí)候,發(fā)現(xiàn)數(shù)組越界了羔巢,這是為什么呢望忆?我們不是還定義了一個(gè)hobby嗎?

由此引出成員變量竿秆、實(shí)例變量屬性的區(qū)別

成員變量/實(shí)例變量/屬性

成員變量: Member Variable declarations
類: Class (description/template for an object)
實(shí)例: Instance (manifestation of a class)
消息: Message (sent to object to make it act)
方法: Method (code invoked by a Message)
實(shí)例變量: Instance Variable (object-specific storage)
超類/子類: Superclass/Subclass (Inheritance)
協(xié)議:  Protocol (non-class-specific methods)

從上面的解釋启摄,可以看出,實(shí)例是針對而言的幽钢,歉备。實(shí)例是指類的聲明,由此推理匪燕,實(shí)例變量是指由類聲明的對象蕾羊。

而屬性和成員變量的區(qū)別在于有無setter、getter方法谎懦。

方法列表 methods()

我們還是先通過lldb調(diào)試來查找方法列表肚豺。

methods

通過上圖的lldb調(diào)試過程,我們可以發(fā)現(xiàn):

  • method_list_t中有方法的總個(gè)數(shù) count=4
  • 會(huì)優(yōu)先存儲(chǔ)我們定義的實(shí)例方法
  • 會(huì)有一個(gè)隱藏的析構(gòu)函數(shù) .cxx_destruct界拦,實(shí)現(xiàn)ARC下自動(dòng)釋放內(nèi)存的工作
  • setter吸申、getter方法的存儲(chǔ)

其實(shí)探索到這里,發(fā)現(xiàn)我們還遺留了兩個(gè)問題:
1享甸、成員變量的存儲(chǔ)
2截碴、類方法的存儲(chǔ)

成員變量的存儲(chǔ)

其實(shí)通過objc_class的源碼,我們可以知道里面其實(shí)還有個(gè)ro方法蛉威,會(huì)返回一個(gè)class_ro_t的結(jié)構(gòu)體的信息日丹。打印信息如下:

ivars

class_ro_t結(jié)構(gòu)體中其實(shí)有很多的成員變量和方法,其實(shí)class_rw_t中的ro蚯嫌,在類的編譯期就已經(jīng)確定了屬性哲虾、方法以及要遵循的協(xié)議,objc_class中的bits中的data部分存放了ro信息择示。而在runtime運(yùn)行之后束凑,準(zhǔn)確的說是在運(yùn)行runtimerealizeClass方法時(shí),會(huì)生成class_rw_t結(jié)構(gòu)體栅盲,該結(jié)構(gòu)體包含了class_ro_t汪诉,并且更新了data部分。換成了class_rw_t結(jié)構(gòu)體的地址谈秫。

由上圖的llbd打印出的ivars信息中扒寄,可以看到鱼鼓,我們生命的屬性,也會(huì)在ivars中生成一個(gè) _name的成員變量该编。這也就是我們?yōu)槭裁纯梢酝ㄟ^ _nameself.name來訪問屬性的原因了迄本。

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;

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];

    _objc_swiftMetadataInitializer swiftMetadataInitializer() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            return _swiftMetadataInitializer_NEVER_USE[0];
        } else {
            return nil;
        }
    }

    method_list_t *baseMethods() const {
        return baseMethodList;
    }

    class_ro_t *duplicate() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
            return ro;
        } else {
            size_t size = sizeof(*this);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            return ro;
        }
    }
};

類方法的存儲(chǔ)

其實(shí)我們從上面的isa走位圖中就可以知道一點(diǎn),所有的實(shí)例對象的isa都是指向其元類的上渴,那么我們是否可以查看其元類中的方法列表岸梨。下面我們通過lldb調(diào)試來看一下。

類方法

所以可以看出對象中的類方法都是存儲(chǔ)在其元類中的稠氮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末曹阔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子隔披,更是在濱河造成了極大的恐慌赃份,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奢米,死亡現(xiàn)場離奇詭異抓韩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鬓长,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門谒拴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涉波,你說我怎么就攤上這事英上。” “怎么了啤覆?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵苍日,是天一觀的道長。 經(jīng)常有香客問我窗声,道長相恃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任笨觅,我火速辦了婚禮拦耐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘见剩。我一直安慰自己揩魂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布炮温。 她就那樣靜靜地躺著,像睡著了一般牵舵。 火紅的嫁衣襯著肌膚如雪柒啤。 梳的紋絲不亂的頭發(fā)上倦挂,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機(jī)與錄音担巩,去河邊找鬼方援。 笑死,一個(gè)胖子當(dāng)著我的面吹牛涛癌,可吹牛的內(nèi)容都是我干的犯戏。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼拳话,長吁一口氣:“原來是場噩夢啊……” “哼先匪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起弃衍,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤呀非,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后镜盯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岸裙,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年速缆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了降允。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,566評論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡艺糜,死狀恐怖剧董,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情倦踢,我是刑警寧澤送滞,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站辱挥,受9級特大地震影響犁嗅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晤碘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一褂微、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧园爷,春花似錦宠蚂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春呀癣,著一層夾襖步出監(jiān)牢的瞬間美浦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工项栏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浦辨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓沼沈,卻偏偏與公主長得像流酬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子列另,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評論 2 348