一、初探懶加載類
上一章我們探索了 iOS
中類的加載,讓我們簡單回顧一下大概的流程。
1.1 類的加載回顧
libObjc
向dyld
註冊了回調_dyld_objc_notify_register
,當dyld
把App
以及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
名稱爲key
,SEL
爲value
) - 修複函數指針遺留
- 將所有的
Protocol
插入到readProtocol
哈希表中(插入方式爲:Protocol
名稱爲key
,Protocol
爲value
) - 對所有的
Protocol
做重映射 - 初始化所有的非懶加載類,包括
rw
和ro
的初始化操作 - 處理所有的分類(包括類的分類和元類的分類)
- 主要會進行類的加載工作,會插入 所有的類 到
1.2 驗證類的加載流程
我們大致明白了類的加載流程,接下來,讓我們在 _read_images
源碼中打印一下類加載之後的結果驗證一下是否加載了我們自己創建的類。
如上圖所示,我們增加一行代碼:
printf("_getObjc2NonlazyClassList Class:%s\n",cls->mangledName());
接着我們觀察打印結果:
忘了提一句,我們這一個有三個類: LGPerson
、 LGStudent
、 LGTeacher
但是打印出來的結果沒有 LGPerson
,這是爲什麼呢?答案看這裏,我們其實是在 LGStudent
和 LGTeacher
內部實現了 +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.m
中 LGPerson
類初始化的地方和 lookUpImpOrForward
入口處打上斷點:
Tips: 這裏有個小技巧,我們先打開
main.m
文件中的斷點,等斷點來到了我們想要探索的LGPerson
初始化的位置的時候,我們再打開lookUpImpOrForward
處的斷點,這樣才能確保當前執行lookUpImpOrForward
的是我們的研究對象LGPerson
因爲我們斷點的位置是 LGPerson
類發送 alloc
消息,而顯然 alloc
作爲類方法是存儲在元類上的,也就是說 lookUpImpOrForward
的 cls
其實是 LGPerson
元類。那麼 inst
就應該是真正的對象,可實際如下圖所示:
此時的 inst
只是一個地址,說明還沒有初始化。我們讓程序接着下面走,會來到這樣一行代碼:
這裏的 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
對象是否已經被加載了。
通過上面的打印,我們可以看到 rw
已經有值了,也就是說 LGPerson
類被加載了。
我們總結一下,如果類沒有實現 load
方法,那麼這個類就是懶加載類,其調用堆棧如下圖所示:
反之、這個類如果實現了 load
方法,那麼這個類就是非懶加載類,其調用堆棧如下圖所示:
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
是分類的名字。
我們的分類如上圖所示,定義了屬性、實例方法和類方法,剛好在底層對應了
_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
的地方:
但是我們斷點之後發現這個時候通過 unattachedCategoriesForClass
方法並沒有取到分類,我們此時不妨通過 LLDB
打印一下當前類裏面是否已經把分類的內容附加上了。
前面的流程大家都很熟悉了,我們直接看 cls
的 rw
中的 methods
是否有內容:
此時 LGTeacher
類裏面是沒有方法的,這裏讀取 rw
卻有一個結果,我們不難看出這是位於 LGTeacher+test
分類中的一個 initialize
方法,這個方法是我手動加個這個分類的。這樣進一步證明了,如果是懶加載類,並且分類也是懶加載,那麼分類的加載並不會來到 unattachedCategoriesForClass
,而是直接在編譯時加載到了類的 ro
裏面,然後在運行時被拷貝到了類的 rw
裏面。這一點可以通過下面的 LLDB
打印來證明。
如果細心的讀者可能會發現,不是在 _read_images
的最後那塊有一個 Discover categories
嗎,萬一懶加載分類是在這裏加載的呢?我們一試便知:
這裏在 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);
}
結果可以看到:
分類還是不在這,同時通過 LLDB
打印,發現分類的方法已經在類的 ro
裏面了,所以說分類的加載其實跟類的懶加載與否並沒有關係,也就是說懶加載的分類都是在編譯時期被加載的。
3.2 實現了 load 的分類
我們再接着分下下面兩種情況:
- 非懶加載分類與懶加載類
- 非懶加載分類和非懶加載類
3.2.1 與懶加載類配合加載
其實懶加載和非懶加載的最大區別就是加載是否提前,而實現了 +load
方法的分類,面對的是懶加載的類,
而懶加載的類我們前面已經知道了,是在第一次發送消息的時候纔會被加載的,那我們直接在lookupImpOrForward
=> realizeClassMaybeSwiftAndLeaveLocked
=> realizeClassMaybeSwiftMaybeRelock
=> realizeClassWithoutSwift
=> methodizeClass
流程中的 methodizeClass
打上斷點,看下在這裏分類會不會被加載:
這一次通過 unattachedCategoriesForClass
取出來值了,並且在這之前 cls
的 ro
中並沒有分類的 initialize
方法:
但是我們注意觀察此時的調用堆棧:
爲什麼走的不是發送消息的流程,而走的是 load_images
裏面的 prepare_load_methods
方法呢?我們來到 prepare_load_methods
方法處:
可以看到,其實是在這裏調用了 realizeClassWithoutSwift
方法來加載類的。而上面的 _getObjc2NonlazyCategoryList
方法顯示就是獲取的所有的非懶加載分類,然後遍歷這些非懶加載分類,然後去加載這些分類所依賴的類。這個邏輯很好理解,非懶加載分類讓我們的懶加載類實現提前了,所以說懶加載類並不一定只會在第一次消息發送的時候加載,還要取決於有沒有非懶加載的分類,如果有非懶加載的分類,那麼就走的是 load_images
裏面的 prepare_load_methods
的 realizeClassWithoutSwift
。
3.2.2 與非懶加載類配合加載
非懶加載類的流程我們也十分熟悉了,在 _read_images
裏面進行加載,而此時,分類也是非懶加載。我們還是在 methodizeClass
裏面進行斷點:
結果如上圖所示,這次從 unattachedCategoriesForClass
方法取出來的是 NULL
值,顯然分類不是在這個地方被加載的,我們回到 _read_images
方法,還記得那個 Discover categories
流程嗎,我們打開裏面的斷點:
因爲當前類已經在前面的非懶加載類加載流程中被加載完成,所以這裏會來到 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
- 非懶加載分類 + 非懶加載類
類的加載在
_read_images
處,分類的加載在類加載之後的reMethodizeClass
分類的加載探索完了,我們下一章將探索類拓展和關聯對象,敬請期待~