dyld與objc的關(guān)聯(lián)

前言

  • 在上一篇中我們了解了dyld加載的流程润讥,此篇我們將介紹dyld與objc的關(guān)聯(lián)倔监。

dyld 加載流程

_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)行時(shí)的環(huán)境變量定鸟,可以透過(guò)打開(kāi)環(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會(huì)調(diào)用_objc_init()
    static_init();
    //runtime運(yùn)行時(shí)環(huán)境初始化羽德,裡面主要是unattachedCategories,allocatedClasses -- 分類初始化
    runtime_init();
    //初始化libobjc的異常處理系統(tǒng)
    exception_init();
    //緩存條件初始化
    cache_init();
    //啟動(dòng)回調(diào)機(jī)制迅办,通常這不會(huì)做什麼宅静,因?yàn)樗械某跏蓟际嵌栊缘模菍?duì)於某些進(jìn)程站欺,我們會(huì)迫不及待的加載Trampolines dylib
    _imp_implementationWithBlock_init();

    /*
    _dyld_objc_notify_register -> dyld 註冊(cè)的地方
    - 僅供objc運(yùn)行時(shí)使用
    - 註冊(cè)處理程序姨夹,以便在映射,取消映射 和初始化objc鏡像文件時(shí)使用镊绪,dyld將使用包含objc-image-info的鏡像文件數(shù)組匀伏,回調(diào)mapped函數(shù)

     map_image:dyld將image鏡像文件加載進(jìn)內(nèi)存時(shí) 會(huì)觸發(fā)函數(shù)
     load_images:dyld初始化image會(huì)觸發(fā)該函數(shù)
     unmap_image:dyld將image移除時(shí)會(huì)觸發(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)境變量

  • 我們可以透過(guò)以下兩種方式達(dá)到打印環(huán)境變量的效果
  • 第一種:將打印變量的for循環(huán)單獨(dú)拿出蝴韭,如下圖所示
  • 第二種:透過(guò)終端機(jī)修改環(huán)境變量

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

示範(fàn)一:OBJC_DISABLE_NONPOINTER_ISA

  • 透過(guò)修改環(huán)境變量OBJC_DISABLE_NONPOINTER_ISA 設(shè)置為YES榄鉴,如下圖所示
  • 在未設(shè)置時(shí)OBJC_DISABLE_NONPOINTER_ISA 前履磨,isa地址的二進(jìn)制打印末尾為1,如下圖
  • 在設(shè)置了OBJC_DISABLE_NONPOINTER_ISA 後庆尘,isa地址的二進(jìn)制打印末尾為0剃诅,如下圖
  • 由此我們可以看到,我們可以透過(guò)修改環(huán)境變量OBJC_DISABLE_NONPOINTER_ISA來(lái)改變isa的第一位驶忌,即isa優(yōu)化開(kāi)關(guān)矛辕,進(jìn)而優(yōu)化整個(gè)內(nèi)存結(jié)構(gòu)。

示範(fàn)二:OBJC_PRINT_LOAD_METHODS

  • 在LGPerson類中重寫(xiě)+load方法付魔。
  • 透過(guò)修改環(huán)境變量OBJC_PRINT_LOAD_METHODS 設(shè)置為YES聊品,可以監(jiān)控所有+load方法。
  • 所以我們可以透過(guò)OBJC_PRINT_LOAD_METHODS 監(jiān)控load方法几苍,從而處理啟動(dòng)優(yōu)化

tls_init方法:線程key的綁定

tls_init方法主要是對(duì)於本地線程的初始化以及析構(gòu)翻屈,源碼如下

void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS //本地線程池,用來(lái)進(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)級(jí)別的C++靜態(tài)構(gòu)造函數(shù)

主要是運(yùn)行行系統(tǒng)級(jí)別的C++靜態(tài)構(gòu)造函數(shù)妻坝,在dyld調(diào)用我們的靜態(tài)構(gòu)造函數(shù)之前伸眶,libc調(diào)用_objc_init方法,即系統(tǒng)級(jí)別的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)行時(shí)環(huán)境初始化

主要是運(yùn)行時(shí)的環(huán)境初始化刽宪,主要分為兩個(gè)部分厘贼,分類初始化,類的表初始化

void runtime_init(void)
{
    objc::unattachedCategories.init(32);//分類初始化
    objc::allocatedClasses.init(); //初始化 -> 開(kāi)闢類的表
}

exception_init方法:初始化libobjc的異常處理系統(tǒng)

主要是初始化libobjc的異常處理系統(tǒng)圣拄,註冊(cè)異常處理的回調(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)給的一些信號(hào)),發(fā)生時(shí)族铆,會(huì)來(lái)到_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層會(huì)傳入一個(gè)函數(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的主要原因是收到了未處理的信號(hào),主要來(lái)源於三個(gè)地方:

  • kernel 內(nèi)核
  • 其他進(jìn)行
  • App本身

所以相對(duì)應(yīng)的crash也分為三種

  • Mach異常:是指最底層的內(nèi)核級(jí)異常栅葡,開(kāi)發(fā)者可以直接透過(guò)Mach API設(shè)置thread茉兰,task,host的異常端口欣簇,來(lái)補(bǔ)獲Mach異常
  • Unix信號(hào):又稱BSD信號(hào)规脸,如果開(kāi)發(fā)者沒(méi)有捕獲Mach異常,則會(huì)被host層的方法ux_exception()將異常轉(zhuǎn)換為對(duì)應(yīng)的UNIX信號(hào)熊咽,並通過(guò)方法threadsignal()將信號(hào)投遞到出錯(cuò)的線程.可以通過(guò)方法signal(x, SignalHandler) 來(lái)捕獲single
  • NSException 應(yīng)用級(jí)異常:它是未被捕獲的Objective-C異常莫鸭,導(dǎo)致程序像自身發(fā)送了SIGABRT信號(hào)而崩潰,對(duì)於未捕獲的Objective-C異常横殴,可以使用try catch來(lái)捕獲的被因,或者通過(guò)NSSetUncaughtExceptionHandler()機(jī)制捕獲。

針對(duì)應(yīng)用級(jí)異常衫仑,可以通過(guò)註冊(cè)異常捕獲的函數(shù)梨与,即NSSetUncaughtExceptionHandler 機(jī)制,實(shí)現(xiàn)線程被蟪耄活蛋欣,收集上傳崩潰日誌。

應(yīng)用級(jí)crash攔截

所以在開(kāi)發(fā)中如贷,會(huì)針對(duì)crash進(jìn)行攔截處理陷虎,即app代碼中的一個(gè)異常句柄NSSetUncaughtExceptionHandler,傳入一個(gè)函數(shù)給系統(tǒng)杠袱,當(dāng)異常發(fā)生後尚猿,調(diào)用函數(shù)(函數(shù)中可以線程保活楣富,收集並上傳崩潰日誌)凿掂,然後回到原有的app層中,其本質(zhì)就是一個(gè)回調(diào)函數(shù),如下圖所示

  • 上述方式只適合收集應(yīng)用級(jí)異常庄萎,我們要做的就是用自定義的函數(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ù)註冊(cè)一組可重新啟動(dòng)的緩存
    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 啟動(dòng)回調(diào)機(jī)制

該方法主要是啟動(dòng)回調(diào)機(jī)制糠涛,通常這不會(huì)做什麼援奢,因?yàn)樗械某跏蓟际嵌栊缘模菍?duì)於某些進(jìn)程忍捡,我們會(huì)迫不及待的加載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)格限制的沙箱配置文件,從而阻止了對(duì)該dylib的訪問(wèn)砸脊。如果有任何調(diào)用imp_implementationWithBlock的操作(如AppKit開(kāi)始執(zhí)行的操作)具篇,那麼我們將在嘗試加載它時(shí)崩潰。將其加載到此處可在啟用沙箱配置文件之前對(duì)其進(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註冊(cè)

_dyld_objc_notify_register方法

這個(gè)方法的具體實(shí)現(xiàn)在上一篇dyld加載已經(jīng)有詳細(xì)說(shuō)明驱显,其源碼實(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)行時(shí)使用
  • 註冊(cè)處理程序侨舆,以便在映射秒紧,取消映射和初始化objc圖像時(shí)調(diào)用
  • dyld將會(huì)通過(guò)一個(gè)包含objc-image-info的鏡像文件的數(shù)組回調(diào)mapped函數(shù)

方法中的三個(gè)參數(shù)分別表示的含義如下:

  • map_image: dyld將image(鏡像文件)加載進(jìn)內(nèi)存,會(huì)觸發(fā)該函數(shù)
  • load_image: dyld初始化image會(huì)觸發(fā)該函數(shù)
  • unmap_image: dyld將將image移除時(shí)挨下,會(huì)觸發(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等價(jià)於map_images
  • init等價(jià)於load_images
  • unmapped等價(jià)於unmap_image

在dyld加載中熔恢,我們知道load_images 是在notifySingle 方法中,通過(guò)sNotifyObjcInit調(diào)用臭笆,如下所示

然後通過(guò)查找sNotifyObjCInit叙淌,最終找到了_dyld_objc_notify_register-> registerObjCNotifiers 在該方法中將_dyld_objc_notify_register 傳入的參數(shù)賦值給了三個(gè)回調(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)用時(shí)機(jī)

  • 關(guān)於load_images的調(diào)用時(shí)機(jī)已經(jīng)在dyld加載流程講解過(guò)愁铺,下面以map_images為例鹰霍,看看其調(diào)用時(shí)機(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中註冊(cè)回調(diào)函數(shù)瓶竭,可以理解為 添加觀察者

  • 在objc中dyld註冊(cè)督勺,可以理解為發(fā)送通知

  • 觸發(fā)回調(diào),可以理解為執(zhí)行通知selector

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末斤贰,一起剝皮案震驚了整個(gè)濱河市智哀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌荧恍,老刑警劉巖瓷叫,帶你破解...
    沈念sama閱讀 223,207評(píng)論 6 521
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡摹菠,警方通過(guò)查閱死者的電腦和手機(jī)盒卸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,455評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)辨嗽,“玉大人世落,你說(shuō)我怎么就攤上這事≡阈瑁” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 170,031評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵谷朝,是天一觀的道長(zhǎng)洲押。 經(jīng)常有香客問(wèn)我,道長(zhǎng)圆凰,這世上最難降的妖魔是什么杈帐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,334評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮专钉,結(jié)果婚禮上挑童,老公的妹妹穿的比我還像新娘。我一直安慰自己跃须,他們只是感情好站叼,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,322評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著菇民,像睡著了一般尽楔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上第练,一...
    開(kāi)封第一講書(shū)人閱讀 52,895評(píng)論 1 314
  • 那天阔馋,我揣著相機(jī)與錄音,去河邊找鬼娇掏。 笑死呕寝,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的婴梧。 我是一名探鬼主播下梢,決...
    沈念sama閱讀 41,300評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼志秃!你這毒婦竟也來(lái)了怔球?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,264評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤浮还,失蹤者是張志新(化名)和其女友劉穎竟坛,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,784評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡担汤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,870評(píng)論 3 343
  • 正文 我和宋清朗相戀三年涎跨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片崭歧。...
    茶點(diǎn)故事閱讀 40,989評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡隅很,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出率碾,到底是詐尸還是另有隱情叔营,我是刑警寧澤,帶...
    沈念sama閱讀 36,649評(píng)論 5 351
  • 正文 年R本政府宣布所宰,位于F島的核電站绒尊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏仔粥。R本人自食惡果不足惜婴谱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,331評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躯泰。 院中可真熱鬧谭羔,春花似錦、人聲如沸麦向。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,814評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)磕蛇。三九已至景描,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秀撇,已是汗流浹背超棺。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,940評(píng)論 1 275
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呵燕,地道東北人棠绘。 一個(gè)月前我還...
    沈念sama閱讀 49,452評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像再扭,于是被迫代替她去往敵國(guó)和親氧苍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,995評(píng)論 2 361