上一節(jié) ,我們已完整的分析分類的加載過程,知識量較大,需要慢慢消化。
本節(jié)進(jìn)行拓展和補(bǔ)充以下內(nèi)容:
-
本類與分類的+load區(qū)別 - Category分類與Extension拓展的區(qū)別
- 關(guān)聯(lián)對象
準(zhǔn)備工作:
- 可編譯的
objc4-781源碼: http://www.itdecent.cn/p/45dc31d91000
1. 本類與分類的+load區(qū)別
上一節(jié)我們的研究都是在本類和分類都實(shí)現(xiàn)+Load方法的前提下完成的。 而且attachCategories有多種被調(diào)用的路徑,具體什么情況走哪條路徑,我們不清楚。
現(xiàn)在,我們開始覆蓋性測試和探究:(ps: 下面以+load和無區(qū)分是否實(shí)現(xiàn)+load方法)
- 本類
+load,分類無- 本類
+load,分類+load- 本類
無,分類無- 本類
無,分類+load- 本類
無,分類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;
}
我們在readClass 和 attachCategories兩個函數(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)

-
attachCategories加入測試代碼和斷點(diǎn)

- 在
HTPerosn首次調(diào)用處加上斷點(diǎn):

準(zhǔn)備工作完成后,我們可以開始探索了:
1.1 本類+load,分類無
測試配置: 保留
HPPerson類的+load,注釋掉CatA和CatB分類的+load方法
- 運(yùn)行代碼,進(jìn)入了
readClass處:

提取信息如下:
- 路徑: 是
map_images調(diào)用的- ro函數(shù)列表:此時
ro讀取的是macho中的值,ro中已包含本類和所有函數(shù)信息(14個)。- 函數(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類、CatA和CatB分類的+load方法
- 運(yùn)行代碼,進(jìn)入了
readClass處:

提取信息如下:
- 路徑: 是
map_images調(diào)用的- ro函數(shù)列表:此時
ro讀取的是macho中的值,ro中僅有HTPerosn本類函數(shù)信息(8個)。
繼續(xù)運(yùn)行代碼,進(jìn)入attachCategories處:

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)入attachLists的0->1:
image.png繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)
CatA函數(shù)進(jìn)入attachLists的1->多:
(可以看到oldList是HTPerson本類的8個函數(shù),addedLists是CatA分類的3個函數(shù))
image.png繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)
CatA屬性進(jìn)入attachLists的1->多:image.png
- 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)
本類的元類函數(shù)(類方法)進(jìn)入attachLists的0->1:image.png
- 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)
CatA的元類函數(shù)(類方法)進(jìn)入attachLists的1->多: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類、CatA和CatB分類的+load方法
- 運(yùn)行代碼,進(jìn)入了
readClass處:

此時在map_images階段,macho中記錄了本類和所有分類的數(shù)據(jù)。
- 繼續(xù)運(yùn)行代碼,沒有進(jìn)入
attachCategories中。
1.4. 本類無,分類+load
測試配置: 注釋
HPPerson類的+load方法、保留CatA和CatB分類的+load方法
- 運(yùn)行代碼,進(jìn)入了
readClass處:

- 繼續(xù)運(yùn)行代碼,進(jìn)入了
attachCategories處,在attachLists加入三個斷點(diǎn),繼續(xù)運(yùn)行,發(fā)現(xiàn)attachLists中0->1加載了HTPerosn本類函數(shù)

- 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)
attachLists中0->1加載了HTPerosn本類屬性

- 繼續(xù)運(yùn)行代碼,發(fā)現(xiàn)進(jìn)入了
attachLists中`1->多:

?? 注意: 此時addedCount為2,表示當(dāng)前需要添加的列表有2個元素。并不是只有CatB分類。我們打印 addedLists[0] 和addedLists[1],就找到了CatA和CatB兩個分類
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處:

- 發(fā)現(xiàn)
ro中加載好了本類和2個分類的所有數(shù)據(jù)(14個函數(shù)),沒有再進(jìn)入attachCategories了。
本類
無,分類A+load,分類B無的結(jié)果與這個一樣
總結(jié):本類和分類的+load區(qū)別:

2. Category分類與Extension拓展的區(qū)別
2.1 Category:類別,分類
- 專門用來給類
添加新的方法 -
不能給類添加成員屬性,添加了也取不到。 - 分類中用
@property定義的變量,只會生成變量的getter和setter方法,不能生成方法實(shí)現(xiàn)和帶下劃線的成員變量。
成員屬性不可添加:@interface HTPerson(CatA) { NSString * catA_name; // 不可這樣添加 }
@property屬性可添加:@interface HTPerson(CatA) @property (nonatomic, copy) NSString *prop_name; @end編譯器
可讀取到名稱。表示有getter和setter方法的聲明。
- 運(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: 在屬性的
get和set方法實(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:

- 點(diǎn)擊進(jìn)入
get():

- 看不懂是什么...,
get()看不懂,那我們往看看它的調(diào)用者:SetAssocHook

- 我們
查看結(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:

加入
斷點(diǎn),驗(yàn)證一下,確實(shí)是給HTPerson屬性完成了賦值image.png
-
進(jìn)入
_object_set_associative_reference:
image.png 在分析
關(guān)聯(lián)對象的寫入操作前,我們先回顧一下本類的正常屬性的寫入操作:
3.1 回顧本類正常屬性寫入操作:
-
cd到main.m文件夾,clang -rewrite-objc main.mm -o main.cpp編譯一份cpp文件,打開main.cpp文件,搜索HTPerson,找到屬性name的set方法:
image.png 發(fā)現(xiàn)常規(guī)是調(diào)用
objc_setProperty完成set方法,我們在源碼中檢查objc_setProperty的實(shí)現(xiàn):

- 進(jìn)入
reallySetProperty:

- 主要流程: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ù)
-
DisguisedPtr和ObjcAssociation分別對入?yún)?code>object、policy和value進(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:

3.2.3 賦值或釋放
- 接下來到了核心執(zhí)行環(huán)節(jié)
1. 創(chuàng)建管理對象 & hashMap
AssociationsManager manager;

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.png2. 代碼結(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)對象

- 查看
setHasAssociatedObjects:

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中的空:

- 觀察內(nèi)容,此時賦值操作已完成。
2.2 value不存在(移除):
-
首先,
尋找類對:
image.png 查看
find內(nèi)部:找到了返回buckets,沒找到返回end()。

- 先找到
類對,再找到當(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:

小總結(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ù)雜的
DisguisedPtr和ObjcAssociation結(jié)構(gòu),都只是類和關(guān)聯(lián)屬性信息的一層包裝,負(fù)責(zé)記錄信息并統(tǒng)計計數(shù)而已。
至此,我們對類的加載,分類和拓展、關(guān)聯(lián)屬性,都已經(jīng)非常熟悉了。


































