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

推荐阅读更多精彩内容