OC底層原理十九:類的加載(下) 本類與分類load區(qū)別 & 關(guān)聯(lián)屬性

OC底層原理 學(xué)習(xí)大綱

上一節(jié) ,我們已完整的分析分類的加載過程,知識量較大,需要慢慢消化。

本節(jié)進(jìn)行拓展和補(bǔ)充以下內(nèi)容:

  1. 本類分類+load區(qū)別
  2. Category分類與Extension拓展的區(qū)別
  3. 關(guān)聯(lián)對象

準(zhǔn)備工作:

1. 本類分類+load區(qū)別

上一節(jié)我們的研究都是在本類分類實(shí)現(xiàn)+Load方法的前提下完成的。 而且attachCategories有多種被調(diào)用的路徑,具體什么情況走哪條路徑,我們不清楚。

現(xiàn)在,我們開始覆蓋性測試和探究:(ps: 下面以+load區(qū)分是否實(shí)現(xiàn)+load方法)

  1. 本類+load,分類
  2. 本類+load,分類+load
  3. 本類,分類
  4. 本類,分類+load
  5. 本類,分類A ,分類B+load

準(zhǔn)備階段

  • main.m文件加入測試代碼
// 本類
@interface HTPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

- (void)func1;
- (void)func3;
- (void)func2;

+ (void)classFunc;

@end

@implementation HTPerson

+ (void)load { NSLog(@"%s",__func__); };

- (void)func1 { NSLog(@"%s",__func__); };
- (void)func3 { NSLog(@"%s",__func__); };
- (void)func2 { NSLog(@"%s",__func__); };

+ (void)classFunc { NSLog(@"%s",__func__); };

@end

// 分類 CatA
@interface HTPerson (CatA)

@property (nonatomic, copy) NSString *catA_name;
@property (nonatomic, assign) int catA_age;

- (void)func1;
- (void)func3;
- (void)func2;

+ (void)classFunc;

@end

@implementation HTPerson (CatA)

+ (void)load { NSLog(@"%s",__func__); };

- (void)func1 { NSLog(@"%s",__func__); };
- (void)func3 { NSLog(@"%s",__func__); };
- (void)func2 { NSLog(@"%s",__func__); };

+ (void)classFunc { NSLog(@"%s",__func__); };

@end

// 分類 CatB
@interface HTPerson (CatB)

@property (nonatomic, copy) NSString *catB_name;
@property (nonatomic, assign) int catB_age;

- (void)func1;
- (void)func3;
- (void)func2;

+ (void)classFunc;

@end

@implementation HTPerson (CatB)

+ (void)load { NSLog(@"%s",__func__); };

- (void)func1 { NSLog(@"%s",__func__); };
- (void)func3 { NSLog(@"%s",__func__); };
- (void)func2 { NSLog(@"%s",__func__); };

+ (void)classFunc { NSLog(@"%s",__func__); };

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HTPerson * person = [HTPerson alloc];
        [person func1];
    }
    return 0;
}

我們在readClassattachCategories兩個函數(shù)內(nèi)部加入定位的測試代碼,并在printf一行加入斷點(diǎn)(確保當(dāng)前觀察的使我們的HTPerson類):

  // >>>> 測試代碼
    const char *mangledName = cls->mangledName();
    const char * HTPersonName = "HTPerson";
    if (strcmp(HTPersonName, mangledName) == 0 ) {
        auto ht_ro = (const class_ro_t *)cls->data();
        auto ht_isMeta = ht_ro->flags & RO_META;
        if (!ht_isMeta) {
            printf("%s - 精準(zhǔn)定位: %s\n", __func__, mangledName);
        }
    }
    // <<<< 測試代碼
  • readClass加入測試代碼斷點(diǎn)
readClass加入測試代碼和斷點(diǎn)
  • attachCategories加入測試代碼斷點(diǎn)
attachCategories加入測試代碼和斷點(diǎn)
  • HTPerosn首次調(diào)用處加上斷點(diǎn):
image.png

準(zhǔn)備工作完成后,我們可以開始探索了:

1.1 本類+load,分類無

測試配置: 保留HPPerson類+load,注釋掉CatACatB分類的+load方法

  • 運(yùn)行代碼,進(jìn)入了readClass處:
image.png

提取信息如下:

  1. 路徑: 是map_images調(diào)用的
  2. ro函數(shù)列表:此時ro讀取的是macho中的,ro中已包含本類和所有函數(shù)信息(14個)。
  3. 函數(shù)排序: 分類的函數(shù)不會覆蓋本類的同名函數(shù),而是后加載的分類函數(shù)排序在先加載的分類和本類前面。
  • 放開斷點(diǎn),繼續(xù)運(yùn)行,發(fā)現(xiàn)沒有進(jìn)入attachCategories內(nèi)部。

結(jié)論:【本類+load,分類無】的情況:數(shù)據(jù)在編譯層已經(jīng)加入data中。

1.2. 本類+load,分類+load

測試配置: 保留HPPerson類、CatACatB分類的+load方法

  • 運(yùn)行代碼,進(jìn)入了readClass處:
image.png

提取信息如下:

  1. 路徑: 是map_images調(diào)用的
  2. ro函數(shù)列表:此時ro讀取的是macho中的,ro中僅有HTPerosn本類函數(shù)信息(8個)。

繼續(xù)運(yùn)行代碼,進(jìn)入attachCategories處:

image.png

attachLists拓展:

此處可觀察到attachLists加載順序,驗(yàn)證上一節(jié)attachLists的分析

  • 我們在attachLists加入三個斷點(diǎn),檢查排序。

  • 運(yùn)行代碼,發(fā)現(xiàn)第一次是從extAllocIfNeeded初始化rwe時進(jìn)入,從macho中只存儲了本類信息,由于當(dāng)前是首次創(chuàng)建,所以attachLists走的是0->1的流程,是直接將addLists[0]賦值給了list

    image.png

  • 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)是本類的屬性進(jìn)入attachLists0->1

    image.png

  • 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)CatA函數(shù)進(jìn)入attachLists1->多
    (可以看到oldListHTPerson本類8個函數(shù),addedListsCatA分類3個函數(shù))

    image.png

  • 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)CatA屬性進(jìn)入attachLists1->多

image.png
  • 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)本類的元類函數(shù)(類方法)進(jìn)入attachLists0->1
image.png
  • 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)CatA的元類函數(shù)(類方法)進(jìn)入attachLists1->多
image.png
  • 繼續(xù)運(yùn)行代碼,又回到了attachCategories處,我們繼續(xù)運(yùn)行代碼,進(jìn)入CatB函數(shù)進(jìn)入attachLists->更多:
image.png
  • 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)CatB屬性進(jìn)入attachLists多->更多

    image.png

  • 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)CatB的元類函數(shù)(類方法)進(jìn)入attachLists多->更多

    image.png

總結(jié):


image.png

1.3. 本類無,分類無

測試配置: 注釋HPPerson類、CatACatB分類的+load方法

  • 運(yùn)行代碼,進(jìn)入了readClass處:
image.png

此時在map_images階段,macho中記錄了本類所有分類數(shù)據(jù)

  • 繼續(xù)運(yùn)行代碼,沒有進(jìn)入attachCategories中。

1.4. 本類無,分類+load

測試配置: 注釋HPPerson類+load方法、保留CatACatB分類的+load方法

  • 運(yùn)行代碼,進(jìn)入了readClass處:
image.png
  • 繼續(xù)運(yùn)行代碼,進(jìn)入了attachCategories處,在attachLists加入三個斷點(diǎn),繼續(xù)運(yùn)行,發(fā)現(xiàn)attachLists0->1加載了HTPerosn本類函數(shù)
image.png
  • 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)attachLists0->1加載了HTPerosn本類屬性
image.png
  • 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)進(jìn)入了attachLists中`1->多:
image.png

?? 注意: 此時addedCount2,表示當(dāng)前需要添加的列表有2個元素。并不是只有CatB分類。我們打印 addedLists[0]addedLists[1],就找到了CatACatB兩個分類

Q: 為什么本類沒有+load方法,只實(shí)現(xiàn)分類+load方法,也在app啟動前加載出來了呢?

A: 我們查看左邊堆棧,load_images調(diào)用了prepare_load_methods

image.png

  • prepare_load_methods中會檢查有沒有非懶加載的分類,如果有就執(zhí)行下面的循環(huán)。
    循環(huán)中在add_category_to_loadable_list加載分類前,會執(zhí)行realizeClassWithoutSwift先檢查本類是否實(shí)現(xiàn)。
image.png

1.5 本類,分類A ,分類B+load

測試配置: 注釋HPPerson類CatA分類的+load方法,保留CatB分類的+load方法

  • 運(yùn)行代碼,進(jìn)入了readClass處:
image.png
  • 發(fā)現(xiàn)ro加載好本類和2個分類所有數(shù)據(jù)(14個函數(shù)),沒有再進(jìn)入attachCategories了。

本類,分類A+load ,分類B 的結(jié)果與這個一樣


總結(jié):本類和分類的+load區(qū)別:

image.png


2. Category分類與Extension拓展的區(qū)別

2.1 Category:類別,分類

  • 專門用來給類添加新的方法
  • 不能給類添加成員屬性,添加了也取不到。
  • 分類中用@property定義的變量,只會生成變量的 gettersetter方法,不能生成方法實(shí)現(xiàn)帶下劃線成員變量。
  • 成員屬性不可添加:
@interface HTPerson(CatA) {
    NSString * catA_name; // 不可這樣添加
}
  • @property屬性可添加:
@interface HTPerson(CatA)
@property (nonatomic, copy) NSString *prop_name;
@end

編譯器可讀取名稱。表示有gettersetter方法的聲明。

  • 運(yùn)行后會crash。是因?yàn)?code>沒有實(shí)現(xiàn)和帶下劃線成員變量
    image.png

2.2 Extension:類拓展

  • 可以說成是特殊的分類,已稱作匿名分類
  • 可以給類添加成員屬性、屬性方法,但都是私有

拓展必須添加在@interface聲明@implementation實(shí)現(xiàn)之間:

image.png

  • Extension拓展@interface聲明是一樣的作用,但是Extension拓展中的成員變量、屬性、方法都是私有的。
  • 可以通過clang,查看編譯結(jié)果進(jìn)行驗(yàn)證。Extension類拓展下劃線成員變量函數(shù)等,都直接加入本類相關(guān)位置,完成相應(yīng)實(shí)現(xiàn)。

Q: Category中的屬性如何用runtime實(shí)現(xiàn)?

  • A: 在屬性的getset方法實(shí)現(xiàn)內(nèi),動態(tài)添加關(guān)聯(lián)對象
// CatA分類
#import <objc/runtime.h>
// 本類
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation HTPerson
@end

// CatA分類
@interface HTPerson (CatA)
@property (nonatomic, copy) NSString *catA_name; // 屬性
@end

@implementation HTPerson(CatA)

- (void)setCatA_name:(NSString *)catA_name { // 給屬性`catA_name`,動態(tài)添加set方法
    objc_setAssociatedObject(self, "catA_name", catA_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)catA_name { // 給屬性`catA_name`,動態(tài)添加get方法
    return objc_getAssociatedObject(self, "catA_name");
}
@end

參數(shù)解讀:

  • 動態(tài)設(shè)置關(guān)聯(lián)屬性: objc_setAssociatedObject(關(guān)聯(lián)對象,關(guān)聯(lián)屬性key,關(guān)聯(lián)屬性value策略)
  • 動態(tài)讀取關(guān)聯(lián)屬性:objc_getAssociatedObject(關(guān)聯(lián)對象,關(guān)聯(lián)屬性key)

3. 關(guān)聯(lián)對象

  • 點(diǎn)擊進(jìn)入objc_setAssociatedObject:
image.png
  • 點(diǎn)擊進(jìn)入get():
image.png
  • 看不懂是什么..., get()看不懂,那我們往看看它的調(diào)用者:SetAssocHook
image.png
  • 我們查看結(jié)構(gòu),發(fā)現(xiàn)它就是嵌套了一層objc_hook_setAssociatedObject的方法。調(diào)用get(),就是讀取內(nèi)容。所以:
SetAssocHook.get()(object, key, value, policy);

可以直接寫成

_base_objc_setAssociatedObject(object, key, value, policy);
  • 我們進(jìn)入_base_objc_setAssociatedObject
image.png

加入斷點(diǎn),驗(yàn)證一下,確實(shí)是給HTPerson屬性完成了賦值

image.png
  • 進(jìn)入_object_set_associative_reference

    image.png

  • 在分析關(guān)聯(lián)對象寫入操作前,我們先回顧一下本類的正常屬性的寫入操作:

3.1 回顧本類正常屬性寫入操作:

  • cdmain.m文件夾,clang -rewrite-objc main.mm -o main.cpp編譯一份cpp文件,打開main.cpp文件,搜索HTPerson,找到屬性nameset方法:

    image.png

  • 發(fā)現(xiàn)常規(guī)是調(diào)用objc_setProperty完成set方法,我們在源碼中檢查objc_setProperty的實(shí)現(xiàn):

image.png
  • 進(jìn)入reallySetProperty
image.png
  • 主要流程:1. 通過地址讀取屬性 -> 2.新值retain -> 3.屬性賦值 -> 4.舊值release

熟悉了常規(guī)屬性寫入流程。 現(xiàn)在我們來對比關(guān)聯(lián)對象寫入操作

3.2 關(guān)聯(lián)對象寫入操作:

我們回到_object_set_associative_reference流程:

3.2.1 記錄數(shù)據(jù)
  • DisguisedPtrObjcAssociation分別對入?yún)?code>object、policyvalue進(jìn)行了包裝。
  • 查看DisguisedPtr結(jié)構(gòu),只有一個value。 所以實(shí)際是將入?yún)?code>object對象給到DisguisedPtr對象的value,包裝記錄一下。
image.png
  • 查看ObjcAssociation結(jié)構(gòu),只有_policy_value。 所以實(shí)際是將入?yún)?code>policy策略和value新值給到ObjcAssociation對象,包裝記錄一下。
image.png
3.2.2 新值retain
  • 接下來查看acquireValue(),發(fā)現(xiàn)是完成了新值retain:
image.png
3.2.3 賦值或釋放
  • 接下來到了核心執(zhí)行環(huán)節(jié)
1. 創(chuàng)建管理對象 & hashMap
   AssociationsManager manager;
image.png

Q: 這樣真的創(chuàng)建了對象嗎?

  • 我們創(chuàng)建HTObjc進(jìn)行測試,打印結(jié)果顯示,確實(shí)是構(gòu)造析構(gòu)函數(shù):
    image.png
  • AssociationsManager結(jié)構(gòu)中,manager只是對外代言人,并不是唯一的,AssociationsHashMap才是唯一的。

1. 運(yùn)行驗(yàn)證:
移除鎖,這樣可以同時存在2個manager了。

image.png

  • 加入測試代碼,創(chuàng)建2個manager,都調(diào)用get(),發(fā)現(xiàn)2個讀取的associations相同地址。
  • 證明AssociationsHashMap在內(nèi)存中是獨(dú)一份的,而manager只是外層包裝,可以創(chuàng)建多個。
    image.png

2. 代碼結(jié)構(gòu)分析:

  • 進(jìn)入get(),發(fā)現(xiàn)是調(diào)用的_storage

    image.png

  • 返回查看_storage,發(fā)現(xiàn)是static靜態(tài)聲明。所以AssociationsHashMap確實(shí)是內(nèi)存中獨(dú)一份

    image.png

2 關(guān)聯(lián)值value是否存在

2.1 value存在(賦值)

  • 返回結(jié)構(gòu)如下:


    image.png
  • try_emplace創(chuàng)建空ObjectAssociationMap查詢的鍵值對

  • 進(jìn)入try_emplace查看源碼:(不管是否存在,都會返回true)

    image.png

運(yùn)行代碼。斷點(diǎn)查詢,發(fā)現(xiàn)沒有這個key插入一個空的BucketT進(jìn)去并返回true

  • 進(jìn)入LookupBucketFor,發(fā)現(xiàn)有兩個同名方法,是重載方法,唯一區(qū)別是第二個入?yún)?/code>的是否有const

    image.png

  • 我們觀察外部try_emplace源碼,入?yún)?code>TheBucket是沒有const聲明的,所以進(jìn)入的是第二個LookupBucketFor:

    image.png

  • 回到第一個LookupBucketFor,循環(huán)查找key對應(yīng)的buckets:

    image.png

  • 通過setHasAssociatedObjects標(biāo)記對象存在關(guān)聯(lián)對象

image.png
  • 查看setHasAssociatedObjects:
image.png

Q:請問關(guān)聯(lián)對象是否需要手動釋放
A:指針優(yōu)化的isa中的has_assoc記錄了是否有關(guān)聯(lián)屬性,在析構(gòu)函數(shù)觸發(fā)時,會檢查是否有關(guān)聯(lián)屬性主動釋放。

image.png
  • 查看hasAssociatedObjects
    image.png
  • 繼續(xù)往下執(zhí)行,我們在第二次try_emplace前后檢查refs:

  • 第二次try_emplace前:插入的Bucktes是空桶,所以還沒值:

    image.png

  • 第二次try_emplace后:插入的Bucktes已經(jīng)有值了:

    image.png

  • 往下走,到達(dá)association.swap(result.first->second)時,我們用當(dāng)前policy策略value值組成了一個ObjcAssociation替換原來BucketT中的空:

image.png
  • 觀察內(nèi)容,此時賦值操作已完成。
2.2 value不存在(移除):
  • 首先,尋找類對

    image.png

  • 查看find內(nèi)部:找到了返回buckets沒找到返回end()。

image.png
  • 先找到類對,再找到當(dāng)前類的關(guān)聯(lián)屬性對,將當(dāng)前關(guān)聯(lián)屬性對質(zhì)空,buckets計數(shù)更新
    image.png
3.2.4 舊值release
  • 接下來查看releaseHeldValue(),發(fā)現(xiàn)是完成了舊值retain:
image.png

小總結(jié):

  • AssociationsHashMap內(nèi)有多個類對key-value結(jié)構(gòu),而每個類對應(yīng)的value,又包含多個關(guān)聯(lián)屬性對key-value結(jié)構(gòu)。
  • 所以我們不管插入還是移除,都是先通過信息找到相應(yīng)的類對,再從類對value中,通過關(guān)聯(lián)屬性key找到對應(yīng)的關(guān)聯(lián)屬性,進(jìn)行相應(yīng)操作。

其中復(fù)雜的DisguisedPtrObjcAssociation結(jié)構(gòu),都只是關(guān)聯(lián)屬性信息的一層包裝,負(fù)責(zé)記錄信息統(tǒng)計計數(shù)而已。


至此,我們對類的加載,分類和拓展、關(guān)聯(lián)屬性,都已經(jīng)非常熟悉了。

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

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