iOS 底層探索 - 分類的加載

iOS 底層探索系列swift

iOS 查漏補缺系列緩存

1、初探懶加載類

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

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 源碼中打印一下類加載以後的結果驗證一下是否加載了咱們本身建立的類。markdown

image.png

如上圖所示,咱們增長一行代碼:app

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

接着咱們觀察打印結果:函數

image.png

忘了提一句,咱們這一個有三個類: LGPerson 、 LGStudent 、 LGTeacheroop

image.png

可是打印出來的結果沒有 LGPerson ,這是爲何呢?答案看這裏,咱們實際上是在 LGStudent 和 LGTeacher 內部實現了 +load 方法。而 LGPerson 則是沒有實現 +load 方法。post

1.3 懶加載類的發現

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

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

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

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

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

由於咱們斷點的位置是 LGPerson 類發送 alloc 消息,而顯然 alloc 做爲類方法是存儲在元類上的,也就是說 lookUpImpOrForward 的 cls 實際上是 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 的賦值操做以及 category 的 attatch ,關於這個方法更多內容能夠查看上一篇文章。

接着咱們返回到 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 ,也就是最終的類的加載的地方

2、分類的底層實現

分類做爲 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 實際上是一個私有屬性,但並非一直都存在的。

3、分類的加載

咱們如今知道了類分爲了 懶加載類 和 非懶加載類 ,它們的加載時機是不同的,那麼分類的加載又是怎麼樣的呢?咱們仍是一樣的先分析沒有實現 load 方法的分類的狀況:

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

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

3.1 沒有實現 load 的分類

3.1.1 與懶加載類配合加載

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

image.png

可是咱們斷點以後發現這個時候經過 unattachedCategoriesForClass 方法並無取到分類,咱們此時不妨經過 LLDB 打印一下當前類裏面是否已經把分類的內容附加上了。
前面的流程你們都很熟悉了,咱們直接看 cls 的 rw 中的 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 取出來值了,而且在這以前 cls 的 ro 中並無分類的 initialize 方法:

image.png

可是咱們注意觀察此時的調用堆棧:

image.png

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

image.png

能夠看到,實際上是在這裏調用了 realizeClassWithoutSwift 方法來加載類的。而上面的 _getObjc2NonlazyCategoryList 方法顯示就是獲取的全部的非懶加載分類,而後遍歷這些非懶加載分類,而後去加載這些分類所依賴的類。這個邏輯很好理解,非懶加載分類讓咱們的懶加載類實現提早了,因此說懶加載類並不必定只會在第一次消息發送的時候加載,還要取決於有沒有非懶加載的分類,若是有非懶加載的分類,那麼就走的是 load_images 裏面的 prepare_load_methods 的 realizeClassWithoutSwift 。

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 裏面去處理。

4、總結

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

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

這也說明分類的加載和類的加載是不同的,而結合着類的懶加載與否,咱們有如下的結論:

  • 懶加載分類 + 懶加載類

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

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

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

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

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

image.png

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

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

image.png

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

5、參考資料

objc category的祕密 - sunnyxx

相關文章
相關標籤/搜索