iOS底層原理 13 :dyld與objc的關聯

本文的主要目的是理解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_registerdyld的注冊

下面針對以上模塊具體分析:

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);
    }
for循環打印結果.png
  • 通過終端命令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:打印 ClassCategory 的 + (void)load 方法的調用信息

  • NSDoubleLocalizedStrings:項目做國際化本地化(Localized)的時候是一個挺耗時的工作,想要檢測國際化翻譯好的語言文字UI會變成什么樣子,可以指定這個啟動項。可以設置 NSDoubleLocalizedStrings 為YES。

  • NSShowNonLocalizedStrings:在完成國際化的時候,偶爾會有一些字符串沒有做本地化,這時就可以設置NSShowNonLocalizedStringsYES,所有沒有被本地化的字符串全都會變成大寫。

環境變量 - OBJC_DISABLE_NONPOINTER_ISA

設置環境變量.png
  • 在未設置OBJC_DISABLE_NONPOINTER_ISA環境變量時,isanonpointer屬性為01

  • 當我們設置OBJC_DISABLE_NONPOINTER_ISAYES時,isanonpointer屬性為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:dyldimage(鏡像文件)加載進內存時,會觸發該函數

  • load_image:dyld初始化image會觸發該函數

  • unmap_image:dyldimage移除時,會觸發該函數。

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中全局搜索 sNotifyObjcMappedregisterObjCNotifiers -- 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 字段的訪問

參考文章 http://www.lxweimin.com/p/25bcb6540045

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380