012-类的加载(上)

前言

上一个篇章, 从dyldlibSystemlibDispatchlibObjc调用起了_objc_init然后调用到了一个回调函数. 然后在回调函数对dyld需要的参数进行赋值, 通知, dyld进行加载.

那么今天就来看一看_objc_init到底做了什么

_objc_init

_objc_init一般看比较标准的源码, 或者三方库, 优先看注释:

01.png

  • 引导程序初始化, 使dyld注册我们的镜像通知
  • libSystem BEFORE library初始化时间调用
    上面两句话说明白了我们之前的流程, 由dyld注册通知, 也就是这里是通知发起的地方, dyld在注册的地方进行调用. 在libSystem初始化的时候就会调用我们的_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?
//修复延迟初始化, 直到找到使用objc的镜像
    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

环境变量初始化

void environ_init(void)  {
    if (PrintHelp  ||  PrintOptions) {
        if (PrintHelp) {
            _objc_inform("Objective-C runtime debugging. Set variable=YES to enable.");
            _objc_inform("OBJC_HELP: describe available environment variables");
            if (PrintOptions) {
                _objc_inform("OBJC_HELP is set");
            }
            _objc_inform("OBJC_PRINT_OPTIONS: list which options are set");
        }
        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);
        }
    }
}

可以看出是一些环境变量的打印:

  • 可以直接赋值打印代码, 去除条件控制, 在函数内打印.(有可编译源码)
  • 终端直接export OBJC_HELP=1
  • objc-env.h可以直接查看
    当做工具使用就行, 不需要去记忆. OBJC_PRINT_LOAD_METHODS设置为YES, 可以打印出项目中所有的load方法调用, 还是挺实用的.

tls_init

初始化线程, key和线程绑定, 方便调度使用.

static_init

静态函数的调用, libc自己的静态函数, 调用以及符号的更换个绑定. 到dyld调用的时候, 就是已经替换的符号了

runtime_init

runtime的初始化
初始化两张表, 后续了解

void runtime_init(void)
{
//分类表
    objc::unattachedCategories.init(32);
//已经开辟内存的对象所在的表
    objc::allocatedClasses.init();
}

exception_init

libc的异常处理系统初始化

/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
初始化异常处理系统, 被map_images()调用. 也就是说这里并不被调用, 只是赋值
**********************************************************************/
void exception_init(void)
{
    old_terminate = std::set_terminate(&_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 //如果是, 请检查是否是OC异常
* 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)();
        }
    }
}

上面注释里面, 第三步注释说明了, 如果是OC异常, 使用OC对象注册一个异常回调来处理这个事情. (*uncaught_handler)((id)e);重点关注下个OC异常的代码就可以.

cache_t::init

缓存条件初始化。

_imp_implementationWithBlock_init

初始化Trampolines, 通常不需要做什么, 初始化是懒加载的. 但是对于某些进程我们会急于去加载trampolines库.
根据宏看是macos才加载的, 不是重点, 无需关注.

_dyld_objc_notify_register

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

这里回调函数是重点, 之前dyld也说了真实的调用在dyld之中, 这里做了一个赋值操作. 在这里:

  • map_images:管理了文件和库中的所有符号. 传入的是地址指针, 保证这部分信息的同步性, 防止信息错乱而发生错误.
  • load_images: 加载load方法, 传入的是值, 调用可以直接实现.
  • unmap_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);
}

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
  if (firstTime) {//如果是第一次架子啊
        sel_init(selrefCount); //C++析构函数
        //1.autoreleasePage的init调用 
        //2.初始化一个SideTablesMap的表(内部含有weak表, 引用计数表等等) 
        //3.初始化关联对象的表
        arr_init();
  }

    if (hCount > 0) {
//核心重点
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
}

接下来我继续看_read_images
先看整体流程, 下面在分开看.
整体分析:

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{   
   static bool doneOnce;
    // 1.条件控制,进行一次的加载。将所有类放入一个表中
    if (!doneOnce) { 
        doneOnce = YES;
.....
 }
    
    // Fix up @selector references
    // 2.修复预编译阶段的 `@selector` 的混乱问题
    // macho加载进来的地址要以dyld链接的内存地址为准
    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();
    
    // 3.修复错误的类
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: discover classes");
    
    // 4.修复重映射⼀些没有被镜像⽂件加载进来的 类
    if (!noClassesRemapped()) { ... }
    
    ts.log("IMAGE TIMES: remap classes");
    
#if SUPPORT_FIXUP
    // 5.修复一些消息
    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
    
    bool cacheSupportsProtocolRoots = sharedCacheSupportsProtocolRoots();
    
    // Discover protocols. Fix up protocol refs.
    // 6.发现协议
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: discover protocols");
    
    // 7.修复没有被加载的协议
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: fix up @protocol references");
    
    // 8.分类处理
    if (didInitialAttachCategories) { ... }
    
    ts.log("IMAGE TIMES: discover categories");
    
    // 9.懒加载类的处理 类实现
    // Realize non-lazy classes (for +load methods and static instances)
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: realize non-lazy classes");
    
    // 10.实现新解析的未来类,以防 CF框架操作它们
    // Realize newly-resolved future classes, in case CF manipulates them
    if (resolvedFutureClasses) { ... }
    
    ts.log("IMAGE TIMES: realize future classes");
    
    if (DebugNonFragileIvars) {
        realizeAllClasses();
    }
}

1. doneOnce第一次加载
//全局只会走一次根据静态变量doneOnce控制
int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);

创建一张类表, 包含所有的类. 负载因子为3/4, 相当于哈希表反向的计算. 所以gdb_objc_realized_classes这是一张哈希表, 存储了所有的类(不管实现未实现)

2. UnfixedSelectors修复方法
SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                //sel_registerNameNoLock → __sel_registerName → search_builtins → _dyld_get_objc_selector
                //从dyld中加载进来的sel链接地址
                SEL sel = sel_registerNameNoLock(name, isBundle);
                if (sels[i] != sel) {
                    sels[i] = sel;
                }
            }

_getObjc2SelectorRefs是从macho里面加载的sel
sel_registerNameNoLock_dyld_get_objc_selector从dyld链接加载的可以访问的内存地址的方法, 修复macho加载出来的方法地址定向.

3.类的处理

在此部分会初始化类的名称。类已移动但未删除,对错误混乱的类进行处理。readClass会对类做一些处理.

(lldb) po cls
objc[43705]: mutex incorrectly locked
objc[43705]: mutex incorrectly locked
0x0000000100409fe8

(lldb) po cls
objc[43705]: mutex incorrectly locked
objc[43705]: mutex incorrectly locked
OS_object

(lldb) po newCls
objc[43705]: mutex incorrectly locked
objc[43705]: mutex incorrectly locked
OS_object

上面是断点打印, 可以发现for循环内部不会走, 经历readClass前后打印cls发生了变化. readClass
整个流程是_read_imagesreadClassaddNamedClass:gdb_objc_realized_classes把类插入表里(name, cls)addClassTableEntry插元类.
终止条件为,allocatedClasses表里已存在, 所以元类不会一直加载.

4.重映射类remap classes
  • 修复重映射的类
  • 类列表和非懒加载类列表保持未重新映射。
  • 类引用和super引用被重新映射用于消息调度。
5.消息修复fixupMessageRef

llvm阶段已经做了, 在这里不会走的. 可以看一下, 哪些方法需要检查修复:

msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == @selector(alloc)) {
            msg->imp = (IMP)&objc_alloc;
        } else if (msg->sel == @selector(allocWithZone:)) {
            msg->imp = (IMP)&objc_allocWithZone;
.....只看几个, 就知道了, llvm阶段会做一些符号的修改, 所以我们在之前篇章的时候, `alloc`调用的符号竟然是`objc_alloc`而疑惑了好久
6.发现协议

简要来说就是将协议插入协议表protocols()

7.修复协议的引用
8.Discover categories.

load_categories_nolock分类的加载, 在dyld调用完成之后才会调用, 之类不会走到, 后面会了解到分类.
你会发现上面都是Discover xxx之后会Fix up xxx, 这里分类却没有, 所以分类的加载, 后续在讨论

9.懒加载类的调用
// Realize non-lazy classes (for +load methods and static instances)

注释写到, 实现非懒加载的类, 调用load方法和静态实例.
在3流程中调用过readClass里面的整个流程. 所以

for (EACH_HEADER) {
//从非懒加载表里取出, 非懒加载类
        classref_t const *classlist = hi->nlclslist(&count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;            
//3流程中走过`readImage`里面加入了表里, 所以这里不会走
            addClassTableEntry(cls);
//在这里进行实现
            realizeClassWithoutSwift(cls, nil);
        }
    }
  • 非懒加载类在这里进行实现.
  • _read_images
    realizeClassWithoutSwiftmethodizeClass
  • 懒加载类, 在进行第一次消息调用的时候会走到消息慢速查找的时候实现.
  • lookUpImpOrForward
    realizeAndInitializeIfNeeded_locked
    realizeClassMaybeSwiftAndLeaveLocked
    realizeClassMaybeSwiftMaybeRelock
    realizeClassWithoutSwiftmethodizeClass
10.realize future classes

实现未来类, 以防止CF操作他们, 调试了一下 ,没有进入到这里. 不关注了.

readImage

readImage的核心流程

  • addNamedClass的时候将namecls加入gdb_objc_realized_classes表里, 并且关联地址
  • addClassTableEntry的时候将类以及ISA走向的类递归加入allocatedClasses表中

所以readClass的核心逻辑是将类与地址关联,并加入gdb_objc_realized_classesallocatedClasses表中

  • 调用runtime_init()初始化了allocatedClassesunattachedCategories表.
  • _read_imagesdoneOnce流程中初始化了gdb_objc_realized_classes这张表.

总结:

objc_init:

  • environ_init();
  • tls_init();
  • static_init();
  • runtime_init();
  • exception_init();
  • cache_t::init();
  • _dyld_objc_notify_register(&map_images, load_images, unmap_image);

map_images:

  • doneOnce创建类的总表, 等一些只进行一次的操作.
  • UnfixedSelectors修复混乱的方法
  • discover classes发现类, 调用了readImage
  • remap classes类的重映射
  • objc_msgSend_fixup消息修复
  • discover protocols协议加进协议表里
  • fix up @protocol references协议的引用修复
  • discover categories分类处理, 不会走到这里, 注释已经说明了, 在回调函数走完之后才可以.
  • realize non-lazy classes实现非懒加载的类, realizeClassWithoutSwift实现的主要方法, 核心在于load方法的实现.
  • realize future classes不是重点, 不关注.
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容