iOS 底層探索系列swift
- iOS 底層探索 - alloc & init
- iOS 底層探索 - calloc 和 isa
- iOS 底層探索 - 類
- iOS 底層探索 - cache_t
- iOS 底層探索 - 方法
- iOS 底層探索 - 消息查找
- iOS 底層探索 - 消息轉發
- iOS 底層探索 - 應用加載
- iOS 底層探索 - 類的加載
- iOS 底層探索 - 分類的加載
- iOS 底層探索 - 類拓展和關聯對象
- iOS 底層探索 - KVC
- iOS 底層探索 - KVO
iOS 查漏補缺系列緩存
上一章咱們探索了 iOS
中類的加載,讓咱們簡單回顧一下大概的流程。bash
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
的初始化操做咱們大體明白了類的加載流程,接下來,讓咱們在 _read_images
源碼中打印一下類加載以後的結果驗證一下是否加載了咱們本身建立的類。markdown
如上圖所示,咱們增長一行代碼:app
printf("_getObjc2NonlazyClassList Class:%s\n",cls->mangledName()); 複製代碼
接着咱們觀察打印結果:函數
忘了提一句,咱們這一個有三個類: LGPerson
、 LGStudent
、 LGTeacher
oop
可是打印出來的結果沒有 LGPerson
,這是爲何呢?答案看這裏,咱們實際上是在 LGStudent
和 LGTeacher
內部實現了 +load
方法。而 LGPerson
則是沒有實現 +load
方法。post
咱們這個時候觀察 _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
就應該是真正的對象,可實際以下圖所示:
此時的 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
方法,那麼這個類就是非懶加載類,其調用堆棧以下圖所示:
關於非懶加載類的加載流程咱們已經很熟悉了,咱們總結下懶加載類的流程:
_class_lookupMethodAndLoadCache3
,關於這個方法咱們在前面的消息查找章節已經介紹過了,不熟悉的同窗能夠去查閱一下。_class_lookupMethodAndLoadCache3
會調用 lookUpImpOrForward
,這個方法的重要性在咱們學習 Runtime
的過程當中不言而喻lookUpImpOrForward
內部會進行一下判斷,若是 cls
沒有被實現,會調用 realizeClassMaybeSwiftAndLeaveLocked
方法realizeClassMaybeSwiftAndLeaveLocked
方法又會調用 realizeClassMaybeSwiftMaybeRelock
方法realizeClassMaybeSwiftMaybeRelock
方法內部會進行一下是不是 Swift
的判斷,若是不是 Swift
環境的話,就會來到 realizeClassWithoutSwift
,也就是最終的類的加載的地方分類做爲 Objective-C
中常見的特性,相信你們都不會陌生,不過在底層它是怎麼實現的呢?
爲了探究分類的底層實現,咱們只須要用 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 裏面的。
咱們根據 _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
方法的分類的狀況:
可是咱們在分析前,還要搞清楚一點,分類必須依附於類而存在,若是隻有分類,沒有類,那麼從邏輯上是說不通的,就算實現了,編譯器也會忽略掉。而關於類是懶加載仍是非懶加載的,因此這裏咱們還要再細分一次。
咱們先分析第一種狀況,也就是類和分類都不實現 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
類進來了,就打印一下,結果發現並無打印,說明分類也不是在這裏被加載的。
一樣的道理,當類爲非懶加載類的時候,走的是 _read_images
裏面的流程,這個時候咱們的懶加載分類是在哪加載的呢?
咱們直接在 methodizeClass
方法中打上斷點,並作了一下簡單的判斷:
const char *cname = ro->name; const char *oname = "LGTeacher"; if (strcmp(cname, oname) == 0) { printf("methodizeClass :%s \n",cname); } 複製代碼
結果能夠看到:
分類仍是不在這,同時經過 LLDB
打印,發現分類的方法已經在類的 ro
裏面了,因此說分類的加載其實跟類的懶加載與否並無關係,也就是說懶加載的分類都是在編譯時期被加載的。
咱們再接着分下下面兩種狀況:
其實懶加載和非懶加載的最大區別就是加載是否提早,而實現了 +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
。
非懶加載類的流程咱們也十分熟悉了,在 _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
分類的加載探索完了,咱們下一章將探索類拓展和關聯對象,敬請期待~