前言
- 在上一篇中我們了解了dyld加載的流程,此篇我們將介紹dyld與objc的關(guān)聯(lián)。
_objc_init分析
- _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?
//讀取運(yùn)行時的環(huán)境變量,可以透過打開環(huán)境變量幫助 export OBJC_HELP = 1
environ_init();
//關(guān)於線程key的綁定,例如線程數(shù)據(jù)的析構(gòu)函數(shù)
tls_init();
//運(yùn)行C++靜態(tài)構(gòu)造函數(shù),在dyld調(diào)用我們的靜態(tài)析構(gòu)函數(shù)之前,libc會調(diào)用_objc_init()
static_init();
//runtime運(yùn)行時環(huán)境初始化,裡面主要是unattachedCategories,allocatedClasses -- 分類初始化
runtime_init();
//初始化libobjc的異常處理系統(tǒng)
exception_init();
//緩存條件初始化
cache_init();
//啟動回調(diào)機(jī)制,通常這不會做什麼,因為所有的初始化都是惰性的,但是對於某些進(jìn)程,我們會迫不及待的加載Trampolines dylib
_imp_implementationWithBlock_init();
/*
_dyld_objc_notify_register -> dyld 註冊的地方
- 僅供objc運(yùn)行時使用
- 註冊處理程序,以便在映射,取消映射 和初始化objc鏡像文件時使用,dyld將使用包含objc-image-info的鏡像文件數(shù)組,回調(diào)mapped函數(shù)
map_image:dyld將image鏡像文件加載進(jìn)內(nèi)存時 會觸發(fā)函數(shù)
load_images:dyld初始化image會觸發(fā)該函數(shù)
unmap_image:dyld將image移除時會觸發(fā)該函數(shù)
*/
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
environ_init方法:環(huán)境變量初始化
- environ_init方法的部分源碼如下
- 省略部分邏輯,顯示關(guān)鍵部分
/***********************************************************************
* environ_init
* Read environment variables that affect the runtime.
* Also print environment variable help, if requested.
**********************************************************************/
void environ_init(void)
{
//省略部分邏輯
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help);
if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env);
}
}
打印環(huán)境變量
- 我們可以透過以下兩種方式達(dá)到打印環(huán)境變量的效果
-
第一種:將打印變量的for循環(huán)單獨(dú)拿出,如下圖所示

-
第二種:透過終端機(jī)修改環(huán)境變量

這個環(huán)境變量,可以透過xcode的target -> Edit Scheme -> Run -> Arguments -> Environment Variables 進(jìn)行配置,如下圖

示範(fàn)一:OBJC_DISABLE_NONPOINTER_ISA
- 透過修改環(huán)境變量
OBJC_DISABLE_NONPOINTER_ISA設(shè)置為YES,如下圖所示

- 在未設(shè)置時
OBJC_DISABLE_NONPOINTER_ISA前,isa地址的二進(jìn)制打印末尾為1,如下圖

- 在設(shè)置了
OBJC_DISABLE_NONPOINTER_ISA後,isa地址的二進(jìn)制打印末尾為0,如下圖

- 由此我們可以看到,我們可以透過修改環(huán)境變量
OBJC_DISABLE_NONPOINTER_ISA來改變isa的第一位,即isa優(yōu)化開關(guān),進(jìn)而優(yōu)化整個內(nèi)存結(jié)構(gòu)。

示範(fàn)二:OBJC_PRINT_LOAD_METHODS
- 在LGPerson類中重寫+load方法。
- 透過修改環(huán)境變量
OBJC_PRINT_LOAD_METHODS設(shè)置為YES,可以監(jiān)控所有+load方法。

- 所以我們可以透過
OBJC_PRINT_LOAD_METHODS監(jiān)控load方法,從而處理啟動優(yōu)化
tls_init方法:線程key的綁定
tls_init方法主要是對於本地線程的初始化以及析構(gòu),源碼如下
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS //本地線程池,用來進(jìn)行處理
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific); //初始化init
#else
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific); //析構(gòu)
#endif
}
static_init:運(yùn)行系統(tǒng)級別的C++靜態(tài)構(gòu)造函數(shù)
主要是運(yùn)行行系統(tǒng)級別的C++靜態(tài)構(gòu)造函數(shù),在dyld調(diào)用我們的靜態(tài)構(gòu)造函數(shù)之前,libc調(diào)用_objc_init方法,即系統(tǒng)級別的C++構(gòu)造函數(shù) 比 自定義的C++構(gòu)造函數(shù) 先運(yùn)行
/***********************************************************************
* static_init
* Run C++ static constructor functions.
* libc calls _objc_init() before dyld would call our static constructors,
* so we have to do it ourselves.
**********************************************************************/
static void static_init()
{
size_t count;
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
inits[i]();
}
}
runtime_init:運(yùn)行時環(huán)境初始化
主要是運(yùn)行時的環(huán)境初始化,主要分為兩個部分,分類初始化,類的表初始化
void runtime_init(void)
{
objc::unattachedCategories.init(32);//分類初始化
objc::allocatedClasses.init(); //初始化 -> 開闢類的表
}
exception_init方法:初始化libobjc的異常處理系統(tǒng)
主要是初始化libobjc的異常處理系統(tǒng),註冊異常處理的回調(diào),從而監(jiān)控、監(jiān)控異常的處理,源碼如下
/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
- 當(dāng)有crash(所謂的crash是指系統(tǒng)發(fā)生不允許的一些指令,然後系統(tǒng)給的一些信號),發(fā)生時,會來到
_objc_terminate方法,走到uncaught_handler扔出異常。
/***********************************************************************
* _objc_terminate
* Custom std::terminate handler.
*
* The uncaught exception callback is implemented as a std::terminate handler.
* 1\. Check if there's an active exception
* 2\. If so, check if it's an Objective-C exception
* 3\. If so, call our registered callback with the object.
* 4\. Finally, call the previous terminate handler.
**********************************************************************/
static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
if (PrintExceptions) {
_objc_inform("EXCEPTIONS: terminating");
}
if (! __cxa_current_exception_type()) {
// No current exception.
(*old_terminate)();
}
else {
// There is a current exception. Check if it's an objc exception.
@try {
__cxa_rethrow();
} @catch (id e) {
// It's an objc object. Call Foundation's handler, if any.
(*uncaught_handler)((id)e);
(*old_terminate)();
} @catch (...) {
// It's not an objc object. Continue to C++ terminate.
(*old_terminate)();
}
}
}
- 搜索
uncaught_handler,在app層會傳入一個函數(shù)用於處理異常,以便於調(diào)用函數(shù),然後到原有的app層中,如下所示其中fn即為傳入的函數(shù),即uncaught_handler等於fn
/***********************************************************************
* objc_setUncaughtExceptionHandler
* Set a handler for uncaught Objective-C exceptions.
* Returns the previous handler.
**********************************************************************/
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn;
return result;
}
crash分類
crash的主要原因是收到了未處理的信號,主要來源於三個地方:
- kernel 內(nèi)核
- 其他進(jìn)行
- App本身
所以相對應(yīng)的crash也分為三種
- Mach異常:是指最底層的內(nèi)核級異常,開發(fā)者可以直接透過Mach API設(shè)置thread,task,host的異常端口,來補(bǔ)獲Mach異常
- Unix信號:又稱BSD信號,如果開發(fā)者沒有捕獲Mach異常,則會被host層的方法ux_exception()將異常轉(zhuǎn)換為對應(yīng)的UNIX信號,並通過方法threadsignal()將信號投遞到出錯的線程.可以通過方法
signal(x, SignalHandler)來捕獲single - NSException 應(yīng)用級異常:它是未被捕獲的Objective-C異常,導(dǎo)致程序像自身發(fā)送了SIGABRT信號而崩潰,對於未捕獲的Objective-C異常,可以使用try catch來捕獲的,或者通過NSSetUncaughtExceptionHandler()機(jī)制捕獲。
針對應(yīng)用級異常,可以通過註冊異常捕獲的函數(shù),即NSSetUncaughtExceptionHandler 機(jī)制,實(shí)現(xiàn)線程?;?,收集上傳崩潰日誌。
應(yīng)用級crash攔截
所以在開發(fā)中,會針對crash進(jìn)行攔截處理,即app代碼中的一個異常句柄NSSetUncaughtExceptionHandler,傳入一個函數(shù)給系統(tǒng),當(dāng)異常發(fā)生後,調(diào)用函數(shù)(函數(shù)中可以線程保活,收集並上傳崩潰日誌),然後回到原有的app層中,其本質(zhì)就是一個回調(diào)函數(shù),如下圖所示

- 上述方式只適合收集應(yīng)用級異常,我們要做的就是用自定義的函數(shù)替代
ExceptionHandler即可。
cache_init:緩存初始化
- 主要是緩存初始化,源碼如下
void cache_init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
mach_msg_type_number_t count = 0;
kern_return_t kr;
while (objc_restartableRanges[count].location) {
count++;
}
//為當(dāng)前任務(wù)註冊一組可重新啟動的緩存
kr = task_restartable_ranges_register(mach_task_self(),
objc_restartableRanges, count);
if (kr == KERN_SUCCESS) return;
_objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}
_imp_implementationWithBlock_init 啟動回調(diào)機(jī)制
該方法主要是啟動回調(diào)機(jī)制,通常這不會做什麼,因為所有的初始化都是惰性的,但是對於某些進(jìn)程,我們會迫不及待的加載libobjc-trampolines.dylib 其源碼如下
/// Initialize the trampoline machinery. Normally this does nothing, as
/// everything is initialized lazily, but for certain processes we eagerly load
/// the trampolines dylib.
void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
// Eagerly load libobjc-trampolines.dylib in certain processes. Some
// programs (most notably QtWebEngineProcess used by older versions of
// embedded Chromium) enable a highly restrictive sandbox profile which
// blocks access to that dylib. If anything calls
// imp_implementationWithBlock (as AppKit has started doing) then we'll
// crash trying to load it. Loading it here sets it up before the sandbox
// profile is enabled and blocks it.
// 在某些進(jìn)程中渴望加載libobjc-trampolines.dylib。一些程序(最著名的是嵌入式Chromium的較早版本使用的QtWebEngineProcess)啟用了嚴(yán)格限制的沙箱配置文件,從而阻止了對該dylib的訪問。如果有任何調(diào)用imp_implementationWithBlock的操作(如AppKit開始執(zhí)行的操作),那麼我們將在嘗試加載它時崩潰。將其加載到此處可在啟用沙箱配置文件之前對其進(jìn)行設(shè)置並阻止它。
// This fixes EA Origin (rdar://problem/50813789)
// and Steam (rdar://problem/55286131)
if (__progname &&
(strcmp(__progname, "QtWebEngineProcess") == 0 ||
strcmp(__progname, "Steam Helper") == 0)) {
Trampolines.Initialize();
}
#endif
}
_dyld_objc_notify_register:dyld註冊
_dyld_objc_notify_register方法
這個方法的具體實(shí)現(xiàn)在上一篇dyld加載已經(jīng)有詳細(xì)說明,其源碼實(shí)現(xiàn)是在dyld源碼中,以下是_dyld_objc_notify_register 方法的聲明
//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
從註釋中可以看出
- 僅提供objc運(yùn)行時使用
- 註冊處理程序,以便在映射,取消映射和初始化objc圖像時調(diào)用
- dyld將會通過一個包含objc-image-info的鏡像文件的數(shù)組回調(diào)mapped函數(shù)
方法中的三個參數(shù)分別表示的含義如下:
- map_image: dyld將image(鏡像文件)加載進(jìn)內(nèi)存,會觸發(fā)該函數(shù)
- load_image: dyld初始化image會觸發(fā)該函數(shù)
- unmap_image: dyld將將image移除時,會觸發(fā)該函數(shù)
dyld與objc關(guān)聯(lián)
//dyld源碼->具體實(shí)現(xiàn)
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
//libobjc源碼中->調(diào)用
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
從上述的調(diào)用與具體實(shí)現(xiàn)的源碼可以看出
-
mapped等價於map_images -
init等價於load_images -
unmapped等價於unmap_image
在dyld加載中,我們知道load_images 是在notifySingle 方法中,通過sNotifyObjcInit調(diào)用,如下所示

然後通過查找sNotifyObjCInit,最終找到了_dyld_objc_notify_register-> registerObjCNotifiers 在該方法中將_dyld_objc_notify_register 傳入的參數(shù)賦值給了三個回調(diào)方法。

依照調(diào)用關(guān)係我們發(fā)現(xiàn)
-
sNotifyObjCMapped==mapped==map_images -
sNotifyObjCInit==init==load_images -
sNotifyObjCUnmapped==unmapped==unmap_image
map_images調(diào)用時機(jī)
- 關(guān)於load_images的調(diào)用時機(jī)已經(jīng)在dyld加載流程講解過,下面以map_images為例,看看其調(diào)用時機(jī)
- dyld中全局搜索
sNotifyObjcMapped:registerObjCNotifiers -- notifyBatchPartial -- sNotifyObjCMapped

- 全局搜索
notifyBatchPartial,在registerObjCNotifiers方法中調(diào)用

所以有以下結(jié)論map_images是先於load_images調(diào)用,即先map_images ,再load_images
- 結(jié)合上一篇dyld加載流程,dyld與Objc的關(guān)聯(lián)如下圖所示

在dyld中註冊回調(diào)函數(shù),可以理解為
添加觀察者在objc中dyld註冊,可以理解為
發(fā)送通知觸發(fā)回調(diào),可以理解為執(zhí)行通知selector