iOS 底層原理-類的加載(中)

在上一篇 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 探索

cdmain.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 方法。這是因為分類中定義的屬性不會生成成員變量,只是有 settergetter 的聲明,并沒有實現(xiàn)它的 settergetter 方法。我們可以通過關(guān)聯(lián)對象來設(shè)置(objc_setAssociatedObjectobjc_getAssociatedObject)。

2. 通過 Xcode 官方文檔探索

如果不會 clang,可以通過 Xcode 文檔搜索 Category 查看

3. 通過 objc 源碼探索

打開 objc 源碼,搜索 category_t

分類的本質(zhì)是一個 _category_t 的結(jié)構(gòu)體類型,它有兩個屬性: name(類名)和 cls(本類對象);兩個方法列表:實例方法列表和類方法列表;一個協(xié)議列表;一個屬性列表。另外分類中的屬性是沒有成員變量的,只有 settergetter 的聲明,并沒有實現(xiàn) settergetter 方法。

分類數(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)用,分別是 attachToClassload_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_imagesloadAllCategories

_read_images 中的調(diào)用如下

通過調(diào)試,不會走 _read_imagesif 流程,走的是 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)完成。

最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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