iOS 底層探索 - 分類的加載

在這裏插入圖片描述

一、初探懶加載類

上一章我們探索了 iOS 中類的加載,讓我們簡單回顧一下大概的流程。

1.1 類的加載回顧

  • libObjcdyld 註冊了回調 _dyld_objc_notify_register,當 dyldApp 以及 App 所依賴的一系列 Mach-O 鏡像加載到當前 App 被分配的內存空間之後,dyld 會通過 _dyld_objc_notify_mapped 也就是 map_images 來通知 libObjc 來完成具體的加載工作,map_images 被調用之後會來到 _read_images
  • _read_images
    • 主要會進行類的加載工作,會插入 所有的類gdb_objc_realized_classes 哈希表中(插入方式爲 類名爲 key,類對象爲value, 不包括通過 共享緩存 裏面的類),同時還會把類插入到 allocatedClasses 這個集合裏面,注意,allocatedClasses 的類型爲 NXHashTable,可以類比爲 NSSet,而 gdb_objc_realized_classes 的類型爲 NXMapTable,可以類比爲 NSDictionary
    • 對所有的類進行重映射
    • 將所有的 SEL 插入到 namedSelectors 哈希表中(插入方式爲:SEL 名稱爲 keySELvalue)
    • 修複函數指針遺留
    • 將所有的 Protocol 插入到 readProtocol 哈希表中(插入方式爲:Protocol 名稱爲 keyProtocolvalue)
    • 對所有的 Protocol 做重映射
    • 初始化所有的非懶加載類,包括 rwro 的初始化操作
    • 處理所有的分類(包括類的分類和元類的分類)

1.2 驗證類的加載流程

我們大致明白了類的加載流程,接下來,讓我們在 _read_images 源碼中打印一下類加載之後的結果驗證一下是否加載了我們自己創建的類。

image.png

如上圖所示,我們增加一行代碼:

printf("_getObjc2NonlazyClassList Class:%s\n",cls->mangledName());

接着我們觀察打印結果:

image.png

忘了提一句,我們這一個有三個類: LGPersonLGStudentLGTeacher

image.png

但是打印出來的結果沒有 LGPerson ,這是爲什麼呢?答案看這裏,我們其實是在 LGStudentLGTeacher 內部實現了 +load 方法。而 LGPerson 則是沒有實現 +load 方法。

1.3 懶加載類的發現

我們這個時候觀察 _read_images 源碼這部分的註釋:

Realize non-lazy classes (for +load methods and static instances)

實現非懶加載類(實現了 +load 方法和靜態實例)

什麼意思呢,我們這裏其實打印的都是所謂的非懶加載類,這裏除了我們自己實現了 +load 方法的兩個類之外,其他的內容都是系統內置的類,包括我們十分熟悉的 NSObject 類。通過這裏其實反過來推論,我們沒有實現 +load 方法就是所謂的**懶加載類,這種類並不會在 ****_read_images** 環節被加載,那麼應該是在哪裏加載呢?我們稍微思考一下,我們一般第一次操作一個類是不是在初始化這個類的時候,而初始化類不就是發送 alloc 消息嗎,而根據我們前面探索消息查找的知識,在第一次發送某個消息的時候,是沒有緩存的,所以會來到一個非常重要的方法叫 lookUpImpOrForward ,我們在 main.mLGPerson 類初始化的地方和 lookUpImpOrForward 入口處打上斷點:

Tips: 這裏有個小技巧,我們先打開 main.m 文件中的斷點,等斷點來到了我們想要探索的 LGPerson 初始化的位置的時候,我們再打開 lookUpImpOrForward 處的斷點,這樣才能確保當前執行 lookUpImpOrForward 的是我們的研究對象 LGPerson

因爲我們斷點的位置是 LGPerson 類發送 alloc 消息,而顯然 alloc 作爲類方法是存儲在元類上的,也就是說 lookUpImpOrForwardcls 其實是 LGPerson 元類。那麼 inst 就應該是真正的對象,可實際如下圖所示:

image.png

此時的 inst 只是一個地址,說明還沒有初始化。我們讓程序接着下面走,會來到這樣一行代碼:

image.png

這裏的 if 判斷通過方法名我們不難看出是隻有當 cls 未實現的時候纔會走裏面的 realizeClassMaybeSwiftAndLeaveLocked 方法,那也就是說 LGPerson 元類沒有被實現,也就是 LGPerson 類沒有實現或者說沒有被加載。

我們就順着 realizeClassMaybeSwiftAndLeaveLocked 方法往下面走走看,看到底是在哪把我們這個懶加載類給加載出來的:

static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
    lock.assertLocked();

    if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
        // Non-Swift class. Realize it now with the lock still held.
        // fixme wrong in the future for objc subclasses of swift classes
        realizeClassWithoutSwift(cls);
        if (!leaveLocked) lock.unlock();
    } else {
        // Swift class. We need to drop locks and call the Swift
        // runtime to initialize it.
        lock.unlock();
        cls = realizeSwiftClass(cls);
        assert(cls->isRealized());    // callback must have provoked realization
        if (leaveLocked) lock.lock();
    }

    return cls;
}

我們一路跟隨斷點來到了 realizeClassMaybeSwiftMaybeRelock 方法,然後我們看到了我們熟悉的一個方法 realizeClassWithoutSwift ,這個方法內部會進行 ro/rw 的賦值操作以及 categoryattatch ,關於這個方法更多內容可以查看上一篇文章。

接着我們返回到 lookUpImpOrForward 方法中來,然後進行一下 LLDB 打印,看一下當前這個 inst 也就是 LGPerson 對象是否已經被加載了。

image.png

通過上面的打印,我們可以看到 rw 已經有值了,也就是說 LGPerson 類被加載了。

我們總結一下,如果類沒有實現 load 方法,那麼這個類就是懶加載類,其調用堆棧如下圖所示:


image.png

反之、這個類如果實現了 load 方法,那麼這個類就是非懶加載類,其調用堆棧如下圖所示:

image.png

1.4 懶加載類的流程

關於非懶加載類的加載流程我們已經很熟悉了,我們總結下懶加載類的流程:

  • 類第一次發送消息的時候是沒有緩存的,所以會來到 _class_lookupMethodAndLoadCache3 ,關於這個方法我們在前面的消息查找章節已經介紹過了,不熟悉的同學可以去查閱一下。
  • _class_lookupMethodAndLoadCache3 會調用 lookUpImpOrForward ,這個方法的重要性在我們學習 Runtime 的過程中不言而喻
  • lookUpImpOrForward 內部會進行一下判斷,如果 cls 沒有被實現,會調用 realizeClassMaybeSwiftAndLeaveLocked 方法
  • realizeClassMaybeSwiftAndLeaveLocked 方法又會調用 realizeClassMaybeSwiftMaybeRelock 方法
  • realizeClassMaybeSwiftMaybeRelock 方法內部會進行一下是否是 Swift 的判斷,如果不是 Swift 環境的話,就會來到 realizeClassWithoutSwift ,也就是最終的類的加載的地方

二、分類的底層實現

分類作爲 Objective-C 中常見的特性,相信大家都不會陌生,不過在底層它是怎麼實現的呢?

2.1 重寫分類源文件

爲了探究分類的底層實現,我們只需要用 clang 的重寫命令

clang -rewrite-objc LGTeacher+test.m -o category.cpp

我們查看 category.cpp 這個文件,來到文件尾部可以看到:

static struct _category_t _OBJC_$_CATEGORY_LGTeacher_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"LGTeacher",
	0, // &OBJC_CLASS_$_LGTeacher,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_LGTeacher_$_test,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_LGTeacher_$_test,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LGTeacher_$_test,
};

我們可以看到 LGTeacher+test 分類在底層的實現是一個結構體,其名字爲 _OBJC_$_CATEGORY_LGTeacher_$_test ,很明顯這是一個按規則生成的符號,中間的 LGTeacher 是類名,後面的 test 是分類的名字。

image.png

我們的分類如上圖所示,定義了屬性、實例方法和類方法,剛好在底層對應了

  • _OBJC_$_PROP_LIST_LGTeacher_$_test
  • _OBJC_$_CATEGORY_INSTANCE_METHODS_LGTeacher_$_test
  • _OBJC_$_CATEGORY_CLASS_METHODS_LGTeacher_$_test

同時,我們在後面可以看到如下的代碼:

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_LGTeacher_$_test,
};

這表明分類是存儲在 __DATA 段的 __objc_catlist section 裏面的。

2.2 分類的定義

我們根據 _category_t 來到 libObjc 源碼中進行查找,不過我們需要去掉一下 _category_t 的下劃線,然後不難找到分類真正的定義所在:

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

根據剛纔 clang 重寫之後的內容,我們不難看出

  • name : 是分類所關聯的類,也就是類的名字,而不是分類的名字
  • cls : 我們在前面可以看到 clang 重寫後這個值爲 0,但是後面有註釋爲 &OBJC_CLASS_$_LGTeacher ,也就是我們的類對象的定義,所以這裏其實就是我們要擴展的類對象,只是在編譯期這個值並不存在
  • instanceMethods : 分類上存儲的實例方法
  • classMethods :分類上存儲的類方法
  • protocols :分類所實現的協議
  • instanceProperties :分類所定義的實例屬性,不過我們一般在分類中添加屬性都是通過關聯對象來實現的
  • _classProperties :分類所定義的類屬性。這裏有一行註釋:

Fields below this point are not always present on disk.
下面的內容並不是一直在磁盤上保存

也就是說 _classProperties 其實是一個私有屬性,但並不是一直都存在的。

三、分類的加載

我們現在知道了類分爲了 懶加載類非懶加載類 ,它們的加載時機是不一樣的,那麼分類的加載又是怎麼樣的呢?我們還是同樣的先分析沒有實現 load 方法的分類的情況:

但是我們在分析前,還要搞清楚一點,分類必須依附於類而存在,如果只有分類,沒有類,那麼從邏輯上是說不通的,就算實現了,編譯器也會忽略掉。而關於類是懶加載還是非懶加載的,所以這裏我們還要再細分一次。

  • 懶加載分類與懶加載類
  • 懶加載分類和非懶加載類

3.1 沒有實現 load 的分類

3.1.1 與懶加載類配合加載

我們先分析第一種情況,也就是類和分類都不實現 load 方法的情況。
首先,非懶加載類的流程上面我們已經探索過了,在向類第一次發送消息的時候,非懶加載類纔會開始加載,而根據我們上一章類的加載探索內容,在 realizeClassWithoutSwift 方法的最後有一個 methodizeClass 方法,在這個方法裏面會有一個 Attach categories 的地方:

image.png

但是我們斷點之後發現這個時候通過 unattachedCategoriesForClass 方法並沒有取到分類,我們此時不妨通過 LLDB 打印一下當前類裏面是否已經把分類的內容附加上了。
前面的流程大家都很熟悉了,我們直接看 clsrw 中的 methods 是否有內容:

image.png

此時 LGTeacher 類裏面是沒有方法的,這裏讀取 rw 卻有一個結果,我們不難看出這是位於 LGTeacher+test 分類中的一個 initialize 方法,這個方法是我手動加個這個分類的。這樣進一步證明了,如果是懶加載類,並且分類也是懶加載,那麼分類的加載並不會來到 unattachedCategoriesForClass ,而是直接在編譯時加載到了類的 ro 裏面,然後在運行時被拷貝到了類的 rw 裏面。這一點可以通過下面的 LLDB 打印來證明。

image.png

如果細心的讀者可能會發現,不是在 _read_images 的最後那塊有一個 Discover categories 嗎,萬一懶加載分類是在這裏加載的呢?我們一試便知:

image.png

這裏在 Discover categories 內部做了一下判斷,如果是 LGTeacher 類進來了,就打印一下,結果發現並沒有打印,說明分類也不是在這裏被加載的。

3.1.2 與非懶加載類配合加載

同樣的道理,當類爲非懶加載類的時候,走的是 _read_images 裏面的流程,這個時候我們的懶加載分類是在哪加載的呢?

我們直接在 methodizeClass 方法中打上斷點,並做了一下簡單的判斷:

    const char *cname = ro->name;
    const char *oname = "LGTeacher";
    if (strcmp(cname, oname) == 0) {
       printf("methodizeClass :%s \n",cname);
    }

結果可以看到:

image.png

分類還是不在這,同時通過 LLDB 打印,發現分類的方法已經在類的 ro 裏面了,所以說分類的加載其實跟類的懶加載與否並沒有關係,也就是說懶加載的分類都是在編譯時期被加載的。

3.2 實現了 load 的分類

我們再接着分下下面兩種情況:

  • 非懶加載分類與懶加載類
  • 非懶加載分類和非懶加載類

3.2.1 與懶加載類配合加載

其實懶加載和非懶加載的最大區別就是加載是否提前,而實現了 +load 方法的分類,面對的是懶加載的類,
而懶加載的類我們前面已經知道了,是在第一次發送消息的時候纔會被加載的,那我們直接在
lookupImpOrForward => realizeClassMaybeSwiftAndLeaveLocked => realizeClassMaybeSwiftMaybeRelock => realizeClassWithoutSwift => methodizeClass 流程中的 methodizeClass 打上斷點,看下在這裏分類會不會被加載:

image.png

這一次通過 unattachedCategoriesForClass 取出來值了,並且在這之前 clsro 中並沒有分類的 initialize 方法:

image.png

但是我們注意觀察此時的調用堆棧:

image.png

爲什麼走的不是發送消息的流程,而走的是 load_images 裏面的 prepare_load_methods 方法呢?我們來到 prepare_load_methods 方法處:

image.png

可以看到,其實是在這裏調用了 realizeClassWithoutSwift 方法來加載類的。而上面的 _getObjc2NonlazyCategoryList 方法顯示就是獲取的所有的非懶加載分類,然後遍歷這些非懶加載分類,然後去加載這些分類所依賴的類。這個邏輯很好理解,非懶加載分類讓我們的懶加載類實現提前了,所以說懶加載類並不一定只會在第一次消息發送的時候加載,還要取決於有沒有非懶加載的分類,如果有非懶加載的分類,那麼就走的是 load_images 裏面的 prepare_load_methodsrealizeClassWithoutSwift

3.2.2 與非懶加載類配合加載

非懶加載類的流程我們也十分熟悉了,在 _read_images 裏面進行加載,而此時,分類也是非懶加載。我們還是在 methodizeClass 裏面進行斷點:

image.png

結果如上圖所示,這次從 unattachedCategoriesForClass 方法取出來的是 NULL 值,顯然分類不是在這個地方被加載的,我們回到 _read_images 方法,還記得那個 Discover categories 流程嗎,我們打開裏面的斷點:

image.png

因爲當前類已經在前面的非懶加載類加載流程中被加載完成,所以這裏會來到 remethodizeClass 方法,我們進入其內部實現:

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

可以看到有一個 attachCategories 方法,斷點也確實來到了這個地方, attachCategories 方法有一段註釋:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.

將分類的方法、屬性和協議添加到類上
假設傳入的分類列表都是按加載順序加載完畢了
先加載的分類排在前面

其實 attachCategories 這個方法只會在實現了 load 方法的分類下才會被調用,而來到 attachCategories 之前又取決於類是否爲懶加載,如果是懶加載,那麼就在 load_images 裏面去處理,如果是非懶加載,那麼就在 map_images 裏面去處理。

四、總結

我們今天探索的內容可能會有點繞,不過其實探索下來,我們只需要保持研究重點就很簡單。分類的加載其實可以籠統的分爲實現 load 方法和沒有實現 load 方法:

  • 沒有實現 load 方法的分類由編譯時確定
  • 實現了 load 方法的分類由運行時去確定

這也說明分類的加載和類的加載是不一樣的,而結合着類的懶加載與否,我們有以下的結論:

  • 懶加載分類 + 懶加載類

類的加載在第一次消息發送的時候,而分類的加載則在編譯時

  • 懶加載分類 + 非懶加載類

類的加載在 _read_images 處,分類的加載還是在編譯時

  • 非懶加載分類 + 懶加載類

類的加載在 load_images 內部,分類的加載在類加載之後的 methodizeClass

image.png

  • 非懶加載分類 + 非懶加載類

類的加載在 _read_images 處,分類的加載在類加載之後的 reMethodizeClass

image.png

分類的加載探索完了,我們下一章將探索類拓展和關聯對象,敬請期待~

五、參考資料

objc category的祕密 - sunnyxx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章