1. Category的作用
- 可以減少單個文件的體積
- 可以把不同的功能組織到不同的Category中
- 可以按需加載
- 聲明私有方法
- 把framework的私有方法公開
Tips:比如一個類的方法,沒有在.h文件中暴露出來,我們可以給這個類創(chuàng)建一個分類,在分類的.h文件中,去聲明這個私有方法。
Extension 擴(kuò)展
- 類的一部分
- 編譯時期就確定的
- 隱藏類的私有屬性
Category
- 運(yùn)行時期確定的
- 不能添加變量(編譯時期,對象的內(nèi)存布局已經(jīng)確定了,添加變量會破壞類的內(nèi)存布局)
2. Category 底層原理探索
Category 的數(shù)據(jù)結(jié)構(gòu)
struct category_t {
const char *name; // 所屬類的名稱
classref_t cls; // 要擴(kuò)展的類對象(編譯時期是沒有值的)
struct method_list_t *instanceMethods; // 實(shí)例方法
struct method_list_t *classMethods; // 類方法
struct protocol_list_t *protocols; // 協(xié)議
struct property_list_t *instanceProperties; // 實(shí)例屬性
// 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);
};
從上面的Category的數(shù)據(jù)結(jié)構(gòu),我們就可以看出category的可為(可以添加實(shí)例方法,類方法,甚至可以實(shí)現(xiàn)協(xié)議,添加屬性)和不可為(無法添加實(shí)例變量)。
2.1 代碼調(diào)用流程分析
-
2.1.1 編譯時期
可以通過clang 來編譯一下:
clang -rewrite-objc DDPerson+Test.m
DDPerson+Test.m就是我隨便創(chuàng)建的一個分類的 .m文件,使用上述命令編譯完成后,會在該文件同級目錄下生成一個DDPerson+Test.cpp文件。
Tips:編譯過后的東西,都會存在Mach-O的可執(zhí)行文件當(dāng)中。
我們可以打開該文件找到分類的結(jié)構(gòu)體定義 _category_t:
再向下可以看到這個分類的結(jié)構(gòu)體到底存放了什么內(nèi)容:
static struct _category_t _OBJC_$_CATEGORY_DDPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
// 這個分類它存放在Mach-O文件__objc_const的字段中
{
"DDPerson", // 當(dāng)前類的名稱
0, // &OBJC_CLASS_$_DDPerson, 指向類對象的指針,運(yùn)行時依賴 當(dāng)前類的名稱進(jìn)行綁定
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_DDPerson_$_Test, // 當(dāng)前分類里面的實(shí)例方法
0,
0,
0,
};
我們可以看到第三個 &OBJC$CATEGORY_INSTANCE_METHODS_DDPerson... ,是編譯器生成的當(dāng)前分類的實(shí)例方法列表。
因為我們只是給分類添加了一個方法,所以其余的類方法列表,協(xié)議列表,屬性列表,都是0,也就是空的。
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_DDPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
// 下面三個參數(shù)分別是 方法編號 方法簽名 真實(shí)的函數(shù)地址
{{(struct objc_selector *)"dd_privateTest", "v16@0:8", (void *)_I_DDPerson_Test_dd_privateTest}}
};
這一步就是生成分類的方法列表,用這個方法列表,初始化分類本身。
到現(xiàn)在為止分類在編譯時期做的事情差不多已經(jīng)完成了,
總結(jié)來說就是把當(dāng)前的分類編譯成結(jié)構(gòu)體,然后把對應(yīng)的字段填充上相應(yīng)的數(shù)據(jù)。
但是,編譯最后的分類數(shù)據(jù)放在哪里呢?我們繼續(xù)向下看:
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_DDPerson_$_Test,
};
上面是非常關(guān)鍵的,所有的分類都會存在 __DATA 數(shù)據(jù)段下的的 __objc_catlist section當(dāng)中。
到目前為止,在編譯時期對于分類的操作都已經(jīng)完成了,運(yùn)行時期,App就會從 __objc_catlist section中去讀取所有的分類了。
-
2.1.2 運(yùn)行時期
運(yùn)行時期就要開始加載編譯時期存儲的所有分類了。
那么Category是如何被加載的呢?
- dyld 是蘋果的動態(tài)加載器,用來加載 image (不是指圖片,而是指 Mach-O 格式的二進(jìn)制文件)
- 當(dāng)程序啟動的時候,系統(tǒng)內(nèi)核首先會加載dyld,而dyld會將我們APP所以來的各種庫加載到內(nèi)存空間中,其中就包括libobjc庫(OC和runtime),這些工作,是在APP的main函數(shù)執(zhí)行前完成的。
- _objc_init 是 OC runtime 的入口函數(shù),在這里面主要功能是讀取Mach-O 文件 OC對應(yīng)的 Segment section,并根據(jù)其中的數(shù)據(jù)代碼信息,完成為OC的內(nèi)存布局,以及初始化runtime相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
我們來看下_objc_init方法
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// 我們主要關(guān)注下面這個方法,這個方法注冊了三個回調(diào)函數(shù):
// dyld將image加載入內(nèi)存的時候(將image映射到內(nèi)存空間的時候)會執(zhí)行 map_images
// dyld初始化image模塊完成的時候調(diào)用 load_images(load方法也會在這個時候調(diào)用)
// dyld將image移出內(nèi)存的時候 unmap_image函數(shù)會被調(diào)用
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
將分類加載到當(dāng)前的類的操作是在 map_images 回調(diào)函數(shù)中完成的,下面我們先去看下這個 回調(diào)函數(shù):
我們忽略不必要的代碼走到下面這個函數(shù)中:
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
在這個函數(shù)底部,我們可以看到 _read_images的函數(shù):
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
_read_images 最主要的作用是: 讀取OC相關(guān)的 Section段(Mach-O文件中的數(shù)據(jù)段),并進(jìn)行初始化。
我們繼續(xù)看 _read_images這個函數(shù),直接到分類相關(guān)的部分:
// EACH_HEADER 就是遍歷頭文件
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[I];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
...
我們拿到的 catlist 就是上節(jié)講到的編譯器為我們生成的category_t數(shù)組,它是通過
_getObjc2ClassList 這個方法拿到的,我們來關(guān)注下 _getObjc2ClassList 這個方法,點(diǎn)進(jìn)去可以看到:
TSECT(_getObjc2NonlazyClassList, classref_t, "__objc_nlclslist");
// 看這個
GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
GETSECT(_getObjc2NonlazyCategoryList, category_t *, "__objc_nlcatlist");
GETSECT(_getObjc2ProtocolList, protocol_t *, "__objc_protolist");
在編譯時期,我們可以知道,所有的分類都是存放在__objc_catlist 這個section當(dāng)中(或者說對應(yīng)這個key值),所以這里就是讀取這個 __objc_catlist 對應(yīng)的數(shù)據(jù)。
讀取完成之后,我們繼續(xù)往下看,下面這段代碼:
// 處理這個Category
// 首先,將Category注冊到它的目標(biāo)類。
// 然后,如果實(shí)現(xiàn)了類,則重構(gòu)類的方法列表(等等)。
bool classExists = NO;
// 把category的實(shí)例方法、協(xié)議以及屬性添加到類上
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{// 把分類注冊到它的目標(biāo)類建立一個關(guān)聯(lián)
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
// 重構(gòu)類的方法列表
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
// 把category的類方法和協(xié)議添加到類的metaclass上
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);
}
}
上面這段代碼,主要的功能如下:
a. 把category的實(shí)例方法、協(xié)議以及屬性添加到類上
b. 把category的類方法和協(xié)議添加到類的metaclass上
在上述的代碼片段里,addUnattachedCategoryForClass只是把類和category做一個關(guān)聯(lián)映射,而remethodizeClass才是真正去處理添加事宜的功臣。
那好,我們再去分析 remethodizeClass 這個關(guān)鍵方法:
// 在runtime源碼中,我們翻譯一下這個函數(shù)的注釋,就能知道這個函數(shù)的作用了
// 1.將當(dāng)前分類附加到現(xiàn)有類。
// 2.重新排列cls的方法列表、協(xié)議列表和屬性列表。
// 3.更新cls及其子類的方法緩存。
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);
}
}
我們看到上面的方法,主要是調(diào)用 attachCategories 這個方法,
那么我們繼續(xù),在 attachCategories 函數(shù)中看到 prepareMethodList函數(shù):
...
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
...
這個函數(shù)的關(guān)鍵內(nèi)容如下,它只是把所有category的實(shí)例方法列表拼成了一個大的實(shí)例方法列表:
...
// 添加方法列表到數(shù)組
// 重新分配方法列表。
// The new methods are PREPENDED to the method list array.
for (int i = 0; i < addedCount; i++) {
method_list_t *mlist = addedLists[I];
assert(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
//
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
// Scan for method implementations tracked by the class's flags
if (scanForCustomRR && methodListImplementsRR(mlist)) {
cls->setHasCustomRR();
scanForCustomRR = false;
}
if (scanForCustomAWZ && methodListImplementsAWZ(mlist)) {
cls->setHasCustomAWZ();
scanForCustomAWZ = false;
}
}
...
準(zhǔn)備好了這個方法列表之后之后,我們繼續(xù)看
...
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
...
主要是 rw->methods.attachLists(mlists, mcount); 這個方法
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
// 舊的大小,指的是原先方法列表的大小
uint32_t oldCount = array()->count;
// 新的就是 舊的 + 分類中要添加的
uint32_t newCount = oldCount + addedCount;
// 然后就是把新的方法添加到舊的方法列表
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
這里面主要是使用 memmove memcpy 兩個方法
都是用來做拷貝的操作,用來移動內(nèi)存空間,為將要添加的方法騰出內(nèi)存空間:
void *memcpy(void *__dst, const void *__src, size_t __n);
void *memmove(void *__dst, const void *__src, size_t __len);
// 把當(dāng)前 src 指向的位置,拷貝len的長度,放到dst的位置
比如:1 2 3 4 5 6 7
memmove(1的位置,4的位置,長度3)
完成之后就是 4 5 6 4 5 6 7
就是把 456 從 4 的位置,拷貝到 1 的位置
這種場景下,使用memcpy 也可以。
如果是另一個場景:將 1 的位置 長度為 3 拷貝到 3 的位置,
那么**有可能**會出現(xiàn) 12121567 這種情況
所以這種內(nèi)存重疊的情況下,memcpy 函數(shù) 有可能成功,有可能失敗。
然后我們分析下 代碼中
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
他的作用:
如果原有方法有10個,新添加的方法有3個,一共有13個,這句代碼的作用就是將原有的10個方法,放到13個方法的后10個
剛開始是
舊 舊 舊 舊 舊 舊 舊 舊 舊 舊 空 空 空
變成
空 空 空 舊 舊 舊 舊 舊 舊 舊 舊 舊 舊
這樣,前面三個方法就空出來了,因為這個場景剛好是上面介紹 memmove memcpy函數(shù)時的第二個場景,有可能會發(fā)生內(nèi)存重疊,所以這里使用的是memmove。
前面3個方法空出來了,此時我們就要把需要添加的三個方法放到這里了,這里不會發(fā)生內(nèi)存重疊,所以我們使用memcpy
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
它就是拷貝要添加的方法 addedLists 地址,長度為要添加的長度 addedCount 放到lists中。
從attachLists函數(shù)中我們可以看到,分類的方法覆蓋掉了主類的方法,分類方法會添加到主類方法的前面,這是因為運(yùn)行時在查找方法的時候是順著方法列表的順序查找的,它只要一找到對應(yīng)名字的方法,就會罷休,殊不知后面可能還有一樣名字的方法:
分類方法 分類方法 主類方法 主類方法。
我們可以打印出類的方法,然后通過代碼可以調(diào)用到主類的原有的方法。
我們現(xiàn)在可以總結(jié)一下map_images中對分類做的事情了:
- _getObjc2CategoryList方法調(diào)用__objc_catlist section 段記錄的所有Category,存放到category_t*數(shù)組
- 依次讀取category_t*數(shù)組
- cat和cls(主類)進(jìn)行映射
- remethodizeClass (修改method_list的結(jié)構(gòu)(memmove memcpy))
3.關(guān)聯(lián)對象探索
我們在開發(fā)中經(jīng)常使用關(guān)聯(lián)對象,在分類中添加一個屬性:
@interface DDPerson (Test)
@property (nonatomic, copy) NSString *name;
@end
我們知道在主類中的屬性其實(shí)包含三個東西:getter setter ivar, 這種在分類中聲明的屬性是沒有ivar的,只是生成了一個存取方法,以及存儲當(dāng)前value的容器(ObjcAssociation 下面會講到),因為上面也提到過,分類是不能添加成員變量的。
3.1 設(shè)置關(guān)聯(lián)對象
在給分類關(guān)聯(lián)對象的時候,我們會調(diào)用這個方法:
objc_setAssociatedObject(關(guān)聯(lián)到哪個對象, 存儲的key, 要關(guān)聯(lián)的對象, 關(guān)聯(lián)策略)
下面,我們就以這個方法為切入點(diǎn),從源碼中看一下:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
實(shí)際上是調(diào)用了 _object_set_associative_reference 這個方法,繼續(xù)看:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
ObjcAssociation old_association(0, nil);
// 取了一個新的value,根據(jù)策略設(shè)置
id new_value = value ? acquireValue(value, policy) : nil;
{
// 關(guān)聯(lián)對象的管理者
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 當(dāng)前要關(guān)聯(lián)到的對象的地址按位取反(作為當(dāng)前hashmap當(dāng)中的key)
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// c++ 語法,獲取一個迭代器,找到 disguised_object 作為key時對應(yīng)的value,里面的 j->first 指的是key j->second 指的是key對應(yīng)的value
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) { // 如果disguised_object 對應(yīng)的value有值,說明該對象有過關(guān)聯(lián)
// 拿到 disguised_object 作為key 對應(yīng)的value,也是一個map
ObjectAssociationMap *refs = i->second;
// 再以key值獲取一個新的迭代器,這個key是我們傳進(jìn)來的,是我們所定義的
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) { // 如果用戶傳進(jìn)的key有對應(yīng)的value
// 獲取到舊的value,賦給old_association,最后會被釋放掉
old_association = j->second;
// 賦值新的value,ObjcAssociation 來存儲我們的關(guān)聯(lián)屬性
j->second = ObjcAssociation(policy, new_value);
} else { // 如果用戶傳進(jìn)的key沒有對應(yīng)的value
// 直接賦值新的value
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// 如果這個對象是第一次關(guān)聯(lián),會走到這里,先創(chuàng)建一個Map,以disguised_object 作為key存進(jìn)manager管理的hashmap,再將new_value 和 policy生成的 ObjcAssociation 對象,以key存入 Map
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
// 設(shè)置當(dāng)前object的isa指針,添加一個標(biāo)識,說明這個對象有關(guān)聯(lián)對象,在這個對象銷毀的時候,去銷毀這些關(guān)聯(lián)對象
object->setHasAssociatedObjects();
}
} else {
// 如果是nil,則斷開關(guān)聯(lián)
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// 釋放 old_association
if (old_association.hasValue()) ReleaseValue()(old_association);
}
代碼很長,我們可以從上到下一點(diǎn)一點(diǎn)的分析:
a.先進(jìn)行內(nèi)存管理
// 這個方法是判斷value有值的話,就去調(diào)用acquireValue 生成一個新的對象,進(jìn)行內(nèi)存管理
id new_value = value ? acquireValue(value, policy) : nil;
// 看下源碼
acquireValue(value, policy) =>
// 清晰明了,根據(jù)關(guān)聯(lián)策略,對value進(jìn)行操作, objc_retain ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy); 這兩個操作就很熟悉了,strong copy關(guān)鍵字的內(nèi)部實(shí)現(xiàn)。
static id acquireValue(id value, uintptr_t policy) {
switch (policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
return objc_retain(value);
case OBJC_ASSOCIATION_SETTER_COPY:
return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
}
return value;
}
b. AssociationsManager 初始化其管理的hashmap
// 管理關(guān)聯(lián)對象,其內(nèi)部有一個static的 hashmap,用來管理關(guān)聯(lián)對象的
AssociationsManager manager;
// 這個是初始化hashMap, 下面代碼中有這個方法的實(shí)現(xiàn)
AssociationsHashMap &associations(manager.associations());
=> 點(diǎn)進(jìn)去看下 AssociationsManager:
// 從下面的注釋中我們也可以看到 這個hashMap 以對象的指針作為key的,value仍然是一個hashMap ,我們先了解下 AssociationsManager 的作用就是維護(hù)了一個hashmap.
class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
static AssociationsHashMap *_map;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
存的過程,可以通過下面這張圖來理解,就會非常的清楚:
3.2 取關(guān)聯(lián)對象
理解了怎么存的也就明白如何取的了,貼上源碼:
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
3.3 關(guān)聯(lián)對象如何釋放呢?
是在關(guān)聯(lián)到的對象釋放的時候,去釋放它的關(guān)聯(lián)對象。
我們可以從dealloc 方法入手去探究一下,這個隱藏的很深:
- (void)dealloc
↓
_objc_rootDealloc(self);
obj->rootDealloc();
object_dispose((id)this);
objc_destructInstance(obj);
終于到了objc_destructInstance方法:
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
我們可以看到是判斷 assoc 這個變量為真時,去調(diào)用 _object_remove_assocations 移除關(guān)聯(lián)對象的方法,獲取 assoc 值的方法是
obj->hasAssociatedObjects(); 這個方法中其實(shí)就是獲取在設(shè)置關(guān)聯(lián)對象時,給isa設(shè)置的那個標(biāo)志。
我們繼續(xù)看 _object_remove_assocations 這個方法:
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
// 獲取manager
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 如果size為0,說明沒有關(guān)聯(lián)屬性,直接return
if (associations.size() == 0) return;
// 對象的地址取反
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// 獲取到disguised_object 對應(yīng)的map
ObjectAssociationMap *refs = i->second;
// 獲取到所有的關(guān)聯(lián)對象,放入elements
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// 釋放掉
for_each(elements.begin(), elements.end(), ReleaseValue());
}
4. load方法探究
調(diào)用規(guī)則
- 一個類的load方法在所有父類load 方法調(diào)用之后調(diào)用
- 分類的load方法在當(dāng)前類的load方法調(diào)用之后調(diào)用
- 分類load的調(diào)用順序和編譯順序有關(guān)
在前面,我們有講到objc_init是runtime的入口函數(shù)
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// 我們主要關(guān)注下面這個方法,這個方法注冊了三個回調(diào)函數(shù):
// dyld將image加載入內(nèi)存的時候(將image映射到內(nèi)存空間的時候)會執(zhí)行 map_images
// dyld初始化image模塊完成的時候調(diào)用 load_images(load方法也會在這個時候調(diào)用)
// dyld將image移出內(nèi)存的時候 unmap_image函數(shù)會被調(diào)用
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
load方法也是在 load_images 中調(diào)用的,我們現(xiàn)在就去從load_images方法入手去看一看:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
// 準(zhǔn)備
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant) 調(diào)用
call_load_methods();
}
這個方法分為兩塊,prepare_load_methods()和 call_load_methods();
4.1 prepare_load_methods
我們這邊先分析 prepare_load_methods (),他是先把各個類或分類load方法都準(zhǔn)備好:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, I;
runtimeLock.assertLocked();
// 從Mach-o 文件加載類的列表
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 數(shù)組[<cls,method>,<cls,method>,<cls,method>] 有順序
// 調(diào)整類的順序
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
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
4.1.1 針對類的load方法的操作
我們?nèi)タ聪?schedule_class_load 這個方法:
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);
// 根據(jù)cls,將cls的load方法添加到一個全局的數(shù)組中
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
這個方法是遞歸調(diào)用,目的就是遵循上面的第一條規(guī)則,父類的load方法,在子類的load方法之前調(diào)用,也就是將類和類的load方法,這樣存入全局的一個容器(可以理解為數(shù)組)中:
[<父類,父類load方法>,<父類,父類load方法>,<子類,子類load方法>]
上面說到添加到一個全局的容器中,這一步是如何操作的呢?就是add_class_to_loadable_list:
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) {
// 字節(jié)對齊(???)
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[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
到現(xiàn)在 schedule_class_load(remapClass(classlist[i]));
這個方法我們就分析完了,它就是調(diào)整順序,先將所有父類的load方法存入全局容器中,再存入當(dāng)前類的load方法。
4.1.2 針對分類load方法的操作
接下來,就到了對于分類的操作了,還回到prepare_load_methods 這個方法
void prepare_load_methods(const headerType *mhdr)
{
...
// 針對分類的操作
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
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
我們知道分類load的調(diào)用順序是 和編譯順序有關(guān),上面的代碼中我們可以看到,當(dāng)從Mach-o文件中讀取到 categorylist 的時候,順序就已經(jīng)決定了,誰在前面就會先去調(diào)用誰的load方法,所以在下面就會直接調(diào)用 add_category_to_loadable_list(cat)方法:
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
method = _category_getLoadMethod(cat);
// Don't bother if cat has no +load method
if (!method) return;
if (PrintLoading) {
_objc_inform("LOAD: category '%s(%s)' scheduled for +load",
_category_getClassName(cat), _category_getName(cat));
}
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
這個方法跟上面對于類的load方法操作一樣,初始化了loadable_categories 這么一個容器,存儲分類的load方法:
[<分類,load方法>,<分類,load方法>,<分類,load方法>]
到此,prepare_load_methods 的方法也就執(zhí)行完畢了,它就是準(zhǔn)備好了兩個全局容器loadable_classes和loadable_category,分別存儲類的load方法,分類的load方法。
4.2 call_load_methods
準(zhǔn)備好了之后,就要開始調(diào)用了:
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) {
// 先調(diào)用類的load方法
call_class_loads();
}
// 2. Call category +loads ONCE 再調(diào)用分類的load方法
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;
}
在上面可以看到,是先 call_class_loads 調(diào)用類的load方法,再 call_category_loads 調(diào)用分類的load方法。
4.2.1 call_class_loads 調(diào)用類的load方法
static void call_class_loads(void)
{
int I;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
4.2.1 call_category_loads 調(diào)用分類的load方法
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}
// Compact detached list (order-preserving)
shift = 0;
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i-shift] = cats[I];
} else {
shift++;
}
}
used -= shift;
// Copy any new +load candidates from the new list to the detached list.
new_categories_added = (loadable_categories_used > 0);
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) {
allocated = allocated*2 + 16;
cats = (struct loadable_category *)
realloc(cats, allocated *
sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[I];
}
// Destroy the new list.
if (loadable_categories) free(loadable_categories);
// Reattach the (now augmented) detached list.
// But if there's nothing left to load, destroy the list.
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else {
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}
if (PrintLoading) {
if (loadable_categories_used != 0) {
_objc_inform("LOAD: %d categories still waiting for +load\n",
loadable_categories_used);
}
}
return new_categories_added;
}
現(xiàn)在,我們可以對load方法的調(diào)用做下總結(jié)了:
調(diào)用順序也就是跟我們上面說的調(diào)用規(guī)則一樣了。
上面關(guān)于給分類設(shè)置關(guān)聯(lián)對象的時候,我們給當(dāng)前對象的isa設(shè)置了一個標(biāo)志,這里做一下補(bǔ)充: