引言
上篇文章講到了dyld與objc的連接,在_objc_init函數(shù)中,通過_dyld_objc_notify_register注冊三個回調(diào)函數(shù):map_images,load_images,unmap_image,如圖所示。我們在011-iOS底層原理-_objc_init中已經(jīng)探索了load_images,unmap_image的作用與流程,本文將探索map_images。

工程:LGProject
map_images
對以 headerList開頭的鏈表中的 headers 進(jìn)行初始處理
在011-iOS底層原理-_objc_init中已經(jīng)探索了map_images函數(shù)內(nèi)部返回的是map_images_nolock()的結(jié)果,進(jìn)入map_images_nolock找到了_read_images()這個函數(shù)。而此函數(shù)是本文所探索的入口。
map_images:管理文件中和動態(tài)庫中所有的符號:class,protocal,selector,category
1、map_images_nolock

2、_read_images
_read_images的源碼共有360行(行行出狀元?)
由我們之前探索dyld加載流程的思路:掌握主線。將if else等分支代碼全部折疊起來,可以看到,共有的特性:ts.log()打印沒段代碼的作用,如圖所示:

因此,我們得到如下過程,我們將逐步探索這10個過程:

2.1 、doneOnce條件控制執(zhí)行一次的加載
doneOnce的定義是static bool doneOnce;,靜態(tài)變量,在if (!doneOnce) {內(nèi)設(shè)置為doneOnce = YES;因此只走一次。
1)disableTaggedPointers()為禁用所有TaggedPointers,其內(nèi)部實現(xiàn)為:
static void disableTaggedPointers()
{
objc_debug_taggedpointer_mask = 0;
objc_debug_taggedpointer_slot_shift = 0;
objc_debug_taggedpointer_slot_mask = 0;
objc_debug_taggedpointer_payload_lshift = 0;
objc_debug_taggedpointer_payload_rshift = 0;
objc_debug_taggedpointer_ext_mask = 0;
objc_debug_taggedpointer_ext_slot_shift = 0;
objc_debug_taggedpointer_ext_slot_mask = 0;
objc_debug_taggedpointer_ext_payload_lshift = 0;
objc_debug_taggedpointer_ext_payload_rshift = 0;
}
2)initializeTaggedPointerObfuscator()隨機(jī)初始化 objc_debug_taggedpointer_obfuscator。標(biāo)記指針混淆器旨在使攻擊者更難將特定對象構(gòu)造為標(biāo)記指針,在存在緩沖區(qū)溢出或其他寫入控制的情況下記憶?;煜髟谠O(shè)置時與標(biāo)記指針異或或檢索有效載荷值。他們首先充滿了隨機(jī)性采用。
總而言之,這個函數(shù)就是為了小對象類型的一些處理,初始化小對象類型(NSNumber、NSString都是有小對象組成的對象,存放在常量區(qū),并且占用空間非常的小。),主要對小對象通過mask做一些混淆
參考文章
3)gdb_objc_realized_classes實際上是NXMapTable類型的哈希表,包含了不在 dyld 共享緩存中的被命名的類,這些類不管是否被實現(xiàn)。此表不包括 必須使用 getClass查找的 被懶加載命名的類。
換句話說,gdb_objc_realized_classes相當(dāng)于一個總表。而在_objc_init函數(shù)中,runtime_init里初始化的allocatedClasses表,是一張已經(jīng)初始化好的類和元類的表。
也就是說:gdb_objc_realized_classes包含allocatedClasses。
這張總表所開辟的內(nèi)存大小,是在總類數(shù)量的4/3倍。4/3是NXMapTable的加載因子。這是為了配合前面cache_t擴(kuò)容的3/4負(fù)載因子。
2.2、修復(fù)預(yù)編譯階段的@selector混亂問題
我們知道SEL是由名字+地址組成的,因此匹配兩個SEL,需要對比名字+地址。否則可判定為不相等。
源碼如下:
// Fix up @selector references
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->hasPreoptimizedSelectors()) continue;
bool isBundle = hi->isBundle();
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
SEL sel = sel_registerNameNoLock(name, isBundle);
if (sels[i] != sel) {
sels[i] = sel;
}
}
}
}
ts.log("IMAGE TIMES: fix up selector references");
我們在objc工程中UnfixedSelectors代碼塊打上幾個斷點,如圖所示。運(yùn)行后用lldb調(diào)試,結(jié)果如下:

1、
sel來自于sel_registerNameNoLock() -> __sel_registerName() ->search_builtins() -> _dyld_get_objc_selector()。換句話說就是sel來自于dyld加載出來的。2、
sels來自于Mach-O文件里的__objc_selrefs,即:_getObjc2SelectorRefs -> __objc_selrefs。兩個sel來源不同,會導(dǎo)致同名不同地址的情況。因此,需要對這些selectors進(jìn)行fix up。
2.3、錯誤混亂的類處理
1、從MachO文件中字段__objc_classlist獲取所有類列表,然后 通過readClass得到相應(yīng)的類。
2、走完for循環(huán),發(fā)現(xiàn)if (newCls != cls && newCls) {}并未進(jìn)入。原因是:如果readClass的結(jié)果newClas與列表中的cls不同,則進(jìn)行修復(fù)操作,但這一般不會出現(xiàn),只有類被移動并且沒有被刪除才會出現(xiàn)。
3、lldb調(diào)試

MachO中獲取的類,未通過readClass時,只有一個地址,并未關(guān)聯(lián)到相應(yīng)的類名。通過readClass之后,關(guān)聯(lián)上了相應(yīng)的類名。并且得到的newCls與原始的cls名字+地址都一致。
2.4、修復(fù)重映射一些沒有被鏡像文件加載進(jìn)來的類
將未映射的類和父類重映射,其中被重映射的類都是非懶加載的類。此代碼塊一般情況下是不會被執(zhí)行。

2.5、修復(fù)一些消息
通過讀取MachO文件的__objc_msgrefs字段,通過fixupMessageRef函數(shù)進(jìn)行修復(fù),如如alloc -> objc_alloc、allocWithZone -> objc_allocWithZone 等,內(nèi)部如下:

__sel_registerName注冊方法名,內(nèi)部源碼如下:
static SEL __sel_registerName(const char *name, bool shouldLock, bool copy)
{
SEL result = 0;
if (shouldLock) selLock.assertUnlocked();
else selLock.assertLocked();
if (!name) return (SEL)0;
// 從dyld里查找,有該name就返回
result = search_builtins(name);
if (result) return result;
conditional_mutex_locker_t lock(selLock, shouldLock);
// 將name插入方法表namedSelectors
auto it = namedSelectors.get().insert(name);
if (it.second) {
// No match. Insert.
*it.first = (const char *)sel_alloc(name, copy);
}
return (SEL)*it.first;
}
2.6、修復(fù)protocol引用,并 readProtocol
通過讀取MachO的__objc_protolist字段,將得到的protolist存入到protocol_map哈希表中。
如果這是來自共享緩存的image鏡像,則跳過讀取協(xié)議。請注意,啟動后我們確實需要遍歷協(xié)議,因為共享緩存中的協(xié)議用 isCanonical()標(biāo)記,如果選擇某些非共享緩存二進(jìn)制文件作為規(guī)范定義,則可能不是這樣。

readProtocol()源碼如下:
static void
readProtocol(protocol_t *newproto, Class protocol_class,
NXMapTable *protocol_map,
bool headerIsPreoptimized, bool headerIsBundle)
{
// This is not enough to make protocols in unloaded bundles safe,
// but it does prevent crashes when looking up unrelated protocols.
auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;
protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);
if (oldproto) {
if (oldproto != newproto) {
如果我們是一個共享緩存二進(jìn)制文件,那么我們就有了這個協(xié)議的定義,但是如果選擇了另一個,那么我們需要清除我們的 isCanonical 位,以便沒有人信任它。
如果 getProtocol 返回共享緩存協(xié)議,則規(guī)范定義已經(jīng)在共享緩存中,我們不需要做任何事情。
if (headerIsPreoptimized && !oldproto->isCanonical()) {
// Note newproto is an entry in our __objc_protolist section which
// for shared cache binaries points to the original protocol in
// that binary, not the shared cache uniqued one.
auto cacheproto = (protocol_t *)
getSharedCachePreoptimizedProtocol(newproto->mangledName);
if (cacheproto && cacheproto->isCanonical())
cacheproto->clearIsCanonical();// 清除isCanonical 位
}
}
}
else if (headerIsPreoptimized) {
共享緩存初始化了協(xié)議對象本身,但為了允許緩存外替換,需要將其添加到協(xié)議表中。
protocol_t *cacheproto = (protocol_t *)
getPreoptimizedProtocol(newproto->mangledName);
protocol_t *installedproto;
if (cacheproto && cacheproto != newproto) {
// Another definition in the shared cache wins (because
// everything in the cache was fixed up to point to it).
installedproto = cacheproto;
}
else {
// This definition wins.
installedproto = newproto;
}
......省略代碼......
insertFn(protocol_map, installedproto->mangledName,
installedproto);
}
else {
未預(yù)優(yōu)化鏡像的新協(xié)議。將其固定到位。修復(fù)可卸載包中的重復(fù)協(xié)議
newproto->initIsa(protocol_class); // fixme pinned
insertFn(protocol_map, newproto->mangledName, newproto);
}
}
2.7、修復(fù)沒有被加載的協(xié)議
如圖所示:remapProtocolRef()未執(zhí)行

remapProtocolRef()函數(shù)如下,通過remapProtocol()函數(shù),重新映射得到新的newproto,再與protoref比較,將newproto賦值給*protoref。
static void remapProtocolRef(protocol_t **protoref)
{
runtimeLock.assertLocked();
protocol_t *newproto = remapProtocol((protocol_ref_t)*protoref);
if (*protoref != newproto) {
*protoref = newproto;
UnfixedProtocolReferences++;
}
}
2.8、分類處理
僅在完成初始化分類后才執(zhí)行此操作。對于啟動時出現(xiàn)的分類,被推遲到_dyld_objc_notify_register 調(diào)用完成后的第一個load_images 調(diào)用。即loadAllCategories();
源碼如下:
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}
2.9、類的加載處理 (重點)
主要是實現(xiàn)類的加載處理,加載非懶加載類。流程如下:
1、通過nlclslist()函數(shù)從MachO文件中的__objc_nlclslist字段獲取classlist類表。
即:nlclslist()-->_getObjc2NonlazyClassList()-->MachO的__objc_nlclslist
classref_t const *classlist = hi->nlclslist(&count);
2、遍歷classlist將class重新映射,得到的新class和metaClass插入類表中。
addClassTableEntry(cls);

3、通過realizeClassWithoutSwift(cls, nil);實現(xiàn)類。
對 cls 執(zhí)行第一次初始化,包括分配其讀寫(r w)數(shù)據(jù),因為前面的readClass只讀取了類的名字和地址,并未讀取r w數(shù)據(jù),因此在此讀取。不執(zhí)行任何 Swift 端初始化,最終返回類的真實類的結(jié)構(gòu)。
2.10 、沒有被處理的類 優(yōu)化那些被侵犯的類
實現(xiàn)新解析的未來類,以防 CF 操作這些類。
在2.3中,resolvedFutureClasses被賦值,但我們通過調(diào)試,可知前面的賦值并未執(zhí)行。因此,此處的resolvedFutureClasses為空。只有第2.3步的resolvedFutureClasses執(zhí)行賦值操作后,此處才會在這步處理這些未來類。

3、(核心重點分析) readClass
在2.3步驟中,從Macho讀取__objc_classlist字段的類表后,遍歷此classlist,通過readClass()讀取類并加入到類表、內(nèi)存中。其中readClass得到的是類的名稱和地址,類的內(nèi)容在此時并沒有配置。
進(jìn)入readClass內(nèi)部,源碼如下:

readClass簡化后的代碼如下:
1、從
ro中讀取到類名;2、
addNamedClass()將類名插入到哈希表中(gdb_objc_realized_classes,前面提到的,該表存放所有類);3、
addClassTableEntry()將類和元類插入到哈希表中(allocatedClasses,前面提到的,該表在_objc_init中的runtime_init創(chuàng)建的表中,該表存放已經(jīng)創(chuàng)建的類)。由于
readClass是在for循環(huán)中調(diào)用的,即從MachO中讀取到的classlist遍歷操作readClass,因此除了我們自定義的類之外,還會有很多系統(tǒng)的類。我們將其打印出來。源碼以及打印結(jié)果如下:
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
const char *mangledName = cls->nonlazyMangledName();
printf("---- %s----%s\n",__func__,mangledName);
---------省略-后面代碼--------
}

由上圖打印結(jié)果可以看到,我們自定義的類名出現(xiàn)在了打印的最后。我們只需要知道類的加載過程,系統(tǒng)類太復(fù)雜,不利于我們添加斷點停下,因此并非我們的首選。我們的思路是通過我們自定義的類的加載來探索,因此,我們只需要判斷
mangledName與QLPerson相等的時候,停下來。即可查看變量的值以及lldb調(diào)試。代碼設(shè)計如下:加入了strcmp函數(shù),將斷點添加進(jìn)來,并在每一個if處打上斷點。
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
const char *mangledName = cls->nonlazyMangledName();
const char *customClsName = "QLPerson";
int cmpResult = strcmp(mangledName, customClsName);
if (cmpResult == 0) {
printf("---- %s----%s\n",__func__,mangledName);
}
---------省略-后面代碼--------
}
斷點停下后,Xcode點擊Step over,再一次驗證了不在此處設(shè)置類的rw 、ro。
1、斷點來到addNamedClass(未執(zhí)行),此時的Class只有一個地址。

2、斷點執(zhí)行
addNamedClass(執(zhí)行完畢)。
3、斷點執(zhí)行到
addClassTableEntry,將cls和元類插入表中。
4、(核心重點分析) realizeClassWithoutSwift
上面第3步read_class加載的是類名+地址。realizeClassWithoutSwift則是加載類的data,配置ro,rw等內(nèi)容。我們將通過斷點調(diào)試,來探索這其中的流程。
【4.1】、加載本類data,設(shè)置ro,rw
由于我們只需要探索我們自定義的類,因此在realizeClassWithoutSwift()函數(shù)內(nèi),我們加入了判斷mangledName = QLPerson,讓斷點停在此處。進(jìn)一步lldb調(diào)試ro,rw,等內(nèi)容。我們所要探索的類的內(nèi)容,請參考006--iOS底層 - 類的結(jié)構(gòu)(屬性、成員變量、方法的探索)。包括屬性,成員變量,方法,cache等。

調(diào)試結(jié)果如下:
1)屬性/成員變量:

2)方法:

打印方法發(fā)現(xiàn)打印不出來。繼續(xù)往下走。
【4.2】遞歸實現(xiàn)父類,元類完善繼承鏈和isa走向
如果父類和元類還沒有被實現(xiàn),則遞歸調(diào)用realizeClassWithoutSwift()去實現(xiàn)父類和元類。
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
實現(xiàn)了父類和元類后,并設(shè)置是否支持Non-pointer isa ,將他們保存。
// Update superclass and metaclass in case of remapping
cls->setSuperclass(supercls);
cls->initClassIsa(metacls);
....省略代碼......
此處要用遞歸的視角去看待,將繼承鏈完善。
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
【4.3】配置類的方法:methodizeClass
在上面的4.1步驟中,我們未能打印method,methodizeClass函數(shù)即為配置類的方法。

【4.3.1】預(yù)處理方法列表:prepareMethodLists
prepareMethodLists源碼中,最主要的是對方法列表的修復(fù),遍歷addedLists,調(diào)用fixupMethodList函數(shù)。

【4.3.2】修復(fù)方法列表:fixupMethodList
此函數(shù)是遍歷方法列表,把方法名設(shè)置后,對方法進(jìn)行排序:
a)meth.setName(sel_registerNameNoLock(name, bundleCopy));實際上是調(diào)用了__sel_registerName(),也就是我們前面的_read_images第2.5步,修復(fù)objc_msgSend重定向的時候提到的地方。

調(diào)試結(jié)果如下:

由此可見,方法的排序,并非以名字排序,而是以地址排序。
5、總結(jié)
【5.1】類的加載(本類)流程圖如下:

【5.2】分類(category)的加載將在下一篇講解
【5.3】此流程為
非懶加載類的流程,即在測試類QLPerson中實現(xiàn)了+load方法,在map_images中加載所有類的數(shù)據(jù)。若是未實現(xiàn)
+load方法,則在實現(xiàn)類的函數(shù)realizeClassWithoutSwift的流程如下:lookUpImpOrForward->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift->methodizeClass。兩者之間的差異,如圖所示:
