011-iOS底层原理-_objc_init

引言

上一篇我们探索到了dyld的加载流程。了解dyldobjc通信的_objc_init函数。我们了解了load_images赋值给dyld内部的sNotifyObjCInit,并回调执行所有+load方法。_objc_init中的_dyld_objc_notify_register函数仍有两个参数未讲到,本文将探究dyldobjc关联的_objc_init函数内部流程。

_objc_init

_objc_init函数,是引导初始化程序,使用dyld注册我们的image镜像通知者,并由libSystem在库初始化调用。在_objc_init函数内打个断点,观察左边的调用栈,可知道_objc_init的调起过程。

_objc_init调起过程

简而言之:dyldlibSystemlibdispatchlibobjc。交汇点就是_objc_init函数。该函数内部如下(已在对应函数添加了简单说明):

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    1、读取影响运行时的环境变量。如果需要,还可以打印环境变量帮助
    environ_init();
    2、关于线程key的绑定 - 比如每线程数据的析构函数
    tls_init();
    3、运行C ++静态构造函数。在dyld调用我们的静态构造函数之前,`libc` 会调用 _objc_init(), 因此我们必须自己做
    static_init();
    4、runtime运行时环境初始化,里面主要是:unattachedCategories,allocatedClasses
    runtime_init();
    5、初始化libobjc的异常处理系统
    exception_init();
#if __OBJC2__
    6、缓存条件初始化
    cache_t::init();
#endif
   7、启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib
    _imp_implementationWithBlock_init();
    8、注册在映射、取消映射和初始化 objc 的镜像时要调用的处理程序。Dyld 将使用包含 objc-image-info 部分的images镜像数组回调“映射”函数。那些 dylib 的镜像将自动增加 ref-counts,因此 objc 将不再需要对它们调用 dlopen() 以防止它们被卸载。在调用 _dyld_objc_notify_register() 期间,dyld 将使用已加载的 objc image镜像调用“映射”函数。在以后的任何 dlopen() 调用中,dyld 还将调用“映射”函数。当 dyld 在该image镜像中被称为初始值设定项时,Dyld 将调用“init”函数。这是当 objc 调用该image镜像中的任何 +load 方法时。
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

【 environ_init 】

environ_init内部会读取影响运行时的环境变量。如果需要,还可以打印环境变量帮助。可在environ_init内部看到如下代码:

environ_init源码

environ_init内部的核心,就是这个for循环所能打印的东西。
environ_init调试过程如下:
去掉if (PrintHelp || PrintOptions)条件,即可打印所有环境变量:

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

运行结果如下:

打印所有环境变量

环境变量Xcode对应的位置为:Edit Scheme → Run → Arguments → Environment
edit scheme
添加环境变量

environ_init测试1:OBJC_PRINT_LOAD_METHODS:打印+load方法的位置。

按照上图方法,添加OBJC_PRINT_LOAD_METHODS,并勾选。在QLPerson.m中添加+load方法。运行程序。

OBJC_PRINT_LOAD_METHODS测试
打印结果

environ_init测试2:OBJC_DISABLE_NONPOINTER_ISA:关闭nonpointer_isa

1)启用nonpointer_isamain.m中创建person对象,打个断点并运行:

启动`nonpointer_isa`

2)禁用nonpointer_isa
禁用nonpointer_isa

运行结果:
image.png

3)所有环境变量表:

        环境变量名                           说明
        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 字段的访问
来源:style_月月

【 tls_init 】

tls_init是关于线程key的绑定 - 比如每线程数据的析构函数
源码如下:

void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

【 static_init 】

这里会运行 C++ 的静态构造函数,在 dyld 调用我们的静态构造函数之前,libc会调用 _objc_init,所以这里我们必须自己来处理,并且这里只会初始化系统内置的 C++ 静态构造函数,我们自己代码里面写的并不会在这里初始化。
换句话说:
static_init主要是运行系统级别的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]();
    }
    auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        UnsignedInitializer init(offsets[i]);
        init();
    }
}

【 runtime_init 】

运行时的初始化,主要分为两个操作:
1、开辟存储分类的表:objc::unattachedCategories.init(32);
2、开辟存储类的表:objc::allocatedClasses.init();

void runtime_init(void)
{
    objc::unattachedCategories.init(32);
    objc::allocatedClasses.init();
}

【 exception_init 】

主要是初始化libobjc的异常处理系统。由map_image()函数调起。源码如下:

/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}

1、old_terminate的定义是:static void (*old_terminate)(void) = nil;
2、_objc_terminate是一个函数名,其对应实现如下:

/***********************************************************************
* _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)();
        }
    }
}

说明:
a、未捕获的异常回调作为 std::terminate 处理程序实现。
b、检查是否有处于活动状态的异常
c、如果是,请检查它是否是 Objective-C 异常
d、如果是这样,回调到我们已经注册好的回调函数。
e、最后,调用前一个终止处理程序。

当异常发生时,回来到static void _objc_terminate(void)函数,最终由(*uncaught_handler)((id)e);抛出异常。uncaught_handler的探索与dyld中的sNotifyObjCInit类似。由他人传进来
1、全局搜索uncaught_handler ,找到uncaught_handler = fn这个赋值位置。则我们继续找出上层调用即可

uncaught_handler赋值

2、我们平时抓取异常用到的NSSetUncaughtExceptionHandler,此处的void QLExceptionHandlers函数,赋值给fnfn赋值给uncaught_handler

+ (void)configExceptionHandler{
    // uncaught_handler() = fn = QLExceptionHandlers
//  objc_setUncaughtExceptionHandler()
    NSSetUncaughtExceptionHandler(&QLExceptionHandlers);
}
/// Exception
void QLExceptionHandlers(NSException *exception) {
    NSLog(@"%s",__func__);
}

【 cache_t::init() 】

cache_t::init()主要是:缓存条件初始化

【 _imp_implementationWithBlock_init】

启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib

void _imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
在某些进程中急切地加载 libobjc-trampolines.dylib。
一些程序(最显着的是旧版本的嵌入式 Chromium 使用的 QtWebEngineProcess)启用了一个高度限制的沙箱配置文件,该配置文件会阻止访问该 dylib。
如果有任何东西调用了 imp_implementationWithBlock(就像 AppKit 开始做的那样),那么我们将在尝试加载它时崩溃。在启用沙箱配置文件并阻止它之前,在此处加载它会对其进行设置。
    if (__progname &&
        (strcmp(__progname, "QtWebEngineProcess") == 0 ||
         strcmp(__progname, "Steam Helper") == 0)) {
        Trampolines.Initialize();
    }
#endif
}

【 _dyld_objc_notify_register】

注册在映射、取消映射和初始化 dyld 的镜像时要调用的处理程序。dyld 将使用包含 objc-image-info 部分的images镜像数组回调“映射”函数。那些 dylib 的镜像将自动增加 ref-counts,因此 objc 将不再需要对它们调用 dlopen() 以防止它们被卸载。在调用 _dyld_objc_notify_register() 期间,dyld 将使用已加载的 objc image镜像调用“映射”函数。在以后的任何 dlopen()调用中,dyld 还将调用“映射”函数。当 dyld 在该image镜像中被称为初始值设定项时,dyld 将调用init函数。这是当 objc 调用该image镜像中的任何 +load 方法时。

load_images

(当dyld把镜像文件初始化时,会调用此函数)
上篇文章,我们探索到dyld通过_dyld_objc_notify_register注册load_images函数,赋值给init,然后在递归初始化中调用notifySingle时回调(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());执行load_images函数,加载所有+load方法。(sNotifyObjCInit = init = load_images)

load_images赋值给sNotifyObjCInit

map_images

(当dyld把镜像文件加载入内存时,会调用此函数)
_dyld_objc_notify_register的第一个参数&map_images传的是map_images函数的指针,原因如下:
因为这个map_images很重要,必须要保证dyld内部加载镜像与map_image的实现保持同步发生变化,内容一致map_images是映射所有镜像文件,是一个比较耗时的过程,如果两者之间没有发生同步变化,会发生错乱现象,导致整个程序发生错乱。而load_images的流程只是为了回调objc加载+load方法,无需传函数指针。

1、流程(dyld):与load_images一样,dyld通过_dyld_objc_notify_register注册&map_images指针,赋值给sNotifyObjCMapped,并立即调用notifyBatchPartial函数,执行(*sNotifyObjCMapped)(objcImageCount, paths, mhs);回调。同时调用notifyMonitoringDyld,并在notifyMonitoringDyld内部递归监控dyld

sNotifyObjCMapped

notifyMonitoringDyld源码如下(递归):

static void notifyMonitoringDyld(bool unloading, unsigned imageCount, const struct mach_header* loadAddresses[], const char* imagePaths[])
{
    dyld3::ScopedTimer(DBG_DYLD_REMOTE_IMAGE_NOTIFIER, 0, 0, 0);
    RemoteNotificationResponder responder;
    if (!responder.active()) { return; }
    notifyMonitoringDyld(responder, unloading, imageCount, loadAddresses, imagePaths);
}

2、map_images实现(objc):进入源码可见

处理由被dyld 映射的给定的image镜像。
void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}
map_images_nolock

map_images_nolock内部为执行所有类注册和修复(或延迟发现丢失的超类等),并调用 +load 方法。

unmap_image

(当dyld把镜像文件删除时,会调用此函数)
处理即将被 dyld 取消映射的给定的镜像文件。与map_images和load_images流程类似,最终dyldremoveImage函数中,回调(*sNotifyObjCUnmapped)(image->getRealPath(), image->machHeader());其内部如下:

处理即将被 dyld 取消映射的给定的镜像文件。
void 
unmap_image(const char *path __unused, const struct mach_header *mh)
{
    recursive_mutex_locker_t lock(loadMethodLock);
    mutex_locker_t lock2(runtimeLock);
    unmap_image_nolock(mh);
}

objc中实现删除镜像文件函数unmap_image_nolock

unmap_image_nolock

总结:

dyldobjc暴露了API的函数:_dyld_objc_notify_register,这个函数接收来自objc方的实现,通过map_imagesload_imagesunmap_image,这三个参数来实现dyldobjc的通信。流程图如下所示。

dyld加载流程+objc连接.png

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

推荐阅读更多精彩内容