dyld與objc的關聯

前言

  • 在上一篇中我們了解了dyld加載的流程,此篇我們將介紹dyld與objc的關聯。

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?
    //讀取運行時的環境變量,可以透過打開環境變量幫助 export OBJC_HELP = 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_image: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方法:環境變量初始化

  • environ_init方法的部分源碼如下
  • 省略部分邏輯,顯示關鍵部分
/***********************************************************************
* 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);
        }
}

打印環境變量

  • 我們可以透過以下兩種方式達到打印環境變量的效果
  • 第一種:將打印變量的for循環單獨拿出,如下圖所示
  • 第二種:透過終端機修改環境變量

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

示範一:OBJC_DISABLE_NONPOINTER_ISA

  • 透過修改環境變量OBJC_DISABLE_NONPOINTER_ISA 設置為YES,如下圖所示
  • 在未設置時OBJC_DISABLE_NONPOINTER_ISA 前,isa地址的二進制打印末尾為1,如下圖
  • 在設置了OBJC_DISABLE_NONPOINTER_ISA 後,isa地址的二進制打印末尾為0,如下圖
  • 由此我們可以看到,我們可以透過修改環境變量OBJC_DISABLE_NONPOINTER_ISA來改變isa的第一位,即isa優化開關,進而優化整個內存結構。

示範二:OBJC_PRINT_LOAD_METHODS

  • 在LGPerson類中重寫+load方法。
  • 透過修改環境變量OBJC_PRINT_LOAD_METHODS 設置為YES,可以監控所有+load方法。
  • 所以我們可以透過OBJC_PRINT_LOAD_METHODS 監控load方法,從而處理啟動優化

tls_init方法:線程key的綁定

tls_init方法主要是對於本地線程的初始化以及析構,源碼如下

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
}

static_init:運行系統級別的C++靜態構造函數

主要是運行行系統級別的C++靜態構造函數,在dyld調用我們的靜態構造函數之前,libc調用_objc_init方法,即系統級別的C++構造函數 比 自定義的C++構造函數 先運行

/***********************************************************************
* 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:運行時環境初始化

主要是運行時的環境初始化,主要分為兩個部分,分類初始化,類的表初始化

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

exception_init方法:初始化libobjc的異常處理系統

主要是初始化libobjc的異常處理系統,註冊異常處理的回調,從而監控、監控異常的處理,源碼如下

/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
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_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 內核
  • 其他進行
  • App本身

所以相對應的crash也分為三種

  • Mach異常:是指最底層的內核級異常,開發者可以直接透過Mach API設置thread,task,host的異常端口,來補獲Mach異常
  • Unix信號:又稱BSD信號,如果開發者沒有捕獲Mach異常,則會被host層的方法ux_exception()將異常轉換為對應的UNIX信號,並通過方法threadsignal()將信號投遞到出錯的線程.可以通過方法signal(x, SignalHandler) 來捕獲single
  • NSException 應用級異常:它是未被捕獲的Objective-C異常,導致程序像自身發送了SIGABRT信號而崩潰,對於未捕獲的Objective-C異常,可以使用try catch來捕獲的,或者通過NSSetUncaughtExceptionHandler()機制捕獲。

針對應用級異常,可以通過註冊異常捕獲的函數,即NSSetUncaughtExceptionHandler 機制,實現線程保活,收集上傳崩潰日誌。

應用級crash攔截

所以在開發中,會針對crash進行攔截處理,即app代碼中的一個異常句柄NSSetUncaughtExceptionHandler,傳入一個函數給系統,當異常發生後,調用函數(函數中可以線程保活,收集並上傳崩潰日誌),然後回到原有的app層中,其本質就是一個回調函數,如下圖所示

  • 上述方式只適合收集應用級異常,我們要做的就是用自定義的函數替代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++;
    }
    //為當前任務註冊一組可重新啟動的緩存
    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 啟動回調機制

該方法主要是啟動回調機制,通常這不會做什麼,因為所有的初始化都是惰性的,但是對於某些進程,我們會迫不及待的加載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.
    // 在某些進程中渴望加載libobjc-trampolines.dylib。一些程序(最著名的是嵌入式Chromium的較早版本使用的QtWebEngineProcess)啟用了嚴格限制的沙箱配置文件,從而阻止了對該dylib的訪問。如果有任何調用imp_implementationWithBlock的操作(如AppKit開始執行的操作),那麼我們將在嘗試加載它時崩潰。將其加載到此處可在啟用沙箱配置文件之前對其進行設置並阻止它。
    // 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方法

這個方法的具體實現在上一篇dyld加載已經有詳細說明,其源碼實現是在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運行時使用
  • 註冊處理程序,以便在映射,取消映射和初始化objc圖像時調用
  • dyld將會通過一個包含objc-image-info的鏡像文件的數組回調mapped函數

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

  • map_image: dyld將image(鏡像文件)加載進內存,會觸發該函數
  • load_image: dyld初始化image會觸發該函數
  • unmap_image: dyld將將image移除時,會觸發該函數

dyld與objc關聯

//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調用,如下所示

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

依照調用關係我們發現

  • 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 方法中調用

所以有以下結論map_images是先於load_images調用,即先map_images ,再load_images

  • 結合上一篇dyld加載流程,dyld與Objc的關聯如下圖所示
  • 在dyld中註冊回調函數,可以理解為 添加觀察者

  • 在objc中dyld註冊,可以理解為發送通知

  • 觸發回調,可以理解為執行通知selector

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335