iOS底层探索之类的加载:read_images分析

Posted 卡卡西Sensei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS底层探索之类的加载:read_images分析相关的知识,希望对你有一定的参考价值。

1. 回顾

在前两篇博文中,已经对dyld动态链接器的底层源码进行了探索分析,但是dyld链接images镜像文件到内存的过程我们还不知道,接下来的几篇博文就着重去探索。

iOS底层探索之dyld(上):动态链接器流程分析

iOS底层探索之dyld(下):动态链接器流程源码分析

在这里插入图片描述

_objc_init方法向dyld中注册了回调函数,下面就补充一点内容,探究下_objc_init方法。

2. _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?
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();

#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();
    _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(),因此我们必须⾃⼰做
  • lock_init(): 没有重写,采⽤C++的特性
  • exception_init () 初始化libobjc的异常处理系统
  • cache_init(): 缓存条件初始化
  • runtime_init() : runtime运⾏时环境初始化,⾥⾯主要
    是:unattachedCategories,allocatedClasses 后⾯会分析
  • _imp_implementationWithBlock_init :启动回调机制。通常这不会做什么,因为所有的初始化都
    是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib

2.1 environ_init

environ_init()主要代码如下,在PrintHelp或者PrintOptions条件下,可以在控制台打印输出所有的环境变量。

 void  environ_init(void) 

{    
     // Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
    if(PrintHelp || PrintOptions) { 
         ...
    if  (PrintOptions) {
        _objc_inform("OBJC_PRINT_OPTIONS is set");
    }

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

那么现在把if(PrintHelp)if(PrintOptions && *opt->var)判断条件都去掉,看看控制台打印了哪些东西。

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

打印如果结果如下:

objc[2964]: OBJC_PRINT_IMAGES: log image and library names as they are loaded
objc[2964]: OBJC_PRINT_IMAGES is set
objc[2964]: OBJC_PRINT_IMAGE_TIMES: measure duration of image loading steps
objc[2964]: OBJC_PRINT_IMAGE_TIMES is set
objc[2964]: OBJC_PRINT_LOAD_METHODS: log calls to class and category +load methods
objc[2964]: OBJC_PRINT_LOAD_METHODS is set
objc[2964]: OBJC_PRINT_INITIALIZE_METHODS: log calls to class +initialize methods
objc[2964]: OBJC_PRINT_INITIALIZE_METHODS is set
。。。。。。省略。。。。。。
objc[2964]: OBJC_DISABLE_NONPOINTER_ISA: disable non-pointer isa fields
objc[2964]: OBJC_DISABLE_NONPOINTER_ISA is set
objc[2964]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY: disable safety checks for +initialize after fork
objc[2964]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY is set
objc[2964]: OBJC_DISABLE_FAULTS: disable os faults
objc[2964]: OBJC_DISABLE_FAULTS is set
objc[2964]: OBJC_DISABLE_PREOPTIMIZED_CACHES: disable preoptimized caches
objc[2964]: OBJC_DISABLE_PREOPTIMIZED_CACHES is set
objc[2964]: OBJC_DISABLE_AUTORELEASE_COALESCING: disable coalescing of autorelease pool pointers
objc[2964]: OBJC_DISABLE_AUTORELEASE_COALESCING is set
objc[2964]: OBJC_DISABLE_AUTORELEASE_COALESCING_LRU: disable coalescing of autorelease pool pointers using look back N strategy
objc[2964]: OBJC_DISABLE_AUTORELEASE_COALESCING_LRU is set

这么多环境变量,看着就晕,有没有我们熟悉的啊???

有比如:

  • OBJC_PRINT_IMAGES打印镜像文件
  • OBJC_DISABLE_NONPOINTER_ISA判断是否是优化的指针
  • OBJC_PRINT_LOAD_METHODS打印出程序中所有的load方法等等。

那么我们没有objc的源码该如何查看环境变量呢?
可以使用终端命令export OBJC_HELP=1打印如下

2.1.1 终端命令查看环境变量

终端命令查看环境变量

2.1.2 Xcode工程设置环境变量

Xcode中环境变量配置的位置:选中运行的target--> Edit scheme... --> Run --> Arguments --> Environment Variables
Xcode设置环境变量

2.1.2.1 OBJC_DISABLE_NONPOINTER_ISA

设置环境变量OBJC_DISABLE_NONPOINTER_ISA表示是否开启ISA指针优化。YES表示纯指针,NO表示优化后的指针就是nonpointer isa
iOS底层探索之对象的本质和类的关联特性initIsa(下)博客中介绍过了ISA

首先不设置环境变量看下nonpointer isa
查看isa

isa低位0号位是1,表示是优化后的isa,而且高位上也有其他的数据

将环境变量OBJC_DISABLE_NONPOINTER_ISA = YES设置为YES 再看看isa是怎么样的。

在这里插入图片描述

isa低位0号位是0,表示是isa是纯指针,而且高位除了cls也没有其他的数据了。

2.1.2.2 OBJC_PRINT_LOAD_METHODS

环境变量OBJC_PRINT_LOAD_METHODS打印出程序中所有的load方法,在自定义类中添加load方法,配置环境变量OBJC_PRINT_LOAD_METHODS = YES
OBJC_PRINT_LOAD_METHODS
+[JPStudent load]这个是我们自定义JPStudent类中的load方法,其它的都是系统级别的load方法。load方法太多会导致你应用程序启动变慢,或者有的人在load方法里面做一些隐藏的操作,那么就可以通过这个环境变量检查出哪个类里面有实现的load方法。

2.2 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
}

2.3 static_init

全局静态C++函数,在dyld调用我们的静态构造函数之前,libobjc会调用_objc_init,会先调用自己的C++构造函数,简单说的就是libobjc会调用自己的全局的C++函数,因为这个全局的构函数是个非常重要的函数,为了及时性,就自己先调用起来,从底层源码的注释也可以知道,先调用自己的全局静态C++函数再走dyld

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

2.4 runtime_init

runtime运行时环境初始化,里面主要是unattachedCategoriesallocatedClasses两张表的初始化。

void runtime_init(void)
{  
   objc::unattachedCategories.init(32);//分类表的初始化
   objc::allocatedClasses.init();//类表的初始化
}

2.5 exception_init

初始化libobjc库的异常处理,这个和objcdyld中注册回调差不多的意思,就是让你去处理异常的。

 void  exception_init(void)
 {
  old_terminate = std::set_terminate(&_objc_terminate);
 }

当你的代码出现crashcrash并不是代码错误,不一定会奔溃。crash是在上层的编写的代码出现一些不符合苹果系统底层的逻辑和规则时系统会发出异常的信号。通过出现异常会进去_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)();
        }
    }
}

_objc_terminate方法里面发现了(*uncaught_handler)((id)e)它会把异常抛出去,全局搜索uncaught_handler

objc_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}

uncaught_handler = fn告诉程序员可以自己传一个函数的句柄,fn可以是程序员自己定义的函数,然后回调时可以自己处理程序抛出的异常的信息。

2.6 cache_t::init

缓存条件的初始化

void cache_t::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
}

2.7 _imp_implementationWithBlock_init

这个是启动回调机制,通常不会做什么。因为所有的初始化都是惰性的,但是对于某些进程会迫不及待的加载trampolines dylib_imp_implementationWithBlock_init源码如下:

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.
    //
    // 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
}

2.8 _dyld_objc_notify_register

_dyld_objc_notify_registerdyld注册回调,这个方法非常重要!!!
_dyld_objc_notify_register

  • _dyld_objc_notify_register源码实现如下:
// _dyld_objc_notify_init
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init 
init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
// call 'mapped' function with all images mapped so far
try {
    notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}
// call 'init' function on all images already init'ed (below libSystem)
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it !=
sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() )  {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
     (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
    }
  }
}

其实在iOS底层探索之dyld(下):动态链接器流程源码分析 博客中也已经介绍了,那么我们再来回顾下。

  • &map_imagesdyldimage镜像文件加载到内存中会调用该函数。
  • load_imagesdyld初始化所有的image镜像文件文件会调用。
  • unmap_image:将image镜像文件移除时会调用。

load_images方法其实就是调用load方法,map_image方法,&map_images是指针传递,指向是同一块实现的地址,如果有什么变化就可以第一时间知道。在dyldsNotifyObjCMapped调用的地方是在notifyBatchPartial方法中,而notifyBatchPartial方法是在registerObjCNotifiers中调用,在objc初始化注册通知时就调用了,所以是先调用map_images后调用load_images这么个函数执行顺序。

那么接下就去看看map_images

3. map_images

3.1 map_images

进入map_images源码

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

}

3.2 map_images_nolock

  • 进入OBJC_PRINT_LOAD_METHODS源码

map_images_nolock

我们要找的无非就是镜像文件的加载和映射相关的,其他的代码就不用看了,直接折叠起来,去重点代码里面看看。

3.3 read_images

查看_read_images方法,发现代码太多了,无从下手,只能从全局来分析了,于是又把代码折叠起来。

void _read_images(header_info **hList, uint32_t hCount, int 
totalClasses, int 
unoptimizedTotalClasses)
{
   ... //表示省略部分代码
#define EACH_HEADER \\
    hIndex = 0;         \\
    hIndex < hCount && (hi = hList[hIndex]); \\
    hIndex++
    // 条件控制进行一次的加载
    if (!doneOnce) { ... }
    // 修复预编译阶段的`@selector`的混乱的问题
    // 就是不同类中有相同的方法 但是相同的方法地址是不一样的
    // Fix up @selector references
    static size_t UnfixedSelectors;
    { ... }
    ts.log("IMAGE TIMES: fix up selector references");
    
    // 错误混乱的类处理
    // Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
    for (EACH_HEADER) { ... }
    ts.log("IMAGE TIMES: discover classes");
    
    // 修复重映射一些没有被镜像文件加载进来的类
    // Fix up remapped classes
    // Class list and nonlazy class list remain unremapped.
    // Class refs and super refs are remapped for message dispatching.
    if (!noClassesRemapped()) { ... }
    ts.log("IMAGE TIMES: remap classes");

#if SUPPORT_FIXUP
    // 修复一些消息
    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) { ... }
    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");

#endif
    // 当类中有协议时:`readProtocol`
    // Discover protocols. Fix up protocol refs.
    for (EACH_HEADER) { ... }
    ts.log("IMAGE TIMES: discover protocols");
    
    // 修复没有被加载的协议
    // Fix up @protocol references
    // Preoptimized images may have the right 
    // answer already but we don't know for sure.
    for (EACH_HEADER) { ... }
    ts.log("IMAGE TIMES: fix up @protocol references");
    
    // 分类的处理
    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes.  
    if (didInitialAttachCategories) { ... }
    ts.log("IMAGE TIMES: discover categories");
    
    // 类的加载处理
    // Category discovery MUST BE Late to avoid potential races
    // when other threads call the new category code befor
    // this thread finishes its fixups.
    // +load handled by prepare_load_methods()
    // Realize non-lazy classes (for +load methods and static instances)
    for (EACH_HEADER) { ... }
    ts.log("IMAGE TIMES: realize non-lazy classes");
    
    // 没有被处理的类,优化那些被侵犯的类
    // Realize newly-resolved future classes, in case CF manipulates them
    if (resolvedFutureClasses) { ... }
    ts.log("IMAGE TIMES: realize future classes");
   ...
#undef EACH_HEADER

}

从大体上可以看出是对一些log日志的打印输出,主要如下:

  • 条件控制进行一次加载
  • 修复预编译阶段的@selector的混乱的问题
  • 错误混乱的类处理
  • 修复重映射一些没有被镜像文件加载进来的类
  • 修复一些消息
  • 当类中有协议时:readProtocol
  • 修复没有被加载的协议
  • 分类的处理
  • 类的加载处理
  • 没有被处理的类,优化那些被侵犯的类

下面就重点分析几个比较主要的;

3.3.1 doneOnce

  • doneOnce
 if(!doneOnce) {
        doneOnce = YES; // 加载一次后,就不会在进判断 doneOnce = YES
        launchTime = YES;
         ...
        iOS开发底层之类加载(中) - 13

iOS开发底层之类加载(中) - 13

iOS底层探索之类的加载: attachCategories分析

iOS底层探索之类的加载:类的关联对象AssociatedObject

iOS底层探索之类的结构—cache分析(上)

iOS底层探索之类的结构(下)