歡迎閱讀iOS探索系列(按序閱讀食用效果更加)c++
上篇文章iOS探索 類的加載過程分析了類的加載
過程,本文就來好好聊聊分類加載
的那些事面試
(請先對類的加載過程
有了必定了解以後再開啓本文)express
爲FXPerson
新建一個分類FXPerson-FX
數組
終端利用clang
輸出cpp
安全
clang -rewrite-objc FXPerson+FX.m -o cate.cpp
複製代碼
從cpp文件最下面看起,首先看到分類是存儲在MachO文件的__DATA段的__objc_catlist
中bash
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_FXPerson_$_FX,
};
複製代碼
其次能看到FXPerson分類的結構app
static struct _category_t _OBJC_$_CATEGORY_FXPerson_$_FX __attribute__ ((used, section ("__DATA,__objc_const"))) = {
"FXPerson",
0, // &OBJC_CLASS_$_FXPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_FXPerson_$_FX,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_FXPerson_$_FX,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_FXPerson_$_FX,
};
複製代碼
來到objc源碼中搜索category_t
查看底層中分類的結構(_category_t
搜索無果)ide
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);
};
複製代碼
根據FXPerson分類
結構和底層分類結構對比:函數
name
:類的名字,不是分類的名字cls
:類對象instanceMethods
:分類上存儲的實例方法classMethods
:分類上存儲的類方法protocols
:分類上所實現的協議instanceProperties
:分類所定義的實例屬性,不過咱們通常在分類中添加屬性都是經過關聯對象來實現的_classProperties
:分類所定義的類屬性爲何分類的方法要將實例方法和類方法分開存呢?post
由於類和元類以前在不斷編譯,實例方法存在類中,類方法存在元類中,已經肯定好其方法歸屬的地方;而分類是後面才加進來的
經過上一篇文章咱們知道了類分爲懶加載類
和非懶加載類
,他們的加載時機不同,那麼分類又是如何呢?下面咱們就依次來進行探究
類、分類均不實現
+load
方法
已知懶加載類
很累,只有調用它發送消息時纔會加載
而添加分類在兩處出現了:_read_images
和methodizeClass
,咱們不妨來嘗試一下
// Discover categories.
// 發現和處理全部Category
for (EACH_HEADER) {
// 外部循環遍歷找到當前類,查找類對應的Category數組
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
// 內部循環遍歷當前類的全部Category
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
// 首先,經過其所屬的類註冊Category。若是這個類已經被實現,則從新構造類的方法列表。
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
// 將Category添加到對應Class的value中,value是Class對應的全部category數組
addUnattachedCategoryForClass(cat, cls, hi);
// 將Category的method、protocol、property添加到Class
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
// 這塊和上面邏輯同樣,區別在於這塊是對Meta Class作操做,而上面則是對Class作操做
// 根據下面的邏輯,從代碼的角度來講,是能夠對原類添加Category的
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
複製代碼
static void methodizeClass(Class cls) {
...
// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
...
}
複製代碼
爲了不其餘類調用_read_images
和methodizeClass
(紅色框)難以調試,分別往這兩處添點代碼(綠色框)從而更好的研究FXPerson
的類和分類
從左邊的函數調用棧能夠得出:
懶加載類
發送消息,lookupOrForward
->realizeClassMaybeSwiftAndLeaveLocked
realizeClassMaybeSwiftMaybeRelock
->realizeClassWithoutSwift
開始加載內存methodizeClass
處理父類、元類關係,調用了兩次打印unattachedCategoriesForClass
返回NULL
_read_images
加載分類沒有調用一不當心就翻車了,先換非懶加載類和懶加載分類
狀況研究吧
只有類實現
+load
方法
①一樣的研究方法,運行項目
查看函數調用棧得出:(不明白的能夠閱讀 淺嘗輒止dyld加載流程、 類的加載過程)dyld
->libSystem_initializer
->libdispatch_init
->_os_object_init
_objc_init
->map_images
->map_images_nolock
->_read_images
realizeClassWithoutSwift
->methodizeClass
加載類到內存中methodizeClass
處理父類、元類關係,調用了兩次打印unattachedCategoriesForClass
返回NULL
_read_images
加載分類沒有調用②又是和懶加載類和懶加載分類
同樣的狀況...繼續探索一下rw
(看不懂就閱讀類的結構分析)
(lldb) p/x cls
(Class) $0 = 0x0000000100001188
(lldb) p (class_data_bits_t *)0x00000001000011a8
(class_data_bits_t *) $1 = 0x00000001000011a8
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000103200060
(lldb) p *$2
(class_rw_t) $3 = {
flags = 2148007936
version = 7
ro = 0x00000001000010e8
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000010b0
arrayAndFlag = 4294971568
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fff9383faa0
demangledName = 0x0000000000000000
}
(lldb) p $3.methods
(method_array_t) $4 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000010b0
arrayAndFlag = 4294971568
}
}
}
(lldb) p $4.list
(method_list_t *) $5 = 0x00000001000010b0
(lldb) p *$5
(method_list_t) $6 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 2
first = {
name = "cate_doClass"
types = 0x0000000100000faa "v16@0:8"
imp = 0x0000000100000e00 (objc-debug`+[FXPerson(FX) cate_doClass] at FXPerson+FX.m:23)
}
}
}
(lldb) p $5->get(0)
(method_t) $7 = {
name = "cate_doClass"
types = 0x0000000100000faa "v16@0:8"
imp = 0x0000000100000e00 (objc-debug`+[FXPerson(FX) cate_doClass] at FXPerson+FX.m:23)
}
(lldb) p $5->get(1)
(method_t) $8 = {
name = "load"
types = 0x0000000100000faa "v16@0:8"
imp = 0x0000000100000e90 (objc-debug`+[FXPerson load] at FXPerson.m:12)
}
(lldb)
複製代碼
第一次調用先處理元類關係metacls = realizeClassWithoutSwift(remapClass(cls->ISA()));
元類中存下了FXPerson類
的+load
方法和FXPerson分類
的+cate_doClass
方法
-cate_doInstance
方法
說明在methodizeClass
的unattachedCategoriesForClass
前已經把分類的方法加載到類中
③修改一下源碼,將調試代碼放到操做rw
的methods
以前,發現此時的methods
還沒賦值
(lldb) p *$5
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory methodizeClass:類名 :FXPerson - 0x100001188 複製代碼
斷點來到methods
賦值以後,分類的方法已經躺在裏面了
method_list_t *list = ro->baseMethods()
這一步只是對ro->baseMethods放到rw中
④從新跑項目,在第一個斷點處打印ro
,分類方法已經存在了...
結論: 不論是懶加載類
或是非懶加載類
,懶加載分類
在編譯時就肯定了
類、分類均實現
+load
方法
恢復成最初的調試代碼,運行項目
①首先斷點來到methodizeClass
兩次unattachedCategoriesForClass
返回的都是NULL
②其次斷點來到_read_images
的Discover categories
,按照圖中的順序依次調用
addUnattachedCategoryForClass
把類/元類和分類作一個關聯映射remethodizeClass
調用attachCategories
處理分類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);
}
}
複製代碼
remethodizeClass
調用attachCategories
處理分類
static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
複製代碼
attachCategories
分析:
+load
)while (i--)
中並非網上所說的按照Compile Sources
倒序加載的,先加載的是FXPerson+FX
,至於這裏這麼寫多是爲了方便吧
attachLists
添加分類的方法、屬性、協議(類的加載過程有詳細介紹)
memmove
將原數據移到末尾memcpy
把新數據拷貝到起始位置doInstance
,那麼分類附加完成以後,類的方法列表裏會有兩個doInstance
運行時
在查找方法
時是順着方法列表的順序查找的,它只要一找到對應名字的方法,就會返回imp
,卻不知後面可能還有同樣名字的方法只有分類實現
+load
方法
①首先斷點來到_read_images
的Discover categories
,不走remethodizeClass
②斷點來到methodizeClass
,此次終於經過unattachedCategoriesForClass
取到值了,而後經過attachCategories
添加
load_images
->
prepare_load_methods
->
realizeClassWithoutSwift
->...->
methodizeClass
(這個知識點後面會提到)
懶加載類 + 懶加載分類
第一次消息發送
的時候,而分類的加載則在編譯時
懶加載類 + 非懶加載分類
_read_images
處,分類的加載則在編譯時
非懶加載類 + 非懶加載分類
_read_images
處,分類的加載在類加載以後的reMethodizeClass
懶加載類 + 非懶加載分類
load_images
處,分類的加載在類加載以後的methodizeClass
+load
)+load
方法,就響應Compile Sources
最後一個分類+load
,響應非懶加載分類
——由於懶加載分類
在編譯時就已經加載到內存,而非懶加載分類
運行時才加載+load
,響應Compile Sources
最後一個分類類拓展extension
又稱做匿名的分類
,爲了給當前類增長屬性
和方法
具體由兩種形式:
.m
文件中新增類拓展.h
文件數據很早的時候都會來到_read_image
,那正好在處理類時使用咱們的慣用伎倆
可是仔細一想不對呀,已經在類中有了方法實現了,此時的do_hExtension
不足以說明問題
那麼能夠經過查看屬性的setter
和getter
方法來驗證
經過上圖就能夠得出:
若是類拓展沒有被引用(#import)就不會編譯到到內存中
上篇文章講到dyld初始化image
會觸發load_image
,本文又提到了懶加載類
和非懶加載分類
狀況下,分類加載到內存時的調用棧中有load_image
,那麼咱們在該種狀況下進行探索
在load_image
實現處打下斷點,發現類和分類都沒有打印+load
方法——load_image
先於+load
方法
prepare_load_methods
call_load_methods
發現並準備+load
方法
void prepare_load_methods(const headerType *mhdr) {
size_t count, i;
runtimeLock.assertLocked();
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
/*********************************************************************** * prepare_load_methods * Schedule +load for classes in this image, any un-+load-ed * superclasses in other images, and any categories in this image. **********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls) {
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
/*********************************************************************** * add_class_to_loadable_list * Class cls has just become connected. Schedule it for +load if * it implements a +load method. **********************************************************************/
void add_class_to_loadable_list(Class cls) {
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
複製代碼
prepare_load_methods
分析:
_getObjc2NonlazyClassList
獲取非懶加載類
列表schedule_class_load
遍歷這些類
+load
方法,確保父類的+load
方法順序排在子類的前面add_class_to_loadable_list
把類的+load
方法存在loadable_classes
裏面
_getObjc2NonlazyCategoryList
取出非懶加載分類
列表realizeClassWithoutSwift
來防止類沒有初始化(若已經初始化了則不影響)add_category_to_loadable_list
加載分類中的+load
方法到loadable_categories
此時就能看懂以前懶加載類
和非懶加載分類
的函數調用棧了
喚醒+load
方法
void call_load_methods(void) {
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
複製代碼
objc_autoreleasePoolPush
壓棧一個自動釋放池do-while
循環開始
+load
方法直到找不到爲止+load
方法關於initalize
蘋果文檔是這麼描述的
Initializes the class before it receives its first message.
在這個類接收第一條消息以前調用。
Discussion
The runtime sends initialize to each class in a program exactly one time just before the class, or any class that inherits from it, is sent its first message from within the program. (Thus the method may never be invoked if the class is not used.) The runtime sends the initialize message to classes in a thread-safe manner. Superclasses receive this message before their subclasses.
Runtime在一個程序中每個類的一個程序中發送一個初始化一次,或是從它繼承的任何類中,都是在程序中發送第一條消息。(所以,當該類不使用時,該方法可能永遠不會被調用。)運行時發送一個線程安全的方式初始化消息。父類的調用必定在子類以前。
複製代碼
而後咱們在objc源碼
中lookUpImpOrForward
找到了它的蹤影
lookUpImpOrForward
->initializeAndLeaveLocked
->initializeAndMaybeRelock
->initializeNonMetaClass
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) {
...
if (initialize && !cls->isInitialized()) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
...
}
...
}
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock) {
return initializeAndMaybeRelock(cls, obj, lock, true);
}
static Class initializeAndMaybeRelock(Class cls, id inst, mutex_t& lock, bool leaveLocked) {
···
initializeNonMetaClass(nonmeta);
···
}
複製代碼
在initializeNonMetaClass
遞歸調用父類initialize
,而後調用callInitialize
/*********************************************************************** * class_initialize. Send the '+initialize' message on demand to any * uninitialized class. Force initialization of superclasses first. **********************************************************************/
void initializeNonMetaClass(Class cls) {
...
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
...
{
callInitialize(cls);
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
pthread_self(), cls->nameForLogging());
}
}
...
}
複製代碼
callInitialize
是一個普通的消息發送
void callInitialize(Class cls) {
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
複製代碼
關於initalize
的結論:
initialize
在類或者其子類的第一個方法被調用前(發送消息前)調用initialize
但不使用的狀況下,是不會調用initialize
initialize
方法會比子類先執行initialize
方法時,會調用父類initialize
方法;子類實現initialize
方法時,會覆蓋父類initialize
方法initialize
方法,會覆蓋類中的方法,只執行一個(會執行最後被加載到內存中的分類的方法)從類結構
、消息發送
、dyld
到類與分類的加載過程
,筆者已經將加載->使用的流程進行一小波探究以後,接下來將開始乾貨分享——底層面試題