今天我們先來看兩個經(jīng)典的面試題:
1、應(yīng)用程序啟動 在
main
函數(shù)之前都具體做了哪些內(nèi)容?2、
load
在什么時(shí)候調(diào)用?子類、父類以及分類load
的調(diào)用順序?
帶著這幾個問題我們開始本節(jié)的內(nèi)容:
- 1、App編譯/啟動流程及動態(tài)鏈接器dyld
- 2、map_images流程分析
- 3、load_images流程分析
- 4、面試題答案(僅供參考~)
一、App編譯流程及啟動流程dyld
下面圖示編譯流程
注意:只有靜態(tài)庫會在編譯階段會打包進(jìn)入可執(zhí)行文件,動態(tài)庫是在程序運(yùn)行時(shí)才會被加入可執(zhí)行文件。靜態(tài)庫與動態(tài)庫知識點(diǎn)深入傳送門
應(yīng)用程序啟動前,會先對代碼進(jìn)行編譯,在編譯階段會把靜態(tài)庫打包到可執(zhí)行性文件中,編譯完成后,進(jìn)入啟動階段,談及App啟動流程,肯定少不了我們的動態(tài)鏈接器dyld,整個啟動過程都是它在進(jìn)行協(xié)調(diào),dyld操作流程如下圖:
其實(shí)在App啟動過程中主要分為main函數(shù)之前和main函數(shù)之后,main函數(shù)之后就是從main函數(shù)到我們第一個視圖出現(xiàn)的這段時(shí)間。先來看一下main()
之前主要做了哪些操作:
main()
之前通過調(diào)用dyld對主程序運(yùn)行環(huán)境初始化,生成imageLoader把動態(tài)庫生成對應(yīng)的image鏡像文件,載入到內(nèi)存中,然后進(jìn)行鏈接綁定,接著初始化所有動態(tài)庫,在執(zhí)行所有插入的動態(tài)庫初始化的時(shí)候,同時(shí)也對load_images
進(jìn)行了綁定。執(zhí)行初始化這個過程中,會優(yōu)先初始化系統(tǒng)庫libSystem
,運(yùn)行起來Runtime,這個過程會進(jìn)入Runtime
的入口函數(shù)_objc_init
,接下來把之前鏈接的動態(tài)庫及符號都交給Runtime
進(jìn)行map_images
和load_images
操作,然后Runtime執(zhí)行完load_images
之后會回調(diào)到dyld
內(nèi)部,dyld
收到信息回調(diào)后,最后查找main()函數(shù)的入口LC_MAIN
,找到后就會調(diào)起我們的main()函數(shù),進(jìn)入我們開發(fā)者的代碼。
接下來結(jié)合文章開始的面試題,我們就來仔細(xì)分析一下Runtime
的map_images
和load_images
流程中間做了哪些操作?
二、map_images流程分析
還是從系統(tǒng)庫libSystem
的Runtime入口函數(shù)_objc_init
開始分析:
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
//讀取影響運(yùn)行時(shí)的環(huán)境變量。
environ_init();
tls_init();
//運(yùn)行C ++靜態(tài)構(gòu)造函數(shù)。libc在dyld調(diào)用我們的靜態(tài)構(gòu)造函數(shù)之前調(diào)用_objc_init()
static_init();
lock_init();
//初始化libobjc的異常處理系統(tǒng)。由map_images()調(diào)用。
exception_init();
注冊回調(diào)函數(shù)
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
接下來我們開始進(jìn)入map_images
:
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
進(jìn)來后發(fā)現(xiàn)map_images
直接返回了map_images_nolock
。
先來看一下map_images
函數(shù)的注釋部分,我們得知:map_images
主要作用就是處理由dyld映射的image(此處的image泛指二進(jìn)制可執(zhí)行程序)。
繼續(xù)點(diǎn)進(jìn)入map_images_nolock
的實(shí)現(xiàn)部分, 我們來分析這里面主要做了什么:
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
定義一系列變量
......
必要時(shí)執(zhí)行首次初始化
//如果是第一次,就準(zhǔn)備初始化環(huán)境
if (firstTime) {
preopt_init();
}
// Find all images with Objective-C metadata.
hCount = 0;
計(jì)算class數(shù)量,根據(jù)總數(shù)調(diào)整各種表的大小。
// Count classes. Size various table based on the total.
int totalClasses = 0;
int unoptimizedTotalClasses = 0;
{
uint32_t i = mhCount;
while (i--) {
const headerType *mhdr = (const headerType *)mhdrs[i];
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) {
// no objc data in this entry
continue;
}
if (mhdr->filetype == MH_EXECUTE) {
// Size some data structures based on main executable's size
#if __OBJC2__
size_t count;
_getObjc2SelectorRefs(hi, &count);
selrefCount += count;
_getObjc2MessageRefs(hi, &count);
selrefCount += count;
#else
_getObjcSelectorRefs(hi, &selrefCount);
#endif
#if SUPPORT_GC_COMPAT
// Halt if this is a GC app.
if (shouldRejectGCApp(hi)) {
_objc_fatal_with_reason
(OBJC_EXIT_REASON_GC_NOT_SUPPORTED,
OS_REASON_FLAG_CONSISTENT_FAILURE,
"Objective-C garbage collection "
"is no longer supported.");
}
#endif
}
hList[hCount++] = hi;
if (PrintImages) {
_objc_inform("IMAGES: loading image for %s%s%s%s%s\n",
hi->fname(),
mhdr->filetype == MH_BUNDLE ? " (bundle)" : "",
hi->info()->isReplacement() ? " (replacement)" : "",
hi->info()->hasCategoryClassProperties() ? " (has class properties)" : "",
hi->info()->optimizedByDyld()?" (preoptimized)":"");
}
}
}
......
執(zhí)行一次運(yùn)行時(shí)初始化,必須將其推遲到找到可執(zhí)行文件本身為止。 這需要在進(jìn)一步初始化之前完成。(如果可執(zhí)行文件不包含Objective-C代碼,但稍后會動態(tài)加載Objective-C,則該可執(zhí)行文件可能不會出現(xiàn)在此infoList中。
if (firstTime) {
//初始化sel方法表 并注冊系統(tǒng)內(nèi)部專門的方法。
sel_init(selrefCount);
arr_init();
#if SUPPORT_GC_COMPAT
// Reject any GC images linked to the main executable.
// We already rejected the app itself above.
// Images loaded after launch will be rejected by dyld.
for (uint32_t i = 0; i < hCount; i++) {
auto hi = hList[i];
auto mh = hi->mhdr();
if (mh->filetype != MH_EXECUTE && shouldRejectGCImage(mh)) {
_objc_fatal_with_reason
(OBJC_EXIT_REASON_GC_NOT_SUPPORTED,
OS_REASON_FLAG_CONSISTENT_FAILURE,
"%s requires Objective-C garbage collection "
"which is no longer supported.", hi->fname());
}
}
#endif
#if TARGET_OS_OSX
// Disable +initialize fork safety if the app is too old (< 10.13).
// Disable +initialize fork safety if the app has a
// __DATA,__objc_fork_ok section.
if (dyld_get_program_sdk_version() < DYLD_MACOSX_VERSION_10_13) {
DisableInitializeForkSafety = true;
if (PrintInitializing) {
_objc_inform("INITIALIZE: disabling +initialize fork "
"safety enforcement because the app is "
"too old (SDK version " SDK_FORMAT ")",
FORMAT_SDK(dyld_get_program_sdk_version()));
}
}
for (uint32_t i = 0; i < hCount; i++) {
auto hi = hList[i];
auto mh = hi->mhdr();
if (mh->filetype != MH_EXECUTE) continue;
unsigned long size;
if (getsectiondata(hi->mhdr(), "__DATA", "__objc_fork_ok", &size)) {
DisableInitializeForkSafety = true;
if (PrintInitializing) {
_objc_inform("INITIALIZE: disabling +initialize fork "
"safety enforcement because the app has "
"a __DATA,__objc_fork_ok section");
}
}
break; // assume only one MH_EXECUTE image
}
#endif
}
直接開始image讀取
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
}
對源碼進(jìn)行分析, map_images_nolock
方法的流程如下:
1、 判斷是不是第一次,如果是第一次,那么就開始準(zhǔn)備初始化環(huán)境
if (firstTime) {
preopt_init();
}
2、計(jì)算class數(shù)量,根據(jù)總數(shù)調(diào)整各種表的大小(這個步驟里面會判斷GC(Garbage Collection),因?yàn)镺bjective-C之前是做了垃圾回收機(jī)制兼容的,現(xiàn)在則不支持了。盡管目前不支持GC了,但是蘋果并沒有刪除這些兼容性代碼)
// Count classes. Size various table based on the total.
int totalClasses = 0;
int unoptimizedTotalClasses = 0;
{
uint32_t i = mhCount;
while (i--) {
調(diào)整表的大小部分操作
......
GC相關(guān)邏輯判斷
#if SUPPORT_GC_COMPAT
// Halt if this is a GC app.
if (shouldRejectGCApp(hi)) {
_objc_fatal_with_reason
(OBJC_EXIT_REASON_GC_NOT_SUPPORTED,
OS_REASON_FLAG_CONSISTENT_FAILURE,
"Objective-C garbage collection "
"is no longer supported.");
}
#endif
}
hList[hCount++] = hi;
}
}
3、判斷是不是首次執(zhí)行,如果是,會初始化各種表
if (firstTime) {
sel_init(selrefCount);
arr_init();
......
繼續(xù)邏輯判斷GC相關(guān)
#if SUPPORT_GC_COMPAT
......
#endif
}
4、接著開始讀取images,并將firstTime
置為 NO
//判斷,然后進(jìn)行images讀取
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
//將firstTime置為NO,下次就不重新創(chuàng)建表了
firstTime = NO;
總結(jié)map_images_nolock
的流程就是:
- 判斷
firstTime
,firstTime
為YES,則執(zhí)行環(huán)境初始化的準(zhǔn)備,為NO就不執(zhí)行 - 計(jì)算class數(shù)量,根據(jù)總數(shù)調(diào)整各種表的大小并做了GC相關(guān)邏輯處理(不支持GC則打印提示信息)
- 判斷
firstTime
,firstTime
為YES,執(zhí)行各種表初始化操作,為NO則不執(zhí)行 - 執(zhí)行
_read_images
進(jìn)行讀取,然后將firstTime
置為NO,就不再進(jìn)入上面的邏輯了,下次進(jìn)入map_images_nolock
就開始直接_read_images
接下來我們重點(diǎn)分析_read_images
底層實(shí)現(xiàn),看看到底做了哪些主要操作,進(jìn)入源碼實(shí)現(xiàn)如下:
/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked
* list beginning with headerList.
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
定義一系列局部變量
......
1. 重新初始化TaggedPointer環(huán)境****************
if (!doneOnce) {
doneOnce = YES;
......
if (DisableTaggedPointers) {
disableTaggedPointers();
}
initializeTaggedPointerObfuscator();
......
注意!!!!!創(chuàng)建表gdb_objc_realized_classes和allocatedClasses
......
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
......
}
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
2. 開始遍歷頭文件,進(jìn)行類與元類的讀取操作并標(biāo)記(舊類改動后會生成新的類,并重映射到新的類上)************************
for (EACH_HEADER) {
//從頭文件中拿到類的信息
classref_t *classlist = _getObjc2ClassList(hi, &count);
if (! mustReadClasses(hi)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->isPreoptimized();
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
//!!!!!!核心操作,readClass讀取類的信息及類的更新
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
......
}
}
......
3. 讀取@selector*************************************
// Fix up @selector references
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->isPreoptimized()) 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]);
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
}
......
4. 讀取協(xié)議protocol*************************************
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
......
5. 處理分類category,并rebuild重建這個類的方法列表method list*******************************
// 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);
......
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
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" : "");
}
}
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);
}
}
}
}
......
if (DebugNonFragileIvars) {
realizeAllClasses();
}
最后是一堆打印***********
......
}
_read_images
的實(shí)現(xiàn)主要分為以下步驟:
- 重新初始化TaggedPointer環(huán)境
- 開始遍歷頭文件,進(jìn)行類與元類的讀取操作并標(biāo)記(舊類改動后會生成新的類,并重映射到新的類上)
- 讀取@selector方法
- 讀取協(xié)議protocol
- 處理分類category,并rebuild重建這個類的方法列表method list
既然是讀取類,類的結(jié)構(gòu)中包含類本身以及類的所有信息(例如分類,方法,協(xié)議)。
下面我們就針對這些我們想知道的內(nèi)容進(jìn)行分析:類與元類的讀取、方法@selector的讀取、協(xié)議protocol的讀取以及分類category的讀取
第1步、重新初始化TaggedPointer環(huán)境
判斷doneOnce,如果doneOnce為NO,則首先重置及初始化taggedPointer,然后創(chuàng)建兩個表,一個叫gdb_objc_realized_classes
用來存放已命名的類的列表,另一個叫allocatedClasses
用來存放已分配的所有類(和元類)
if (!doneOnce) {
doneOnce = YES;//這個邏輯只執(zhí)行一次
//重置及初始化TaggedPointer環(huán)境
if (DisableTaggedPointers) {
disableTaggedPointers();
}
initializeTaggedPointerObfuscator();
//創(chuàng)建表gdb_objc_realized_classes和allocatedClasses
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
}
分別點(diǎn)到這兩個表的定義部分,根據(jù)注釋能查看出這兩個表大概的作用
// This is a misnomer: gdb_objc_realized_classes is actually a list of
// named classes not in the dyld shared cache, whether realized or not.
gdb_objc_realized_classes實(shí)際上是不在dyld共享緩存中的已命名類的列表,無論是否實(shí)現(xiàn)
NXMapTable *gdb_objc_realized_classes; // exported for debuggers in objc-gdb.h
/***********************************************************************
* allocatedClasses
* A table of all classes (and metaclasses) which have been allocated
* with objc_allocateClassPair.
**********************************************************************/
static NXHashTable *allocatedClasses = nil;
這里拓展一下這兩張表的類型:gdb_objc_realized_classes
的類型是NXMapTable
,allocatedClasses
表的類型是NXHashTable
。
可以簡單理解NSHashTable、NSMapTable分別對應(yīng)的是我們常用的NSSet和NSDictionary,并且額外提供了weak指針來使用垃圾回收機(jī)制。
NSDictionary底層實(shí)現(xiàn)也是使用了NSMapTable(散列表),(備注:蘋果官網(wǎng)并沒有這些類的實(shí)現(xiàn),想要查看NSDictionary和NSArray的實(shí)現(xiàn)源碼可以去GNUstep官網(wǎng)下載或者百度網(wǎng)盤下載)
使用NSMapTable是因?yàn)樗鼜?qiáng)大NSMapTable相對于NSDictionary的優(yōu)勢
第2步、類與元類的讀取
遍歷頭文件,進(jìn)行類與元類的讀取操作,讀取完后標(biāo)記
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
for (EACH_HEADER) {
classref_t *classlist = _getObjc2ClassList(hi, &count);
if (! mustReadClasses(hi)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->isPreoptimized();
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
if (newCls != cls && newCls) {
// Class was moved but not deleted. Currently this occurs
// only when the new class resolved a future class.
// Non-lazily realize the class below.
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
其中主要做了readClass
來讀取編譯器編寫的類和元類,我們重點(diǎn)來仔細(xì)分析一下類的讀取過程,進(jìn)入readClass
源碼
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
const char *mangledName = cls->mangledName();
if (missingWeakSuperclass(cls)) {
// No superclass (probably weak-linked).
// Disavow any knowledge of this subclass.
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING class '%s' with "
"missing weak-linked superclass",
cls->nameForLogging());
}
addRemappedClass(cls, nil);
cls->superclass = nil;
return nil;
}
......
Class replacing = nil;
if (Class newCls = popFutureNamedClass(mangledName)) {
......
class_rw_t *rw = newCls->data();
const class_ro_t *old_ro = rw->ro;
memcpy(newCls, cls, sizeof(objc_class));
rw->ro = (class_ro_t *)newCls->data();
newCls->setData(rw);
freeIfMutable((char *)old_ro->name);
free((void *)old_ro);
addRemappedClass(cls, newCls);
replacing = cls;
cls = newCls;
}
if (headerIsPreoptimized && !replacing) {
......
assert(getClass(mangledName));
} else {
addNamedClass(cls, mangledName, replacing);
addClassTableEntry(cls);
}
......
return cls;
}
從源碼中可以看出,readClass
方法有返回值,并且包含三種邏輯處理:
- 找不到該類的父類,可能是弱綁定,直接返回nil;
- 找到類了,判斷這個類是否是一個future的類(可以理解為需要實(shí)現(xiàn)的一個類,也可以理解為這個類是否有變化),如果有變化則創(chuàng)建新類,并把舊類的數(shù)據(jù)拷貝一份然后賦值給新類newCls,然后調(diào)用addRemappedClass進(jìn)行重映射,用新的類替換掉舊的類,并返回新類newCls的地址
- 找到類了,如果類沒有任何變化,則不進(jìn)行任何操作,直接返回class
從readClass
的底層實(shí)現(xiàn)部分做個延伸思考:日常開發(fā)中,對于已經(jīng)啟動完成的工程項(xiàng)目,如果我們未修改任何類的數(shù)據(jù),那么再次點(diǎn)擊運(yùn)行會很快完成,但是一旦我們在對這些類進(jìn)行修改后,在讀取這些類的信息(包括類本身的信息以及下面我們要繼續(xù)分析的協(xié)議protocol、分類category、方法selector),就需要對該類的數(shù)據(jù)進(jìn)行更新,這個更新實(shí)際上是新建一個類,然后拷貝舊類的數(shù)據(jù)賦值給新類,然后重映射并用新類替換掉新類,這里面的拷貝以及讀寫過程其實(shí)是相當(dāng)耗時(shí)的!這是類信息改動之后項(xiàng)目再次Run運(yùn)行起來會比較慢的原因之一。
繼續(xù)分析,既然做了類信息的讀取,那么讀取到的數(shù)據(jù)到底存在哪里呢?在readClass
源碼最后部分找到這兩句代碼
addNamedClass(cls, mangledName, replacing);
addClassTableEntry(cls);
先看這兩句的第一句代碼做了什么:進(jìn)入addNameClass
找到
NXMapInsert(gdb_objc_realized_classes, name, cls);
發(fā)現(xiàn)已經(jīng)讀取完成的類,會被存放到了這個表gdb_objc_realized_classes
里面!
然后繼續(xù)看第二句里面做了啥:
static void addClassTableEntry(Class cls, bool addMeta = true) {
runtimeLock.assertLocked();
......
if (!isKnownClass(cls))
NXHashInsert(allocatedClasses, cls);
if (addMeta)
addClassTableEntry(cls->ISA(), false);
}
分析源碼注釋及源碼得出,addClassTableEntry
里面會把這個讀取完成的類直接先添加到allocatedClasses
表里面,然后再判斷addMeta
是否為YES
,然后會把當(dāng)前這個類的元類metaClass
也添加到allocatedClasses
這個表里面。
這里是一個遞歸的邏輯,我們需要來分析一下:由于我們上一步是這樣直接調(diào)用的:
addClassTableEntry(cls);
所以進(jìn)入這個方法的時(shí)候,只傳入了一個cls
并沒有傳入addMeta
,所以這里addMeta
默認(rèn)就是YES
,然后繼續(xù)遞歸調(diào)用當(dāng)前addClassTableEntry
,注意:第二次遞歸調(diào)用的時(shí)候,addMeta
傳入的是false
,所以第二次就不會再添加元類了,這里的邏輯主要是保證元類只添加一次。所以addClassTableEntry
里面其實(shí)是做了把類和元類都加到allocatedClasses
表里面。
到此為止,類和元類的讀取我們已經(jīng)明白了,下面用同樣的分析,分析類的方法、協(xié)議以及分類
第3步、方法@selector的讀取
接下來進(jìn)到第四部的代碼部分
// Fix up @selector references
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->isPreoptimized()) 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]);
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
}
點(diǎn)擊sel_registerNameNoLock
,找到__sel_registerName
,在它里面找到關(guān)鍵代碼
if (!namedSelectors) {
namedSelectors = NXCreateMapTable(NXStrValueMapPrototype,
(unsigned)SelrefCount);
}
if (!result) {
result = sel_alloc(name, copy);
// fixme choose a better container (hash not map for starters)
NXMapInsert(namedSelectors, sel_getName(result), result);
}
邏輯其實(shí)就是:把方法名插入并存儲到namedSelectors
這個表里面.
第4步,協(xié)議protocol的讀取
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
創(chuàng)建表protocol_map
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
拿到頭文件中協(xié)議列表
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
讀取protocol
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
找到關(guān)鍵函數(shù)readProtocol
,進(jìn)入發(fā)現(xiàn)其實(shí)讀取protocol的操作是把protocol存進(jìn)協(xié)議表protocol_map
。
insertFn(protocol_map, installedproto->mangledName, installedproto);
第5步,分類category的讀取
來看看分類部分的邏輯
// 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);
根據(jù)分類,獲取分類對應(yīng)的類
if (!cls) {
如果分類所屬的類找不到,那么就會把這個這個分類category_t置為nil
......
catlist[i] = nil;
......
continue;
}
......
如果分類所屬的類找到了,那么判斷這個分類里面的實(shí)例方法instanceMethods,協(xié)議protocols,以及屬性instanceProperties是否存在,如果存在,就把這些方法分別同步更新到對應(yīng)的類和元類中
bool classExists = NO;
//把分類新增的方法、協(xié)議、屬性都添加到類中
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
......
}
//把分類新增的方法、協(xié)議、屬性都添加到元類中
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
......
}
}
}
總結(jié)一下分類category的讀取,里面主要做了下面這些步驟:
- 從頭文件中獲取所有的分類列表
catlist
,然后循環(huán)遍歷這個列表 - 在循環(huán)中,判斷當(dāng)前分類
cat
所屬的類是否存在,如果不存在則把這個分類置為空catlist[i] = nil
; 如果這個分類所屬的類存在,那么開始下面兩個步驟: - 第一個步驟:判斷這個分類
cat
中是否有實(shí)例方法instanceMethods
,協(xié)議protocols
以及屬性實(shí)例instanceProperties
,如果有,那么進(jìn)入remethodizeClass
,重新rebuild
當(dāng)前類cls
的方法列表 - 第二個步驟:繼續(xù)判斷這個分類
cat
中是否有類方法classMethods
,協(xié)議protocols
以及類屬性_classProperties
,然后重新rebuild
當(dāng)前類所對應(yīng)元類cls->ISA()
的方法列表。
注意第一步和第二步這兩個方法分別對應(yīng)處理的是分類的類和分類的類對應(yīng)的元類。處理類調(diào)用的是
remethodizeClass(cls);
,而處理元類調(diào)用的是remethodizeClass(cls->ISA())
。
接下來我們進(jìn)入remethodizeClass
方法實(shí)現(xiàn)部分繼續(xù)深究這個方法實(shí)現(xiàn)步驟:
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*/))) {
......
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
找到關(guān)鍵實(shí)現(xiàn)attachCategories
函數(shù),進(jìn)入
將方法列表以及屬性和協(xié)議從類別附加到類。
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);
}
注意,這里面的類型和Class類中的類型是完全一致并且對應(yīng)的,下面貼上Class的class_rw_t
結(jié)構(gòu)做個對比
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
......
}
注意類結(jié)構(gòu)中方法表、屬性表、協(xié)議表的類型分別是method_array_t
、property_array_t
、protocol_array_t
,而這個三個表底層都是由list_array_tt
進(jìn)行實(shí)現(xiàn)的,只不過里面存儲的數(shù)據(jù)類型不相同,這個三個表里面分別對應(yīng)存儲的是method_list_t
、property_list_t
以及protocol_list_t
類型的數(shù)據(jù)
class method_array_t :
public list_array_tt<method_t, method_list_t>
{
typedef list_array_tt<method_t, method_list_t> Super;
public:
method_list_t **beginCategoryMethodLists() {
return beginLists();
}
method_list_t **endCategoryMethodLists(Class cls);
method_array_t duplicate() {
return Super::duplicate<method_array_t>();
}
};
class property_array_t :
public list_array_tt<property_t, property_list_t>
{
typedef list_array_tt<property_t, property_list_t> Super;
public:
property_array_t duplicate() {
return Super::duplicate<property_array_t>();
}
};
class protocol_array_t :
public list_array_tt<protocol_ref_t, protocol_list_t>
{
typedef list_array_tt<protocol_ref_t, protocol_list_t> Super;
public:
protocol_array_t duplicate() {
return Super::duplicate<protocol_array_t>();
}
};
并且list_array_tt
在設(shè)計(jì)的時(shí)候,提供了attachList
方法,可以調(diào)用這個方法往表里繼續(xù)添加數(shù)據(jù)
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]));
}
}
所以,通過結(jié)合objc_class
源碼,來對比分析attachCategories
源碼,我們能夠明白attachCategories
函數(shù)里面主要做了下面的操作:
- 對應(yīng)Class的結(jié)構(gòu),新建方法表
method_list_t **mlists
、property_list_t **proplists
、protocol_list_t **protolists
- 根據(jù)當(dāng)前類cls分類的數(shù)量,進(jìn)行while循環(huán),把分類里面包含的方法,協(xié)議,屬性都加到上面的三個表中
- 獲取當(dāng)前類的
rw
,通過rw
拿到對應(yīng)的methods
、properties
、protocols
,然后由于這三個表都是由list_array_tt
實(shí)現(xiàn),直接調(diào)用list_array_tt
的attachLists
方法,把category分類里面的方法,協(xié)議,屬性都添加到當(dāng)前類的表里面去。
到此,map_images
的主要操作都已經(jīng)分析完成,下面總結(jié)一下map_images
的主要流程及流程圖。
流程如下:
- 初始化環(huán)境TaggedPointer環(huán)境,同時(shí)新建兩個表:
gdb_objc_realized_classes
用來存儲讀取完成的類的類名,allocatedClasses
存儲已經(jīng)創(chuàng)建的類及元類,接下來作為類是否創(chuàng)建的邏輯判斷 - 讀取類
read_class
,如果是需要實(shí)現(xiàn)的新類,那么進(jìn)行實(shí)現(xiàn)并重映射,并用新類的地址替換舊類的地址,然后把實(shí)現(xiàn)的類的類名存儲到表gdb_objc_realized_classes
中,同時(shí)順帶把這個類以及元類都保存到表allocatedClasses
中做了關(guān)聯(lián)綁定,方便后續(xù)邏輯處理 - 讀取類的方法@selector,調(diào)用
sel_registerNameNoLock
,把方法名存儲到表namedSelectors
中 - 讀取類的protocol協(xié)議,調(diào)用
readProtocol
,把協(xié)議對象protocol_t
的mangledName
存儲到表protocol_map
中。 - 最后讀取類的分類category,category對應(yīng)兩個邏輯分別調(diào)用
remethodizeClass
,這兩個邏輯分別是:實(shí)例方法/屬性/協(xié)議添加到當(dāng)前類,而類方法/屬性/協(xié)議添加給當(dāng)前類對應(yīng)的元類,因?yàn)轭惙椒ū旧砭褪鞘谴鎯υ谠愔械摹>唧w操作就是先獲取到所有分類及中的數(shù)據(jù),添加到新的數(shù)組中,然后直接調(diào)用rw->methods.attachLists
/rw->properties.attachLists
/rw->protocols.attachLists
,利用list_array_tt
中的attachLists
方法,把這些分類,協(xié)議,屬性都添加到類和元類的rw
數(shù)據(jù)中
流程圖如下:
三、load_images流程分析
接下來我們分析load_images
底層的邏輯流程,點(diǎn)擊load_images
進(jìn)入
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);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
找到關(guān)鍵代碼
- prepare_load_methods
- call_load_methods
開始分析
1、prepare_load_methods底層實(shí)現(xiàn)
貼上prepare_load_methods
源碼
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
//先遞歸調(diào)度 類和父類
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
//再調(diào)度分類
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);
}
}
進(jìn)入schedule_class_load
,這個函數(shù)底層如下
static void schedule_class_load(Class cls)
{
......
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
......
}
這里面添加的方法add_class_to_loadable_list
的底層實(shí)現(xiàn)如下
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
取到load方法
method = cls->getLoadMethod();
......
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
我們發(fā)現(xiàn)這個添加過程實(shí)際上就是把loadable_class
類型的結(jié)構(gòu)體,存儲到表待調(diào)度load的這張表loadable_classes
中,而存儲的結(jié)構(gòu)體loadable_class
類型包含類名cls
以及該類的load
方法IMP
。
struct loadable_class {
Class cls; // may be nil
IMP method;
};
cls->getLoadMethod()
方法得到的就是該類的Load
方法
IMP
objc_class::getLoadMethod()
{
......
mlist = ISA()->data()->ro->baseMethods();
if (mlist) {
for (const auto& meth : *mlist) {
const char *name = sel_cname(meth.name);
if (0 == strcmp(name, "load")) {
return meth.imp;
}
}
}
return nil;
}
schedule_class_load
底層實(shí)現(xiàn)原理是:獲取父類,然后繼續(xù)遞歸調(diào)用schedule_class_load
,然后把這些類按父類的父類->父類->子類這個順序把類和類的load方法添加到loadable_classes
表中。這也是為什么類的+(load)
方法執(zhí)行順序是從父類到子類的。
在調(diào)用schedule_class_load
添加完成類之后,又繼續(xù)處理分類,分類內(nèi)部調(diào)用_category_getLoadMethod
拿到分類中重寫的load
方法,然后也調(diào)用add_category_to_loadable_list
把分類cat
和分類的load
方法添加到表loadable_categories
中。
所以總結(jié)prepare_load_methods
準(zhǔn)備load方法的邏輯就是:
- 先處理類:遞歸按照先父類再子類的順序,把類和類的load方法整合成一個結(jié)構(gòu)體對象
loadable_class
,然后把這個結(jié)構(gòu)體對象存到表loadable_classes
中。 - 處理完成類之后,再開始處理分類:獲取分類的load方法,把分類和分類的load方法整合成一個結(jié)構(gòu)體對象
loadable_category
然后存儲到表loadable_categories
中。
到這里,load方法準(zhǔn)備工作完畢。
2、call_load_methods底層實(shí)現(xiàn)
接下來進(jìn)入重點(diǎn),load方法的調(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) {
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;
}
先觀察這個函數(shù)實(shí)現(xiàn)部分,發(fā)現(xiàn)這個do—while循環(huán)被包含在objc_autoreleasePoolPush()
和objc_autoreleasePoolPop
中,蘋果使用了autoreleasePool是為了節(jié)省內(nèi)存開銷。
然后我們繼續(xù)來看循環(huán)體部分:
do {
//1、while循環(huán)調(diào)用call_class_loads()方法
while (loadable_classes_used > 0) {
call_class_loads();
}
//2、調(diào)用call_category_loads()并返回一個bool布爾值并賦值給more_categories
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
接下來我們繼續(xù)分析call_class_loads
和call_category_loads
底層實(shí)現(xiàn)。
先來看看調(diào)用類的load函數(shù)call_class_loads
:
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);
}
簡化源碼如下
static void call_class_loads(void)
{
......
for (i = 0; i < loadable_classes_used; i++) {
Class cls = loadable_classes[i].cls;
load_method_t load_method = (load_method_t) loadable_classes[i].method;
......
(*load_method)(cls, SEL_load);
}
......
}
這個過程其實(shí)就是從之前存儲好的表loadable_classes
中取出Class和對應(yīng)load方法的load_method_t
對象,直接調(diào)用。
然后看看調(diào)用分類的load函數(shù)call_category_loads
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()) {
......
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}
......
return new_categories_added;
}
這個過程和類的邏輯基本一致,也是就是從之前存儲好的表loadable_categories
中取出分類Category和對應(yīng)load方法的load_method_t
對象,然后通過_category_getClass
獲取到分類對應(yīng)的類,然后用類直接調(diào)用load方法。
到此為止,load_images主要流程也已經(jīng)分析完畢。load_images主要做了下面這些步驟:
第一步.準(zhǔn)備load
方法:prepare_load_methods
以先處理類,后處理分類 以及 先處理父類,后處理子類的順序存儲到待調(diào)度的表中。
類的處理邏輯:把類對象
Class
和類對應(yīng)的load方法的IMP
整合成一個loadable_class
類型的結(jié)構(gòu)體對象存儲在表loadable_classes
中。分類的處理邏輯:把分類對象
Category
和對應(yīng)的load方法IMP
整合成一個loadable_category
類型的結(jié)構(gòu)體對象存儲在表loadable_categories
中。
第二步.調(diào)用load
方法:call_load_methods
以先調(diào)用類
Class
的load,后處理分類Category
,通過分類找到對應(yīng)的類,然后由類調(diào)用load
方法的順序進(jìn)行處理這個調(diào)用處理的順序是根據(jù)準(zhǔn)備方法
prepare_load_methods
中準(zhǔn)備好的兩張表loadable_classes
和loadable_categories
的順序而來的。調(diào)用完就從表中移除,全部調(diào)用完結(jié)束循環(huán)。
下面是我對load_images
方法實(shí)現(xiàn)邏輯的的流程圖:
四、面試題答案(僅供參考~)
1、應(yīng)用程序啟動 在main
函數(shù)之前都具體做了哪些內(nèi)容?
程序啟動時(shí),系統(tǒng)XNU執(zhí)行程序的可執(zhí)行二進(jìn)制文件,從內(nèi)核態(tài)切換到用戶態(tài),根據(jù)路徑找到并運(yùn)行動態(tài)鏈接器dyld,并把控制權(quán)交給dyld,然后啟動dyld進(jìn)行程序環(huán)境初始化,然后讀取可執(zhí)行文件Mach-O,開始根據(jù)頭文件內(nèi)容讀取動態(tài)庫并初始化主程序,初始化主程序后,就開始鏈接讀取完成的動態(tài)庫到主程序可執(zhí)行文件中,然后初始化動態(tài)庫。在初始化其他動態(tài)庫之前,會最先初始化系統(tǒng)庫libsystem,運(yùn)行Runtime。系統(tǒng)庫libsystem初始化完成后,就會初始化其他動態(tài)庫,然后由Runtime調(diào)用
map_images
來讀取類、方法、協(xié)議以及分類并存儲到對應(yīng)的表中(注意:分類并不是直接存,而是通過attachLists方法把分類的數(shù)據(jù)添加到類里面),然后Runtime會繼續(xù)調(diào)用load_images
調(diào)用所有類的load方法以及分類的load方法,這些都做完之后,通過dyld提供的回調(diào)_dyld_objc_notify_register
,告訴dyld加載完畢,然后dyld就開始找主程序的入口main函數(shù),最后進(jìn)入程序的main函數(shù)。
2、load
在什么時(shí)候調(diào)用?子類、父類以及分類load
的調(diào)用順序?
load
方法調(diào)用是在應(yīng)用程序main
函數(shù)之前,應(yīng)用啟動時(shí)dyld
處理完image
鏡像文件,通過回調(diào)傳給runtime
,交由runtime
在load_images
方法中調(diào)用的。
load
方法調(diào)用順序?yàn)椋合忍幚眍悾筇幚矸诸悾惶幚眍惖捻樞蚴窍雀割悾笞宇?/p>在調(diào)用類的
load
方法時(shí),做了遞歸處理,會先調(diào)用父類的load,然后再調(diào)用子類的load,所有類的load方法調(diào)用完成后,才會開始處理所有類的分類,分類的處理順序取決于Mach-O頭文件,和類的順序沒有直接關(guān)系。先后順序即:父類->子類->所有類的分類驗(yàn)證方式:實(shí)現(xiàn)子類和父類,重寫load方法,在其中進(jìn)行NSLog打印便可以看出,這里我就不驗(yàn)證了。