[iOS] dyld和 objc 的關(guān)聯(lián)

1._objc_init源碼分析

首先,我們直接取objc 源碼中找_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)境變量,如果需要,還可以打開環(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)行時(shí)環(huán)境初始化,里面主要是unattachedCategories、allocatedClasses -- 分類初始化
    runtime_init();
    // 初始化libobjc的異常處理系統(tǒng)
    exception_init();
    // 緩存條件初始化
    cache_init();
    // 啟動回調(diào)機(jī)制,通常這不會做什么,因?yàn)樗械某跏蓟际嵌栊缘?,但是對于某些進(jìn)程,我們會迫不及待地加載trampolines dylib
    _imp_implementationWithBlock_init();
    /*
        _dyld_objc_notify_register -- dyld 注冊的地方
        - 僅供objc運(yùn)行時(shí)使用
        - 注冊處理程序,以便在映射、取消映射 和初始化objc鏡像文件時(shí)使用,dyld將使用包含objc_image_info的鏡像文件數(shù)組,回調(diào) mapped 函數(shù)
        map_images:  dyld將image鏡像文件加載進(jìn)內(nèi)存時(shí),會觸發(fā)該函數(shù)
        load_images:dyld初始化image會觸發(fā)該函數(shù)
        unmap_image:dyld將image移除時(shí)會觸發(fā)該函數(shù)
        */
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}
1.1 environ_init:環(huán)境變量初始化

environ_init方法的源碼如下,其中的關(guān)鍵代碼是 for 循環(huán):

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);
        }
    }
}

有以下兩種方式可以打印所有的環(huán)境變量:
-for循環(huán)之前的判斷條件去掉

image.jpeg

image.png
  • 通過export OBJC_HELP = 1,打印環(huán)境變量

這些環(huán)境變量,均可以通過target -- Edit Scheme -- Run --Arguments -- Environment Variables配置,其中常用的環(huán)境變量主要有以下幾個:

  • DYLD_PRINT_STATISTICS:設(shè)置DYLD_PRINT_STATISTICSYES,控制臺就會打印App 的加載時(shí)長,包括整體加載時(shí)長和動態(tài)庫加載時(shí)長,即main函數(shù)之前的啟動時(shí)間(查看pre-main耗時(shí)),可以通過設(shè)置了解其耗時(shí)部分,并對其進(jìn)行啟動優(yōu)化。

  • OBJC_DISABLE_NONPOINTER_ISA:杜絕生成相應(yīng)的 nonpointer isa (nonpointer isa指針地址末尾為 1),生成的都是普通的 isa

  • OBJC_PRINT_LOAD_METHODS:打印 ClassCategory+(void)load方法的調(diào)用信息

-NSDoubleLocalizedStrings:項(xiàng)目做國際化本地化(Localized)的時(shí)候是一個挺耗時(shí)的工作,想要檢測國際化翻譯好的語言文字UI會變成什么樣子,可以指定這個啟動項(xiàng)??梢栽O(shè)置 NSDoubleLocalizedStringsYES。

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

1.1.1 環(huán)境變量 - OBJC_DISABLE_NONPOINTER_ISA

OBJC_DISABLE_NONPOINTER_ISA為例,將其設(shè)置為YES,如下所示:

image.jpeg

  • 未設(shè)置OBJC_DISABLE_NONPOINTER_ISA之前,isa地址的二進(jìn)制打印,末尾為 1:

    image.jpeg

  • 設(shè)置OBJC_DISABLE_NONPOINTER_ISA環(huán)境變量之后,末尾變成了 0:

    image.jpeg

所以OBJC_DISABLE_NONPOINTER_ISA可以控制isa優(yōu)化開關(guān),從而優(yōu)化整個內(nèi)存結(jié)構(gòu)。

1.1.2 環(huán)境變量 - OBJC_PRINT_LOAD_METHODS
  • 配置環(huán)境變量OBJC_PRINT_LOAD_METHODS,設(shè)置為YES
  • LGPerson類中重寫+load函數(shù),運(yùn)行之后,打印如下:
    image.jpeg

    所以,OBJC_PRINT_LOAD_METHODS可以監(jiān)控所有的+load方法,從而處理啟動優(yōu)化。
1.2tls_init: 線程 key 的綁定

主要是本地線程池的初始化以及析構(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
}
1.3 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 void static_init()
{
    size_t count;
    auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        inits[i]();
    }
}
1.4runtime_init:運(yùn)行時(shí)環(huán)境初始化

這個主要是運(yùn)行時(shí)的初始化,主要分為兩部分:分類初始化、類的表初始化

void runtime_init(void)
{
    objc::unattachedCategories.init(32);
    objc::allocatedClasses.init(); //初始化 -- 開辟的類的表
}
1.5exception_init:初始化libobjc 的異常處理系統(tǒng)

主要是初始化libobjc的異常處理系統(tǒng),注冊異常處理的回調(diào),從而監(jiān)控異常的處理,源碼如下:

void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}
  • 當(dāng)有crash發(fā)生時(shí),會來到_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_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
//    fn為設(shè)置的異常句柄 傳入的函數(shù),為外界給的
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn; //賦值
    return result;
}

crash分類
crash 的主要原因是收到了未處理的信號,主要來源于三個地方:

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

所以相對應(yīng)的,crash 也分為了3 種:

  • Mach 異常:是指最底層的內(nèi)核級異常。用戶態(tài)的開發(fā)者可以直接通過 Mach API設(shè)置thread,task,host 的異常端口,來捕獲 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ù),如下圖:

image.png

上述方式只適合收集應(yīng)用級異常,我們要做的就是用自定義的函數(shù)替代該ExceptionHandler即可。

1.6cache_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
}
1.7_imp_implementationWithBlock_init : 啟動回調(diào)機(jī)制

該方法主要是啟動回調(diào)機(jī)制,通常這不會做什么,因?yàn)樗械某跏蓟际嵌栊缘?,但是對于某些進(jìn)程,我們會迫不及待地加載libobjc-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í)行的操作),那么我們將在嘗試加載它時(shí)崩潰。將其加載到此處可在啟用沙箱配置文件之前對其進(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
}
1.8_dyld_objc_notify_register: dyld注冊

這個方法的源碼實(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í)使用
  • 注冊處理程序,以便于在鏡像映射、取消鏡像映射和初始化objc 時(shí)調(diào)用
  • dyld 將會通過一個包含 objc-image-info的鏡像文件的數(shù)組回調(diào) mapped 函數(shù)

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

  • map_images:
    dyldimage(鏡像文件)加載進(jìn)內(nèi)存時(shí),會觸發(fā)該函數(shù)
  • load_image:
    dyld初始化 image 會觸發(fā)該函數(shù)
    -upmap_image:
    dyldimage移除時(shí),會觸發(fā)該函數(shù)

2. dyldobjc 的關(guān)聯(lián)

其方法的源碼實(shí)現(xiàn)和調(diào)用如下,即dyldObjc的關(guān)聯(lián)可以通過源碼提現(xiàn):

===> libobjc源碼中--調(diào)用
_dyld_objc_notify_register(&map_images, load_images, unmap_image);

??

===> 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);
}

從上可以得出:

  • mapped 等價(jià)于map_images
    -init等價(jià)于 load_images
  • unmapped 等價(jià)于 unmap_image

在之前 dyld 加載流程中,我們知道了load_images是在 notifySingle 方法中,通過 sNotifyObjCInit調(diào)用的,如下所示:

image.jpeg

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

image.png

所以有下面等價(jià)關(guān)系:

  • sNotifyObjCMapped == mapped == map_images
  • sNotifyObjCInit == init == load_images
  • sNotifyObjCUnmapped == unmapped == unmap_image
    image.jpeg

關(guān)于load_images的調(diào)用時(shí)機(jī)已經(jīng)在 dyld加載過程中講過了,下面以map_images為例,看看其調(diào)用時(shí)機(jī)

  • dyld中全局搜索 sNotifyObjcMapped :registerObjCNotifiers -- notifyBatchPartial -- sNotifyObjCMapped

    image.png

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

    image.jpeg

所以有以下結(jié)論:map_images是先于load_images調(diào)用,即先map_images ,再load_images

3. 總結(jié)

結(jié)合 dyld 加載流程,dyldObjc 的關(guān)聯(lián)如下圖所示:

image.png

4. 環(huán)境變量匯總

環(huán)境變量名 說明
OBJC_PRINT_OPTIONS 輸出OBJC已設(shè)置的選項(xiàng)
OBJC_PRINT_IMAGES 輸出已load的image信息
OBJC_PRINT_LOAD_METHODS 打印 Class 及 Category 的 + (void)load 方法的調(diào)用信息
OBJC_PRINT_INITIALIZE_METHODS 打印 Class 的 + (void)initialize 的調(diào)用信息
OBJC_PRINT_RESOLVED_METHODS 打印通過 +resolveClassMethod: 或 +resolveInstanceMethod: 生成的類方法
OBJC_PRINT_CLASS_SETUP 打印 Class 及 Category 的設(shè)置過程
OBJC_PRINT_PROTOCOL_SETUP 打印 Protocol 的設(shè)置過程
OBJC_PRINT_IVAR_SETUP 打印 Ivar 的設(shè)置過程
OBJC_PRINT_VTABLE_SETUP 打印 vtable 的設(shè)置過程
OBJC_PRINT_VTABLE_IMAGES 打印 vtable 被覆蓋的方法
OBJC_PRINT_CACHE_SETUP 打印方法緩存的設(shè)置過程
OBJC_PRINT_FUTURE_CLASSES 打印從 CFType 無縫轉(zhuǎn)換到 NSObject 將要使用的類(如 CFArrayRef 到 NSArray * )
OBJC_PRINT_GC 打印一些垃圾回收操作
OBJC_PRINT_PREOPTIMIZATION 打印 dyld 共享緩存優(yōu)化前的問候語
OBJC_PRINT_CXX_CTORS 打印類實(shí)例中的 C++ 對象的構(gòu)造與析構(gòu)調(diào)用
OBJC_PRINT_EXCEPTIONS 打印異常處理
OBJC_PRINT_EXCEPTION_THROW 打印所有異常拋出時(shí)的 Backtrace
OBJC_PRINT_ALT_HANDLERS 打印 alt 操作異常處理
OBJC_PRINT_REPLACED_METHODS 打印被 Category 替換的方法
OBJC_PRINT_DEPRECATION_WARNINGS 打印所有過時(shí)的方法調(diào)用
OBJC_PRINT_POOL_HIGHWATER 打印 autoreleasepool 高水位警告
OBJC_PRINT_CUSTOM_RR 打印含有未優(yōu)化的自定義 retain/release 方法的類
OBJC_PRINT_CUSTOM_AWZ 打印含有未優(yōu)化的自定義 allocWithZone 方法的類
OBJC_PRINT_RAW_ISA 打印需要訪問原始 isa 指針的類
OBJC_DEBUG_UNLOAD 卸載有不良行為的 Bundle 時(shí)打印警告
OBJC_DEBUG_FRAGILE_SUPERCLASSES 當(dāng)子類可能被對父類的修改破壞時(shí)打印警告
OBJC_DEBUG_FINALIZERS 警告實(shí)現(xiàn)了 -dealloc 卻沒有實(shí)現(xiàn) -finalize 的類
OBJC_DEBUG_NIL_SYNC 警告 @synchronized(nil) 調(diào)用,這種情況不會加鎖
OBJC_DEBUG_NONFRAGILE_IVARS 打印突發(fā)地重新布置 non-fragile ivars 的行為
OBJC_DEBUG_ALT_HANDLERS 記錄更多的 alt 操作錯誤信息
OBJC_DEBUG_MISSING_POOLS 警告沒有 pool 的情況下使用 autorelease,可能內(nèi)存泄漏
OBJC_DEBUG_DUPLICATE_CLASSES 當(dāng)出現(xiàn)類重名時(shí)停機(jī)
OBJC_USE_INTERNAL_ZONE 在一個專用的 malloc 區(qū)分配運(yùn)行時(shí)數(shù)據(jù)
OBJC_DISABLE_GC 強(qiáng)行關(guān)閉自動垃圾回收,即使可執(zhí)行文件需要垃圾回收
OBJC_DISABLE_VTABLES 關(guān)閉 vtable 分發(fā)
OBJC_DISABLE_PREOPTIMIZATION 關(guān)閉 dyld 共享緩存優(yōu)化前的問候語
OBJC_DISABLE_TAGGED_POINTERS 關(guān)閉 NSNumber 等的 tagged pointer 優(yōu)化
OBJC_DISABLE_NONPOINTER_ISA 關(guān)閉 non-pointer isa 字段的訪問
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容