在上一篇 iOS 底層原理-類的加載(上) 分析了 map_images 的流程,那么問題來了,一個應(yīng)用程序有很多的類,如果都按照這個流程走,太耗費(fèi)性能了,這是蘋果不允許的,那么它真實的流程是什么樣子呢?下面我們一起來探索下
如何定位當(dāng)前研究的類
我們知道一個應(yīng)用程序運(yùn)行需要加載很多的類文件,那么我們?nèi)绾稳ザㄎ坏轿覀円芯康闹付ǖ念惸??這里我們需要對源碼進(jìn)行一些處理,讓它只針對我們需要研究的類。上一篇中講到 objc 源碼通過 cls->mangledName() 來獲取類名,那么我們判斷自定義研究的類名與 mangledName 是否一致,如果一致,則就是我們需要研究的,反之,則不需要研究。如下
const char *mangledName = cls->mangledName();
const char *LCPersonName = "LCPerson";
if (strcmp(mangledName, LCPersonName) == 0) {
bool lc_isMeta = cls->isMetaClass(); // 用來判斷是否是元類,排除干擾(如果需要)
if (!lc_isMeta) {
printf("%s: 這個是我要研究的 %s \n", __func__, LCPersonName);
}
}
這樣我們就可以避免了其他類的干擾,只關(guān)注我們自定義的類。核心的方法在上一篇中提到了,只需要將自定義的代碼添加上去就可以了
懶加載與非懶加載類
在 ObjC 中,判斷一個類是否是懶加載類,就是看它是否實現(xiàn)了 +load 方法
實現(xiàn)了
+load方法,它就是非懶加載類反之,就是懶加載類
+load方法會提前加載(+load會在load_images調(diào)用,前提是類存在,這個會在后面進(jìn)行驗證)。如果沒實現(xiàn)+load方法,會在第一次調(diào)用方法時加載
實現(xiàn) load 方法的類加載
創(chuàng)建一個 LCPerson 類,聲明并實現(xiàn)一個實例方法以及重寫 +load 方法
@interface LCPerson : NSObject
- (void)lc_instanceMethod1;
@end
@implementation LCPerson
+ (void)load {
NSLog(@"%s", __func__);
}
- (void)lc_instanceMethod1 {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello!");
}
return 0;
}
運(yùn)行 objc-781 源碼,查看打印結(jié)果,如下

非懶加載的流程圖如下

未實現(xiàn) load 方法的類加載
刪除 +load 方法,此時我們運(yùn)行 objc-781 源碼,發(fā)現(xiàn)只會打印 readClass 方法,那么它是什么時候加載的呢?現(xiàn)在我們在 main 函數(shù)中添加如下代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [LCPerson alloc];
[person lc_instanceMethod1];
NSLog(@"Hello!");
}
return 0;
}
1. 斷點(diǎn)調(diào)試
此時在 main 函數(shù)處打個斷點(diǎn),我們運(yùn)行 objc-781 源碼,調(diào)用了 readClass 方法后直接來到斷點(diǎn)處,再次執(zhí)行下一步,可以看到調(diào)用流程如下

2.堆棧信息
我們在 realizeClassWithoutSwift 處打個斷點(diǎn),運(yùn)行 objc-781 源碼,查看堆棧信息

方法調(diào)用流程為什么能來 realizeClassWithoutSwift?,本質(zhì)上調(diào)用 alloc,alloc 的本質(zhì)是消息的發(fā)送。因為是第一次調(diào)用,會走消息的慢速查找 lookUpImpOrForward,類沒有初始化,會調(diào)用 realizeClassMaybeSwiftAndLeaveLocked,后續(xù)調(diào)用 realizeClassMaybeSwiftMaybeRelock,最后調(diào)用 realizeClassWithoutSwift,后面是實現(xiàn)類。
懶加載類的流程圖如下

分類
分類是什么?要研究分類,首先我們需要知道分類是什么,怎么研究呢?可以從下面三種方法探索,首先在 main 文件中定義個分類,如下
@interface LCPerson (LC)
@property (nonatomic, copy) NSString *lc_name;
@property (nonatomic, assign) int lc_age;
- (void)lc_instanceMethod3;
- (void)lc_instanceMethod1;
- (void)lc_instanceMethod2;
@end
@implementation LCPerson (LC)
- (void)lc_instanceMethod3 {
NSLog(@"%s",__func__);
}
- (void)lc_instanceMethod1 {
NSLog(@"%s",__func__);
}
- (void)lc_instanceMethod2 {
NSLog(@"%s",__func__);
}
+ (void)lc_sayClassMethod {
NSLog(@"%s",__func__);
}
@end
1. 通過 clang 探索
cd 到 main.m 所在的文件夾,執(zhí)行 clang -rewrite-objc main.m -o main.cpp,文件夾中會多出一個 main.cpp 類文件,打開 main.cpp,就可以看到底層編譯,分類的類型是 _category_t 結(jié)構(gòu)體

搜索 struct _category_t,看它的結(jié)構(gòu)如下
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
從上面對應(yīng)關(guān)系可以看出,由于 LCPerson (LC) 沒有協(xié)議,所以對應(yīng)的為 0,結(jié)構(gòu)體中有兩個 _method_list_t,分別表示的是 實例方法列表 和 類方法列表。搜索 _CATEGORY_INSTANCE_METHODS_LCPerson_ 查看它的實例方法列表底層實現(xiàn)

對應(yīng)分類中我們添加的三個實例方法,其格式為 sel + 簽名 + 地址,看著很熟悉是不是?就是 method_t 結(jié)構(gòu)體的屬性
struct method_t {
SEL name; // 方法名
const char *types; // 方法簽名
MethodListIMP imp; // 函數(shù)地址
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
我們再來看下分類的屬性列表

可以看到,我們在分類里定義了屬性,但是在底層編譯中并沒有看到它的成員變量,而且在實例方法列表中也沒有看到屬性的 setter 以及 getter 方法。這是因為分類中定義的屬性不會生成成員變量,只是有 setter 和 getter 的聲明,并沒有實現(xiàn)它的 setter 和 getter 方法。我們可以通過關(guān)聯(lián)對象來設(shè)置(objc_setAssociatedObject 和 objc_getAssociatedObject)。
2. 通過 Xcode 官方文檔探索
如果不會 clang,可以通過 Xcode 文檔搜索 Category 查看

3. 通過 objc 源碼探索
打開 objc 源碼,搜索 category_t

分類的本質(zhì)是一個
_category_t的結(jié)構(gòu)體類型,它有兩個屬性:name(類名)和cls(本類對象);兩個方法列表:實例方法列表和類方法列表;一個協(xié)議列表;一個屬性列表。另外分類中的屬性是沒有成員變量的,只有setter和getter的聲明,并沒有實現(xiàn)setter和getter方法。
分類數(shù)據(jù)的加載時機(jī)
在上一篇 iOS 底層原理-類的加載(上) 中分析了分類數(shù)據(jù)的加載是在 attachCategories 方法中實現(xiàn)的,且分類的加載順序是根據(jù)編譯器編譯的先后順序加載到類中,越晚加進(jìn)來,越在前面。
但是它在什么時機(jī)調(diào)用的,我們還不得而知,下面就讓我們就一起探索下吧
什么時機(jī)調(diào)用
下面我們通過反推法和堆棧信息兩種方法去探索
1. 反推法
- 在
objc源碼中全局搜索attachCategories(,發(fā)現(xiàn)只有兩處調(diào)用,分別是attachToClass和load_categories_nolock

通過調(diào)試發(fā)現(xiàn)不會走 attachToClass 中的 attachCategories(這里我們設(shè)置的主類和分類都實現(xiàn)了 +load 方法,如果主類未實現(xiàn) +load 方法,分類有實現(xiàn) +load 方法,則會調(diào)用 attachToClass 中的 attachCategories,后面會分析到)

- 全局搜索
load_categories_nolock的調(diào)用,發(fā)現(xiàn)有兩處調(diào)用_read_images和loadAllCategories
_read_images 中的調(diào)用如下

通過調(diào)試,不會走 _read_images 的 if 流程,走的是 loadAllCategories 的流程

- 再次全局搜索
loadAllCategories的調(diào)用,發(fā)現(xiàn)只有一次調(diào)用,是在load_images時調(diào)用的

2. 堆棧信息
現(xiàn)在我們在 attachCategories 中加上自定義的斷點(diǎn),bt 查看它的堆棧

這里也驗證了我們剛剛反推的流程,反推流程和正常流程圖如下

分類與類的搭配使用(+load 方法實現(xiàn)與否)
上面我們分析了主類的懶加載與非懶加載,下面我們看下它們搭配使用(+load 實現(xiàn)與否)的加載情況。大致可以分為四種情況
| 分類實現(xiàn) +load | 分類未實現(xiàn) +load | |
|---|---|---|
| 主類實現(xiàn) +load | 非懶加載類 + 非懶加載分類 | 非懶加載類 + 懶加載分類 |
| 主類未實現(xiàn) +load | 懶加載類 + 非懶加載分類 | 懶加載類 + 懶加載分類 |
主類源碼
/*------ .h ------**/
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;
@property (nonatomic, assign) int kc_age;
- (void)kc_instanceMethod1;
- (void)kc_instanceMethod2;
- (void)kc_instanceMethod3;
+ (void)kc_sayClassMethod;
@end
/*------ .m ------**/
#import "LGPerson.h"
@implementation LGPerson
+ (void)load {
}
- (void)kc_instanceMethod3{
NSLog(@"%s",__func__);
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)kc_instanceMethod2{
NSLog(@"%s",__func__);
}
+ (void)kc_sayClassMethod{
NSLog(@"%s",__func__);
}
@end
分類源碼
/*------分類LGA.h ------**/
@interface LGPerson (LGA)
- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;
@end
/*------分類LGA.m ------**/
#import "LGPerson+LGA.h"
@implementation LGPerson (LGA)
+ (void)load{
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateA_2{
NSLog(@"%s",__func__);
}
- (void)cateA_1{
NSLog(@"%s",__func__);
}
- (void)cateA_3{
NSLog(@"%s",__func__);
}
@end
/*------分類LGB.h ------**/
@interface LGPerson (LGB)
- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;
@end
/*------分類LGB.m ------**/
#import "LGPerson+LGB.h"
@implementation LGPerson (LGB)
+ (void)load{
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateB_2{
NSLog(@"%s",__func__);
}
- (void)cateB_1{
NSLog(@"%s",__func__);
}
- (void)cateB_3{
NSLog(@"%s",__func__);
}
@end
非懶加載類與非懶加載分類
這種情況是 主類實現(xiàn)了 +load 方法,分類也實現(xiàn)了 +load 方法,前面我們分析的就是這種情況,我們可以得出如下結(jié)論
- 類的數(shù)據(jù)加載是在
_read_images中調(diào)用_getObjc2NonlazyClassList加載,插入表操作,ro、rw 的操作。調(diào)用路徑:
map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ,后面會走 load_images 方法
- 分類的數(shù)據(jù)加載是通過
load_images加載到類中的,調(diào)用路徑為
load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists
通過我們自定義的打印數(shù)據(jù),運(yùn)行程序,打印日志如下

非懶加載類與懶加載分類
這種情況是 主類實現(xiàn)了 +load 方法,分類沒有實現(xiàn) +load 方法
- 首先,我們在
realizeClassWithoutSwift的自定義代碼中下個斷點(diǎn),查看ro情況

從上面可以看到,方法列表中有 16 個方法,但是在主類中沒有這么多,那剩余的方法是從哪里來的呢?我們通過 lldb 命令一一打印出方法列表中的方法


從上面的打印信息可以看出,除了主類的方法外,分類的方法也被加載進(jìn)來了,依次是 LGA->LGB->LGPerson。方法還沒有排序,說明分類的數(shù)據(jù)沒有進(jìn)行非懶加載時,通過 cls->data() 讀取到 mach-o 可執(zhí)行文件時,數(shù)據(jù)就已經(jīng)進(jìn)來了,不需要在運(yùn)行時添加進(jìn)去了
- 下面我們進(jìn)入
methodizeClass方法中查看排序后的方法列表數(shù)據(jù)

通過打印發(fā)現(xiàn),方法排序只對 同名方法進(jìn)行了排序,而類中的其他方法則是按照 imp地址有序排列,排序的源碼如下(核心代碼)
static void
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
bool baseMethods, bool methodsFromBundle)
{
for (int i = 0; i < addedCount; i++) {
method_list_t *mlist = addedLists[I];
ASSERT(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
}
}
??
static void
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
// sel - imp
// Sort by selector address.
if (sort) {
method_t::SortBySELAddress sorter;
std::stable_sort(mlist->begin(), mlist->end(), sorter);
}
// Mark method list as uniqued and sorted
mlist->setFixedUp();
}
通過我們自定義的打印數(shù)據(jù),運(yùn)行程序,打印日志如下

懶加載類與懶加載分類
這種情況是 主類和分類都沒有實現(xiàn) +load 方法,這里我們需要在 main 函數(shù)中調(diào)用類的實例方法來輔助,添加代碼如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person kc_instanceMethod1];
}
return 0;
}
- 我們先通過添加我們自定義的打印,不加任何斷點(diǎn),直接運(yùn)行程序,看下打印日志

其中的 realizeClassMaybeSwiftMaybeRelock 方法調(diào)用是消息發(fā)送流程的慢速查找的函數(shù),在第一次發(fā)送消息時才走的函數(shù)
- 我們在
readClass處下個斷點(diǎn),讀取此時的ro情況

此時的 baseMethodList 的個數(shù)是 16 個,說明也是從 data 中讀取出來的
懶加載類 與 非懶加載分類
這種情況是 主類沒有實現(xiàn) +load 方法,分類實現(xiàn)了 +load 方法
- 我們先運(yùn)行程序,獲取打印日志

- 我們在
readClass處打個斷點(diǎn),查看ro情況

可以看到,baseMethodList 的 count 是 8 個,我們打印出每個方法如下

可以看到方法列表里是 LGPerson 的三個實例方法和屬性的 setter、getter 方法以及 1 個 cxx 方法,說明 ro 中只有主類的數(shù)據(jù)。那么怎么查看分類的數(shù)據(jù)呢?為了調(diào)試分類的數(shù)據(jù)加載,繼續(xù)往下執(zhí)行:load_images -> loadAllCategories -> load_categories_nolock。打印此時的堆棧信息

繼續(xù)執(zhí)行,在 attachToClass 方法打個斷點(diǎn),繼續(xù)點(diǎn)擊下一步,走到 attachCategories

主類未實現(xiàn)
+load,分類實現(xiàn)了+load,會迫使主類提前加載,即主類強(qiáng)行轉(zhuǎn)換為非懶加載類樣式
總結(jié)
類和分類搭配使用,其數(shù)據(jù)的加載時機(jī)總結(jié)如下:
非懶加載類 + 非懶加載分類:類的數(shù)據(jù)加載是在 _read_images 中調(diào)用 _getObjc2NonlazyClassList 加載;分類的數(shù)據(jù)加載是通過 load_images 加載到類中的懶加載類 + 非懶加載分類:分類實現(xiàn)了+load,會迫使主類提前加載,即在_read_images中不會對類做實現(xiàn)操作,在load_images方法中觸發(fā)類的數(shù)據(jù)加載,同時加載分類數(shù)據(jù)。

非懶加載類 + 懶加載分類:數(shù)據(jù)加載在read_image就加載數(shù)據(jù),數(shù)據(jù)來自data,data在編譯時期就已經(jīng)完成,即data中除了類的數(shù)據(jù),還有分類的數(shù)據(jù),與類綁定在一起。懶加載類 + 懶加載分類:其數(shù)據(jù)加載推遲到 第一次消息時,數(shù)據(jù)同樣來自data,data在編譯時期就已經(jīng)完成。
