iOS底层-类的加载之read_images分析

852 阅读13分钟

前言

上篇 dyld加载流程 中,核心方法是 _dyld_objc_notify_register,是在_objc_init中实现了通知的注册,该方法有三个参数:map_imagesload_imagesunmap_image,下面具体看一下这三个函数的具体作用。

_objc_init

void _objc_init(void) {
    // 判断初始化标记
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // 环境变量的初始化
    environ_init();
    // 关于线程key的绑定
    tls_init();
    // 全局静态C++的构造函数的调用
    static_init();
    // runtime运行环境的初始化
    runtime_init();
    // 异常处理系统的初始化。
    exception_init();
#if __OBJC2__
    // 缓存类的初始化
    cache_t::init();
#endif
    // 启动回调机制。
    _imp_implementationWithBlock_init();
    // 注册dyld通知,第二个参数是load_images
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    // 完成通知dyld的注册回调
    didCallDyldNotifyRegister = true;
#endif
}
  • environ_init():环境变量的初始化,主要是影响运行时的环境变量,通过环境变量可以帮助调试。
  • tls_init():关于线程key的绑定,比如线程数据的构造函数。
  • static_init():全局静态C++的构造函数的调用。这里只是调用objc自己的,在dyld调用之前,不是dyld调用的。
  • runtime_init():runtime运行环境的初始化。主要是 unattachedCategories 和 allocatedClasses 两张表的创建
  • exception_init():初始化 libobjc 的异常处理系统。
  • cache_t::init():缓存类的初始化。
  • _imp_implementationWithBlock_init():启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的。但是对于某些进程,比如 QtWebEngineProcess,则会初始化Trampolines库。
  • _dyld_objc_notify_register(&map_images, load_images, unmap_image):加载镜像资源,加载类,加载分类等。

environ_init

void environ_init(void)
{
    // 打印 OBJC_HELP 和 OBJC_PRINT_OPTIONS 输出。
    if (PrintHelp  ||  PrintOptions) {
        // 打印 Settings 列表所有的选项
        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);
        }
    }
}

通过OBJC_HELPOBJC_PRINT_OPTIONS打印出系统提供的环境变量。可以通过几种方法获取环境变量。

获取环境变量

  • 直接方式

查看Settings源码:

const option_t Settings[] = {
#define OPTION(var, env, help) option_t{&var, #env, help, strlen(#env)}, 
#include "objc-env.h"
#undef OPTION
};

Settings[]来源于objc-env.h,在objc源码中,直接搜索objc-env.h文件:

  • 源码调试方式

将核心代码修改如下:

void environ_init(void)
{
    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);
    }
}

打印如下:

  • 终端方式
export OBJC_HELP=1

打印如下:

环境变量的使用

设置OBJC_PRINT_IMAGESOBJC_PRINT_LOAD_METHODSOBJC_DISABLE_NONPOINTER_ISA等环境变量。

开启OBJC_PRINT_IMAGES打印:

可以查看已经加载的images

开启OBJC_PRINT_LOAD_METHODS打印:

查看所有实现的load()方法。

开启OBJC_DISABLE_NONPOINTER_ISA打印:

(lldb) x/4gx subObj
0x1012042a0: 0x0000000100008248 0x0000000000000000
0x1012042b0: 0x0000000780880000 0x00000001004c6228
(lldb) p/t 0x0000000100008248
(long) $1 = 0b0000000000000000000000000000000100000000000000001000001001001000

关闭OBJC_DISABLE_NONPOINTER_ISA打印:

(lldb) x/4gx subObj
0x101304a80: 0x011d800100008249 0x0000000000000000
0x101304a90: 0x746e6f43534e5b2d 0x6f79616c206c6f72
(lldb) p/t 0x011d800100008249
(long) $1 = 0b0000000100011101100000000000000100000000000000001000001001001001

isa最后一位,开启时是0,关闭时是1,也就是是否是纯指针(0为纯指针)。

tls_init

void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
    // 读取线程Key
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    // 设置线程key
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

线程key的初始化:

  • 是否支持THREAD_KEYS,如果支持,根据TLS_DIRECT_KEY读取线程key
  • 如果不支持,直接创建线程key

static_init

static void static_init()
{
    // 根据 SECTION_TYPE 不同读取 Mach-O中不同的 section。
    size_t count;
    // 加载__objc_init_func,对应MachO文件中的 (__DATA,__mod_init_func)。
    auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        // 内存平移加载
        inits[i]();
    }
    
    // 加载__objc_init_offs,对应MachO文件中的 (__TEXT,__init_offsets)。
    auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        // 内存平移加载
        UnsignedInitializer init(offsets[i]);
        init();
    }
}

// getLibobjcInitializers定义
GETSECT(getLibobjcInitializers,       UnsignedInitializer, "__objc_init_func");

// getLibobjcInitializerOffsets定义
uint32_t *getLibobjcInitializerOffsets(const headerType *mhdr, size_t *outCount) {
    unsigned long byteCount = 0;
    uint32_t *offsets = (uint32_t *)getsectiondata(mhdr, "__TEXT", "__objc_init_offs", &byteCount);
    if (outCount) *outCount = byteCount / sizeof(uint32_t);
    return offsets;
}
  • 全局静态C++的构造函数的调用。在dyld调用doModInitFunctions静态构造函数之前,objc调用自己的构造函数。因为此时objc的初始化,对section进行了替换,所以后续dyld不会调用到这块。
  • 内部有两个方式(从macho文件读取c++构造函数):
    • 通过__DATA,__mod_init_func
    • 通过__TEXT,__init_offsets

验证

执行了__objc_init_func函数。

扩展

macho文件的读取时,macho对应的文件名和方法对应不上,原因是在dosect (markgc.cpp)中对section数据进行的映射修改:

  • __mod_init_func被修改称为__objc_init_funcSECTION_TYPES_MOD_INIT_FUNC_POINTERS(0x9)
  • __init_offsets被修改称为__objc_init_offsSECTION_TYPES_INIT_FUNC_OFFSETS(0x16)
  • __mod_term_func被修改称为__objc_term_funcSECTION_TYPES_MOD_TERM_FUNC_POINTERS(0xa)

宏定义在#import <mach-o/loader.h>头文件中可以找到。

#define S_MOD_INIT_FUNC_POINTERS 0x9 /* section with only function pointers for initialization*/
#define S_MOD_TERM_FUNC_POINTERS 0xa /* section with only function pointers for termination */
#define S_INIT_FUNC_OFFSETS      0x16 /* 32-bit offsets to initializers */

runtime_init

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

void runtime_init(void)
{
    // 未附加的类别表
    objc::unattachedCategories.init(32);
    // 已加载的类表,包含的是所有`allocated`的类和元类
    objc::allocatedClasses.init();
}

// 初始化表方法
init() {
    template <typename... Ts>
    void init(Ts &&... Args) {
        new (_storage) Type(std::forward<Ts>(Args)...);
    }
    Type &get() {
        return *reinterpret_cast<Type *>(_storage);
    }
}

在后续的类和分类的加载中会使用到。

exception_init

void exception_init(void)
{
    // 设置异常捕获函数
    old_terminate = std::set_terminate(&_objc_terminate);
}

static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
    if (PrintExceptions) {
        _objc_inform("EXCEPTIONS: terminating");
    }
    // 当前c++异常类型不存在
    if (! __cxa_current_exception_type()) {
        // c++终止回调
        (*old_terminate)();
    } else {
        // 如果是当前异常类型,判断是否是objc异常。
        @try {
            // 回滚作用域,重新捕获
            __cxa_rethrow();
        } @catch (id e) {
            // 如果是objc对象,错误回调处理
            (*uncaught_handler)((id)e);
            // c++终止回调
            (*old_terminate)();
        } @catch (...) {
            // 如果不是objc对象,执行c++终止回调
            (*old_terminate)();
        }
    }
}

从源码分析:当执行exception_init时会挂载一个异常处理的回调uncaught_handler,或许可以通过uncaught_handler截取异常崩溃信息。

uncaught_handler

static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;

objc_uncaught_exception_handler objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    // 设置未捕获的objc对象的异常处理的回调
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    // 返回回调
    return result;
}

异常捕获案例

根据uncaught_handler函数可以实现异常捕获类的封装:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 注册异常监听
    [ZLCrashCapturer registerCrashCapture];
    return YES;
}

didFinishLaunchingWithOptions实现异常捕获类的注册。

+ (void)registerCrashCapture {
    NSSetUncaughtExceptionHandler(&ZLUncaughtException);
}

// 挂载回调函数
void ZLUncaughtException(NSException *exception) {
    // 保存崩溃信息到本地
    saveLocal();
    // 回调处理
    if (handler) {
        handler(exception);
    }
}

通过注册回调函数,当发生异常时,会实时回调给ZLUncaughtException函数,进而记录异常信息,并回调。

注意:

  • 该方法只能捕获oc对象的异常。
  • objc_setUncaughtExceptionHandler对应上层方法NSSetUncaughtExceptionHandler
  • 自定义的监听方法尽量放在didFinishLaunchingWithOptions的最后面,防止其它SDK覆盖。过多SDK调用NSSetUncaughtExceptionHandler有时会混乱,表现是捕获到的crash没有符号表。

cache_t::init

缓存类的初始化

#if TARGET_OS_SIMULATOR || defined(__i386__) || defined(__arm__) || !TARGET_OS_MAC
#   define HAVE_TASK_RESTARTABLE_RANGES 0
#else
#   define HAVE_TASK_RESTARTABLE_RANGES 1
#endif

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;
#endif
}

_imp_implementationWithBlock_init

启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的。但是对于某些进程,比如 QtWebEngineProcess或者Steam Helper,则会初始化Trampolines库。

void _imp_implementationWithBlock_init(void) {
#if TARGET_OS_OSX
    if (__progname &&
        (strcmp(__progname, "QtWebEngineProcess") == 0 ||
         strcmp(__progname, "Steam Helper") == 0)) {
        Trampolines.Initialize();
    }
#endif
}

_dyld_objc_notify_register

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

_dyld_objc_notify_register注册了map_imagesload_imagesunmap_image的回调。其实现是在dyld初始化主程序完成时调用notifySingle

  • map_images:管理文件中和动态库中所有的符号 (classProtocolselectorcategory)。
  • load_images:加载执行load方法
  • unmap_image:释放类相关资源。

这里的map_imagesload_images传递的参数方式不同,map_images是指针拷贝,load_images是值传递。因为map_images需要同步变化,它非常重要,是映射镜像文件方法,同步变化是避免发生错乱的可能性。而load_images比较简单只是load的调用,不需要同步变化。

map_images分析

void map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    // 创建锁
    mutex_locker_t lock(runtimeLock);
    // 调用 map_images_nolock
    return map_images_nolock(count, paths, mhdrs);
}

map_images_nolock

其中map_images_nolock核心代码为_read_images

void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    if (hCount > 0) {
        // 读取镜像文件
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
}

_read_images 流程分析

_read_images是管理文件中和动态库中所有的符号(classprotocolsselectorcategories )

_read_images函数源码进行代码块折叠后发现,每一个log说明了该代码快的作用。主要的步骤如下:

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

doneOnce

if (!doneOnce) {
    doneOnce = YES;
    // 初始化小对象类型的混淆处理
    initializeTaggedPointerObfuscator();
    // 获取开辟的内存容量,相当于是负载因子3/4的逆过程。
    // totalClasses总容量,unoptimizedTotalClasses要占用的空间。
    int namedClassesSize = (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
    // 创建一张总表,包括在runtime_init创建的两张表,包含关系
    gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
}
  • taggedPointer混淆
  • 通过负载因子load factor计算总表大小
  • 创建总表,包含runtime_init创建的unattachedCategoriesallocatedClasses
  • 总表gdb_objc_realized_classes,无论类是否实例化。

总结:doneOnce主要作用创建总表(只执行一次)。

unfixedSelectors

mutex_locker_t lock(selLock);
for (EACH_HEADER) {
    if (hi->hasPreoptimizedSelectors()) continue;
    bool isBundle = hi->isBundle();
    // 从macho读取所有的sels符号表
    SEL *sels = _getObjc2SelectorRefs(hi, &count);
    UnfixedSelectors += count;
    for (i = 0; i < count; i++) {
        const char *name = sel_cname(sels[i]);
        // 从dyld中获取sel
        SEL sel = sel_registerNameNoLock(name, isBundle);
        if (sels[i] != sel) {
            // 地址比较,对地址的修复
            sels[i] = sel;
        }
    }
}
  • sel是在dyld加载的,sels[i]是从macho读出来的,从macho读出来的地址可能是混乱的。
  • sels[i] = sel是对地址进行覆盖,因为在dyld加载出来的sel地址是有效的。
  • 查找sel的调用路径:sel_registerNameNoLock__sel_registerNamesearch_builtins_dyld_get_objc_selector。明显看出sel_dyld_get_objc_selector获取的。

总结:unfixedSelectors主要作用修复sel地址混乱问题。

sels[i] = sel打断点,验证:

discover classes

for (EACH_HEADER) {
    // 从macho读取ClassList
    classref_t const *classlist = _getObjc2ClassList(hi, &count);
    for (i = 0; i < count; i++) {
        Class cls = (Class)classlist[i];
        // 读取类(重点)
        Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
        // 只有当新类解析了将来的类时,才会执行
        if (newCls != cls  &&  newCls) {
            // 当类已经移除(出现空内存),但是并没有删除。
            // 非懒加载实现未来类的决议
            resolvedFutureClasses = (Class *)realloc(resolvedFutureClasses, 
                        (resolvedFutureClassCount+1) * sizeof(Class));
            resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
        }
    }
}
  • macho加载类的表信息
  • 通过readClass读取类
  • 未来类的决议

readClass前后打断点,分别验证:

总结:discover classes主要作用调用readClass加载类信息。(后面具体分析readClass)

remap classes

for (EACH_HEADER) {
    // 读取macho的类引用表
    Class *classrefs = _getObjc2ClassRefs(hi, &count);
    for (i = 0; i < count; i++) {
        // 重映射类引用,以防引用的类被重新分配
        remapClassRef(&classrefs[i]);
    }
    // 读取macho的父类引用表
    classrefs = _getObjc2SuperRefs(hi, &count);
    for (i = 0; i < count; i++) {
        // 重映射父类引用,以防引用的父类被重新分配
        remapClassRef(&classrefs[i]);
    }
}
  • macho分别读取引用表和父类引用表
  • 调用remapClassRef重映射类的引用,防止引用的类被重新分配
  • remapClassRef()remapClass()重映射类引用。

总结:remap classes主要作用重映射类及父类引用,防止被重新分配

objc_msgSend_fixup

for (EACH_HEADER) {
    // 获取macho中sel方法表
    message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
    if (count == 0) continue;
    for (i = 0; i < count; i++) {
        // 修复方法引用(映射方法),比如上层调用alloc,底层调用的是objc_alloc
        // 此处为了防止出错,其实llvm在编译期间就做了,这是是防止有其他影响变了,所以进行修复
        fixupMessageRef(refs+i);
    }
}
  • 获取machosel方法表
  • 重映射方法引用(映射方法)
  • 正常情况下不会走这里,因为方法映射在llvm阶段已经处理了。

总结:objc_msgSend_fixup主要作用映射sel引用

discover protocols

for (EACH_HEADER) {
    // 创建协议表(如果当前类有协议的话)
    NXMapTable *protocol_map = protocols();
    bool isBundle = hi->isBundle();
    // 从macho获取协议表
    protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
    for (i = 0; i < count; i++) {
        // 读取协议,并重映射协议 
        readProtocol(protolist[i], cls, protocol_map, 
                     isPreoptimized, isBundle);
    }
}
  • NXMapTable创建协议表
  • 获取macho协议列表
  • 重映射协议,并存储到协议表中

总结:discover protocols主要作用读取协议

readProtocol

static void
readProtocol(protocol_t *newproto, Class protocol_class,
             NXMapTable *protocol_map, 
             bool headerIsPreoptimized, bool headerIsBundle)
{
    // 插入表方法
    auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;
    // 获取旧的协议
    protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);
    if (oldproto) {
        // 如果存在旧的协议,且不等于新的协议
        if (oldproto != newproto) {
            if (headerIsPreoptimized && !oldproto->isCanonical()) {
                // 获取缓存的协议,也就是旧的协议
                auto cacheproto = (protocol_t *)
                    getSharedCachePreoptimizedProtocol(newproto->mangledName);
                if (cacheproto && cacheproto->isCanonical())
                    // 清除协议(通过与运算)
                    cacheproto->clearIsCanonical();
            }
        }
    } else if (headerIsPreoptimized) {
        // 获取预优化协议
        protocol_t *cacheproto = (protocol_t *)
            getPreoptimizedProtocol(newproto->mangledName);
        protocol_t *installedproto;
        if (cacheproto  &&  cacheproto != newproto) {
            installedproto = cacheproto;
        } else {
            installedproto = newproto;
        }
        // 如果存在预加载协议,则插入;否则插入新的协议
        insertFn(protocol_map, installedproto->mangledName, 
                 installedproto);
    } else {
        // 初始化协议类
        newproto->initIsa(protocol_class);  // fixme pinned
        // 插入新协议
        insertFn(protocol_map, newproto->mangledName, newproto);
    }
}
  • 判断是否存在旧的协议,如果存在则清除
  • 如果不存在且有预优化标记,查看预优化缓存协议,如果存在,则插入,否则插入新的
  • 如果都没有,则创建新的协议类,并插入新协议。

fix up @protocol references

for (EACH_HEADER) {
    // 获取macho协议引用列表
    protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
    for (i = 0; i < count; i++) {
        // 重映射协议引用,防止协议被重新分配
        remapProtocolRef(&protolist[i]);
    }
}
  • 获取macho协议引用表
  • 重映射协议引用,防止协议被重新分配。(自定义的协议不会走到这里)

总结:fix up @protocol references主要作用重映射协议引用

discover categories

if (didInitialAttachCategories) {
    for (EACH_HEADER) {
        // 加载分类
        load_categories_nolock(hi);
    }
}
  • 仅在初始类别之后执行此操作,因此此处不会执行。
  • 推迟到load_images调用。

总结:discover categories并不会加载分类,分类的加载必须在load_images之后

realize non-lazy classes

for (EACH_HEADER) {
    // 获取macho非懒加载的列表
    classref_t const *classlist = hi->nlclslist(&count);
    for (i = 0; i < count; i++) {
        // 映射非懒加载类
        Class cls = remapClass(classlist[i]);
        if (!cls) continue;
        // 添加非懒加载类到 allocatedClasses 表,该方法默认添加类的元类。
        addClassTableEntry(cls);
        // 初始化非懒加载类
        realizeClassWithoutSwift(cls, nil);
    }
}
  • 获取macho非懒加载的类的列表
  • 加载类,并添加到allocatedClasses
  • 初始化非懒加载类(realizeClassWithoutSwift函数后续分析)
  • 一般情况下,类实现了+ load方法,会进入。因为+load方法的类会出现在__objc_nlclslist中。

非懒加载类分为三种情况:

  • 自己实现了+ load方法(会出现在__objc_nlclslist中)
  • 子类实现了+ load方法(不会出现在__objc_nlclslist中)
  • 分类实现了+ load方法(包括自己的分类以及子类的分类,因为分类的加载不是在map_images流程,而是在load_images流程。因此不会出现在__objc_nlclslist中)

总结:realize non-lazy classes主要作用初始化非懒加载类

realize future classes

// 实现新解析的未来类,以防CF操纵它们
if (resolvedFutureClasses) {
    for (i = 0; i < resolvedFutureClassCount; i++) {
        Class cls = resolvedFutureClasses[i];
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class is not allowed to be future");
        }
        // 初始化未来类
        realizeClassWithoutSwift(cls, nil);
        cls->setInstancesRequireRawIsaRecursively(false/*inherited*/);
    }
    // 释放内存
    free(resolvedFutureClasses);
}

realize future classes初始化future类,正常不会执行此逻辑。

readClass重点分析

加载类和元类,主要是进行了地址关联,将地址关联到类的操作。

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->nonlazyMangledName();
    // 如果没有父类(可能是弱链接的)。
    // 通常情况下,不会执行这里
    if (missingWeakSuperclass(cls)) {
        // 重新绑定父类为nil
        addRemappedClass(cls, nil);
        // 设置父类为nil
        cls->setSuperclass(nil);
        return nil;
    }
    // 替换类
    Class replacing = nil;
    if (mangledName != nullptr) {
        // 根据类名获取未来类
        // 通常情况下,不会执行这里
        if (Class newCls = popFutureNamedClass(mangledName)) {
            ...
        }
    }
    if (headerIsPreoptimized  &&  !replacing) {
        // 断言 cls == getClass(name)
        ASSERT(mangledName == nullptr || getClassExceptSomeSwift(mangledName));
    } else {
        if (mangledName) {
            // 关联类信息,加入总表 gdb_objc_realized_classes
            addNamedClass(cls, mangledName, replacing);
        } else {
            // 断言非元类是否存在
        }
        // 加入 cls 到 allocatedClasses 表
        addClassTableEntry(cls);
    }
    // 通常情况下,不会执行这里
    if (headerIsBundle) {
        // 设置标志位
        cls->data()->flags |= RO_FROM_BUNDLE;
        cls->ISA()->data()->flags |= RO_FROM_BUNDLE;
    }
    return cls;
}

该方法中虽然判断很多,但是通过断点分析,正常情况下,会执行addNamedClassaddClassTableEntry

addNamedClass

关联类信息,类和地址的关联,并加入总表gdb_objc_realized_classes

static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
    Class old;
    // 按名称查找,获取可能未实现的类。
    if ((old = getClassExceptSomeSwift(name))  &&  old != replacing) {
        inform_duplicate(name, old, cls);
        // 未按名称查找的类存于非元类表 nonmeta_class_map
        addNonMetaClass(cls);
    } else {
        // 如果没找到,插入到 gdb_objc_realized_classes 表
        NXMapInsert(gdb_objc_realized_classes, name, cls);
    }
}
  • 主要目的是将cls插入到gdb_objc_realized_classes总表中(doneOnce中创建的总表)
  • NXMapInsert处理了关联类与地址。在插入总表的时候,进行MapPair(key-value)的形式关联。

NXMapInsert

typedef struct _MapPair {
    const void *key;
    const void *value;
} MapPair;

void *NXMapInsert(NXMapTable *table, const void *key, const void *value) {
    // 获取buckets
    MapPair *pairs = (MapPair *)table->buckets;
    unsigned index = bucketOf(table, key);
    // 找到对应的pair
    MapPair *pair = pairs + index;
    // 新buckets地址
    unsigned numBuckets = table->nbBucketsMinusOne + 1;
    
    if (isEqual(table, pair->key, key)) {
        const void *old = pair->value;
        // 非必需避免写入
        if (old != value) pair->value = value;
        return (void *)old;
    } else if (table->count == numBuckets) {
        // 没有空间,重新hash
        _NXMapRehash(table);
        return NXMapInsert(table, key, value);
    } else {
        unsigned index2 = index;
        while ((index2 = nextIndex(table, index2)) != index) {
            pair = pairs + index2;
            if (pair->key == NX_MAPNOTAKEY) {
                pair->key = key; pair->value = value;
                table->count++;
                // 判断负载因子
                if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);
                return NULL;
            }
            if (isEqual(table, pair->key, key)) {
                const void *old = pair->value;
                // 非必需避免写入
                if (old != value) pair->value = value;
                return (void *)old;
            }
        }
        return NULL;
    }
}

该方法的主要作用是将加入到总表中,并做地址关联

addClassTableEntry

加入clsallocatedClasses表,默认也会将元类加入其中。

static void addClassTableEntry(Class cls, bool addMeta = true)
{
    // 获取allocatedClasses表
    auto &set = objc::allocatedClasses.get();
    // 如果类不存在,则直接插入
    if (!isKnownClass(cls))
        set.insert(cls);
    if (addMeta)
        // 默认插入元类,但元类的元类不在插入
        addClassTableEntry(cls->ISA(), false);
}

该方法的主要作用是将元类加入到allocatedClasses表中(runtime_init中创建的allocatedClasses表)。

小结

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

总结

承接dyld加载流程,类的加载流程如下:

补充

懒加载和非懒加载

懒加载和非懒加载类的说明:

  • 为了按需分配,启动过程中初始化的类越少启动速度就越快
  • 对于非懒加载类实现了+load方法(子类/分类/自己),类就会提前加载,为+ load的调用做准备。
  • 对于懒加载类,是在第一次消息发送objc_msgSend,进行lookUpImpOrForward消息慢速查找的时候进行初始化的。

懒加载和非懒加载区别:

  • 懒加载类:数据加载推迟到第一次发送消息的时候。
    • lookUpImpOrForward
    • realizeClassMaybeSwiftMaybeRelock
    • realizeClassWithoutSwift
    • methodizeClass
  • 非懒加载类map_images的时候加载所有类数据。
    • readClass
    • _getObjc2NonlazyClassList
    • realizeClassWithoutSwift
    • methodizeClass