本文的主要目的是理解dyld與objc是如何關聯的
_objc_init 源碼解析
我們來看一下libobjc
中_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?
//讀取影響運行時的環境變量,如果需要,還可以打開環境變量幫助 export OBJC_HRLP = 1
environ_init();
//關于線程key的綁定,例如線程數據的析構函數
tls_init();
//運行C++靜態構造函數,在dyld調用我們的靜態析構函數之前,libc會調用_objc_init(),因此我們必須自己做
static_init();
//runtime運行時環境初始化,里面主要是unattachedCategories、allocatedClasses -- 分類初始化
runtime_init();
//初始化libobjc的異常處理系統
exception_init();
//緩存條件初始化
cache_init();
//啟動回調機制,通常這不會做什么,因為所有的初始化都是惰性的,但是對于某些進程,我們會迫不及待地加載trampolines dylib
_imp_implementationWithBlock_init();
/*
_dyld_objc_notify_register -- dyld 注冊的地方
- 僅供objc運行時使用
- 注冊處理程序,以便在映射、取消映射 和初始化objc鏡像文件時使用,dyld將使用包含objc_image_info的鏡像文件數組,回調 mapped 函數
map_images:dyld將image鏡像文件加載進內存時,會觸發該函數
load_images:dyld初始化image會觸發該函數
unmap_image:dyld將image移除時會觸發該函數
*/
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
根據源碼所知,主要分為以下幾部分:
environ_init
:初始化一系列環境變量,并讀取影響運行時的環境變量tls_init
:關于線程key的綁定static_init
:運行C++靜態構造函數(只會運行系統級別的構造函數),在dyld調用靜態析構函數之前,libc會調用_objc_init
runtime_init
:runtime運行時環境初始化,里面操作是unattachedCategories、allocatedClasses(表的初始化)exception_init
:初始化libObjc的異常處理系統cache_init
: cache緩存初始化_imp_implementationWithBlock_init
:啟動回調機制,通常這不會做什么,因為所有的初始化都是惰性的,但是對于某些進程,我們會迫不及待地加載trampolines dylib_dyld_objc_notify_register
:dyld的注冊
下面針對以上模塊具體分析:
1、environ_init方法:環境變量初始化
environ_init
方法的源碼如下,其中的關鍵代碼是 for 循環
void environ_init(void)
{
//...省略部分邏輯
if (PrintHelp || PrintOptions) {
//...省略部分邏輯
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);
}
}
}
有以下兩種方法可以打印所有的環境變量
- 通過·environ_init
的源碼,將
for·循環單獨拿出來,去除所有條件,打印環境變量
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
_objc_inform("%s: %s", opt->env, opt->help);
_objc_inform("%s is set", opt->env);
}
- 通過終端命令
export OBJC_HELP=1
,打印環境變量
export OBJC_HELP=1.png
我們可以在Xcode中,通過target -- Edit Scheme -- Run --Arguments -- Environment Variables
設置環境變量,控制打印的結果
DYLD_PRINT_STATISTICS
:設置 DYLD_PRINT_STATISTICS 為YES,控制臺就會打印 App 的加載時長,包括整體加載時長和動態庫加載時長,即main函數之前的啟動時間(查看pre-main耗時),可以通過設置了解其耗時部分,并對其進行啟動優化。
OBJC_DISABLE_NONPOINTER_ISA
:杜絕生成相應的nonpointer isa
(nonpointer isa指針地址 末尾為1 ),生成的都是普通的isa
OBJC_PRINT_LOAD_METHODS
:打印Class
及Category
的 + (void)load 方法的調用信息NSDoubleLocalizedStrings
:項目做國際化本地化(Localized)的時候是一個挺耗時的工作,想要檢測國際化翻譯好的語言文字UI會變成什么樣子,可以指定這個啟動項。可以設置 NSDoubleLocalizedStrings 為YES。NSShowNonLocalizedStrings
:在完成國際化的時候,偶爾會有一些字符串沒有做本地化,這時就可以設置NSShowNonLocalizedStrings
為YES
,所有沒有被本地化的字符串全都會變成大寫。
環境變量 - OBJC_DISABLE_NONPOINTER_ISA
-
在未設置
OBJC_DISABLE_NONPOINTER_ISA
環境變量時,isa
的nonpointer
屬性為01
-
當我們設置
OBJC_DISABLE_NONPOINTER_ISA
為YES
時,isa
的nonpointer
屬性為00
所以OBJC_DISABLE_NONPOINTER_ISA
可以控制isa
優化開關,從而優化整個內存結構
環境變量 - OBJC_PRINT_LOAD_METHODS
- 配置打印
load 方法
的環境變量OBJC_PRINT_LOAD_METHODS
,設置為YES - 在LGPerson類中重寫+load函數,運行程序,load函數的打印如下
所以,OBJC_PRINT_LOAD_METHODS
可以監控
所有的+load方法
,從而處理啟動優化
2、tls_init:線程key的綁定
主要是本地線程池的初始化以及析構
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS//本地線程池,用來進行處理
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);//初始init
#else
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);//析構
#endif
}
3、static_init:運行系統級別的C++靜態構造函數
主要是運行系統級別的C++靜態構造函數,在dyld調用我們的靜態構造函數之前,libc調用_objc_init方法,即系統級別的C++構造函數 先于 自定義的C++構造函數 運行
4、runtime_init:運行時環境初始化
主要是運行時的初始化,主要分為兩部分:分類初始化、類的表初始化(后續會詳細講解對應的函數)
5、 exception_init:初始化libobjc的異常處理系統
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
- 當有crash(
crash是指系統發生的不允許的一些指令,然后系統給的一些信號
)發生時,會來到_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層會傳入一個函數用于處理異常,以便于調用函數,然后回到原有的app層中,如下所示,其中fn即為傳入的函數,即uncaught_handler
等于 fn
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
// fn為設置的異常句柄 傳入的函數,為外界給的
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn; //賦值
return result;
}
NSException 應用級異常
:它是未被捕獲的Objective-C異常,導致程序向自身發送了SIGABRT信號
而崩潰,對于未捕獲的Objective-C異常,是可以通過try catch來捕獲的,或者通過NSSetUncaughtExceptionHandler()
機制來捕獲。
針對應用級異常,可以通過注冊異常捕獲的函數,即NSSetUncaughtExceptionHandler機制
,實現線程保活, 收集上傳崩潰日志
應用級crash攔截
所以在開發中,會針對crash進行攔截處理,即app代碼中給一個異常句柄NSSetUncaughtExceptionHandler
,傳入一個函數給系統,當異常發生后,調用函數(函數中可以線程保活、收集并上傳崩潰日志),然后回到原有的app層中,其本質就是一個回調函數,如下圖所示
處理NSException 應用級異常,我們要做的就是用自定義的函數替代該ExceptionHandler即可
6、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++;
}
//為當前任務注冊一組可重新啟動的緩存
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
}
7、_imp_implementationWithBlock_init:啟動回調機制
該方法主要是啟動回調機制,通常這不會做什么,因為所有的初始化都是惰性的,但是對于某些進程,我們會迫不及待地加載libobjc-trampolines.dylib,其源碼如下
8、_dyld_objc_notify_register:dyld注冊
//
// 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圖像時調用
dyld
將會通過一個包含objc-image-info
的鏡像文件的數組回調mapped函數
方法中的三個參數分別表示的含義如下:
map_images:
dyld
將image(鏡像文件)
加載進內存時,會觸發該函數load_image:
dyld
初始化image
會觸發該函數unmap_image:
dyld
將image移除
時,會觸發該函數。
dyld與Objc的關聯
我們結合libobjc源碼和dyld源碼看出,在libobjc
中調用_dyld_objc_notify_register的函數
,其實是在dyld源碼
中定義的
// dyld源碼--具體實現
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源碼中--調用
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
從上傳入的參數可以得出:
-
mapped
等價于map_images
-
init
等價于load_images
-
unmapped
等價于unmap_image
在學習dyld的加載流程中,我們知道load_images
是在notifySingle方法
中,通過sNotifyObjCInit
調用的,如下所示:
通過查詢sNotifyObjCMapped
,得到sNotifyObjCInit
等價于_dyld_objc_notify_init
static _dyld_objc_notify_mapped sNotifyObjCMapped;
static _dyld_objc_notify_init sNotifyObjCInit;
static _dyld_objc_notify_unmapped sNotifyObjCUnmapped;
通過_dyld_objc_notify_init
,最終找到_dyld_objc_notify_register --> registerObjCNotifiers
,在該方法中將_dyld_objc_notify_register
傳入的參數賦值給了3個回調方法
所以 有以下等價關系
sNotifyObjCMapped == mapped == map_images
sNotifyObjCInit == init == load_images
sNotifyObjCUnmapped == unmapped == unmap_image
map_images調用時機
關于load_images的調用時機已經在dyld加載流程中講解過了,下面以map_images為例,看看其調用時機
- dyld中全局搜索
sNotifyObjcMapped
:registerObjCNotifiers -- notifyBatchPartial -- sNotifyObjCMapped
- 全局搜索
notifyBatchPartial
,在registerObjCNotifiers
方法中調用
dyld與Objc關聯圖
附錄
環境變量匯總
環境變量名 | 說明 |
---|---|
OBJC_PRINT_OPTIONS | 輸出OBJC已設置的選項 |
OBJC_PRINT_IMAGES | 輸出已load的image信息 |
OBJC_PRINT_LOAD_METHODS | 打印 Class 及 Category 的 + (void)load 方法的調用信息 |
OBJC_PRINT_INITIALIZE_METHODS | 打印 Class 的 + (void)initialize 的調用信息 |
OBJC_PRINT_RESOLVED_METHODS | 打印通過 +resolveClassMethod: 或 +resolveInstanceMethod: 生成的類方法 |
OBJC_PRINT_CLASS_SETUP | 打印 Class 及 Category 的設置過程 |
OBJC_PRINT_PROTOCOL_SETUP | 打印 Protocol 的設置過程 |
OBJC_PRINT_IVAR_SETUP | 打印 Ivar 的設置過程 |
OBJC_PRINT_VTABLE_SETUP | 打印 vtable 的設置過程 |
OBJC_PRINT_VTABLE_IMAGES | 打印 vtable 被覆蓋的方法 |
OBJC_PRINT_CACHE_SETUP | 打印方法緩存的設置過程 |
OBJC_PRINT_FUTURE_CLASSES | 打印從 CFType 無縫轉換到 NSObject 將要使用的類(如 CFArrayRef 到 NSArray * ) |
OBJC_PRINT_GC | 打印一些垃圾回收操作 |
OBJC_PRINT_PREOPTIMIZATION | 打印 dyld 共享緩存優化前的問候語 |
OBJC_PRINT_CXX_CTORS | 打印類實例中的 C++ 對象的構造與析構調用 |
OBJC_PRINT_EXCEPTIONS | 打印異常處理 |
OBJC_PRINT_EXCEPTION_THROW | 打印所有異常拋出時的 Backtrace |
OBJC_PRINT_ALT_HANDLERS | 打印 alt 操作異常處理 |
OBJC_PRINT_REPLACED_METHODS | 打印被 Category 替換的方法 |
OBJC_PRINT_DEPRECATION_WARNINGS | 打印所有過時的方法調用 |
OBJC_PRINT_POOL_HIGHWATER | 打印 autoreleasepool 高水位警告 |
OBJC_PRINT_CUSTOM_RR | 打印含有未優化的自定義 retain/release 方法的類 |
OBJC_PRINT_CUSTOM_AWZ | 打印含有未優化的自定義 allocWithZone 方法的類 |
OBJC_PRINT_RAW_ISA | 打印需要訪問原始 isa 指針的類 |
OBJC_DEBUG_UNLOAD | 卸載有不良行為的 Bundle 時打印警告 |
OBJC_DEBUG_FRAGILE_SUPERCLASSES | 當子類可能被對父類的修改破壞時打印警告 |
OBJC_DEBUG_FINALIZERS | 警告實現了 -dealloc 卻沒有實現 -finalize 的類 |
OBJC_DEBUG_NIL_SYNC | 警告 @synchronized(nil) 調用,這種情況不會加鎖 |
OBJC_DEBUG_NONFRAGILE_IVARS | 打印突發地重新布置 non-fragile ivars 的行為 |
OBJC_DEBUG_ALT_HANDLERS | 記錄更多的 alt 操作錯誤信息 |
OBJC_DEBUG_MISSING_POOLS | 警告沒有 pool 的情況下使用 autorelease,可能內存泄漏 |
OBJC_DEBUG_DUPLICATE_CLASSES | 當出現類重名時停機 |
OBJC_USE_INTERNAL_ZONE | 在一個專用的 malloc 區分配運行時數據 |
OBJC_DISABLE_GC | 強行關閉自動垃圾回收,即使可執行文件需要垃圾回收 |
OBJC_DISABLE_VTABLES | 關閉 vtable 分發 |
OBJC_DISABLE_PREOPTIMIZATION | 關閉 dyld 共享緩存優化前的問候語 |
OBJC_DISABLE_TAGGED_POINTERS | 關閉 NSNumber 等的 tagged pointer 優化 |
OBJC_DISABLE_NONPOINTER_ISA | 關閉 non-pointer isa 字段的訪問 |