iOS底层原理之OC类的加载原理(上)

2,100 阅读22分钟

前言

前文iOS底层原理之dyld应用程序加载分析了dyld的整个流程以及dyldobjc的交互流程。本文将接着分析dyld调用map_images究竟做了什么操作。

准备工作

1: _objc_init函数分析

从前文可知,dyld的回调是在_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 关于线程key的绑定,比如线程数据的析构函数
    tls_init();
    // 运行 C++ 静态构造函数,这里只是调用objc自己的。
    // libc在dyld调用静态构造函数之前调用了_objc_init(),所以我们必须自己做。
    static_init();
    // runtime相关两张表的初始化(类表和未附加到类的分类表)
    runtime_init();
    // 初始化 libobjc 的异常处理系统
    exception_init();
#if __OBJC2__
    // 缓存条件初始化
    cache_t::init();
#endif
    // 启动trampoline机制。通常情况下,这不起任何作用,
    // 因为一切都是惰性地初始化的,但是对于某些进程,
    // 我们急切地加载libobjc-trampolines.dylib动态库。
    _imp_implementationWithBlock_init();

    // map_images:管理由 dyld 映射的给定镜像文件的所有符号(class,Protocol, selector,category)
    // map_images加了&符号,表示指针传递,因为这个函数负责类的加载,
    // 非常重要,需要跟着dyld里面的函数一起变化
    // load_images:执行由dyld映射的给定镜像文件的 +load方法
    // unmap_image:处理即将被 dyld 取消映射的给定镜像文件
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    // 标识对 _dyld_objc_notify_register 的调用已完成。
    didCallDyldNotifyRegister = true;
#endif
}
  • environ_init:环境变量的初始化,可以通过在Xcode->Edit Scheme->Arguments->Environment Variables中配置打印查看。
  • tls_init:关于线程key的绑定,比如线程数据的析构函数。
  • static_init:运行C++静态构造函数,这里只是调用objc自己的。libcdyld调用静态构造函数之前调用了_objc_init()函数,所以我们必须自己调。
  • runtime_init:runtime相关两张表的初始化(类表和未附加到类的分类表)。
  • exception_init:初始化libobjc的异常处理系统。
  • cache_t::init:缓存条件初始化。
  • _imp_implementationWithBlock_init:启动trampoline机制。通常情况下,这不起任何作用,因为一切都是惰性地初始化的,但是对于某些进程,我们急切地加载libobjc-trampolines.dylib动态库。
  • _dyld_objc_notify_register:注册map_imagesload_imagesunmap_image的回调。
  • didCallDyldNotifyRegister = true:标识对_dyld_objc_notify_register的调用已完成。

1.1: environ_init函数

environ_init函数对一些环境变量进行了初始化,核心代码如下:

/***********************************************************************
* environ_init
* Read environment variables that affect the runtime.
* Also print environment variable help, if requested.
**********************************************************************/
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);
        }
    }
}

可以看到是通过PrintHelp  ||  PrintOptions来控制是否打印环境变量的。下面通过三种方式来打印出当前的环境变量配置。

1.1.1: 修改代码位置输出日志信息

直接将for循环打印环境变量的相关代码拷贝到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);
    }

    // Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
    if (PrintHelp  ||  PrintOptions) {
        ...
    }

打印输出:

image.png

1.1.2: 终端输出

使用终端命令export OBJC_HELP=1输出环境变量。

image.png

  • 先使用export OBJC_HELP=1命令导出OBJC_HELP,然后执行一个终端命令就可以打印出环境变量了。

1.1.3: 查看源码文件

objc源码里搜索前两步打印出来的相关环境配置,定位到objc-env.h文件中查看相关源码。

image.png

1.1.4: Xcode设置环境变量辅助调试

Xcode->Edit Scheme->Arguments->Environment Variables中配置OBJC_DISABLE_NONPOINTER_ISAOBJC_PRINT_LOAD_METHODSOBJC_PRINT_INITIALIZE_METHODS三个环境变量,依次勾选测试。

image.png

OBJC_DISABLE_NONPOINTER_ISA:配置是否关闭nonpointer isa

// 勾选OBJC_DISABLE_NONPOINTER_ISA = YES前
(lldb) x/4gx p
0x100544e20: 0x011d8001000083b1 0x0000000000000000
0x100544e30: 0x0000000000000000 0x0000000000000000
(lldb) p/t 0x011d8001000083b1
// isa最后一位为1,是nonpointer isa
(long) $1 = 0b0000000100011101100000000000000100000000000000001000001110110001

// 勾选OBJC_DISABLE_NONPOINTER_ISA = YES后
(lldb) x/4gx p
0x101074150: 0x00000001000083b0 0x0000000000000000
0x101074160: 0x0000000000000000 0x0000000000000000
(lldb) p/t 0x00000001000083b0
// isa最后一位为0,是纯isa
(long) $1 = 0b0000000000000000000000000000000100000000000000001000001110110000
  • 可以看到isa在勾选前输出为1,勾选后输出为0。也就是是否为存指针。

关于nonpointer isa可以学习这篇文章OC底层原理初探之对象的本质(三)alloc探索下

OBJC_PRINT_LOAD_METHODS:打印所有所有实现了+load方法的类信息

objc[39927]: LOAD: class 'NSApplication' scheduled for +load
objc[39927]: LOAD: class 'NSBinder' scheduled for +load
objc[39927]: LOAD: class 'NSColorSpaceColor' scheduled for +load
objc[39927]: LOAD: class 'NSNextStepFrame' scheduled for +load
objc[39927]: LOAD: category 'NSColor(NSUIKitSupport)' scheduled for +load
objc[39927]: LOAD: +[NSApplication load]

objc[39927]: LOAD: +[NSBinder load]

objc[39927]: LOAD: +[NSColorSpaceColor load]

objc[39927]: LOAD: +[NSNextStepFrame load]

objc[39927]: LOAD: +[NSColor(NSUIKitSupport) load]

objc[39927]: LOAD: category 'NSError(FPAdditions)' scheduled for +load
objc[39927]: LOAD: +[NSError(FPAdditions) load]

objc[39927]: LOAD: class '_DKEventQuery' scheduled for +load
objc[39927]: LOAD: +[_DKEventQuery load]

objc[27830]: LOAD: class 'XJPerson' scheduled for +load
objc[27830]: LOAD: class 'XJStudent' scheduled for +load
objc[27830]: LOAD: +[XJPerson load]
  • 直接通过配置OBJC_PRINT_LOAD_METHODS环境变量,就可以知道项目中哪些类实现了+load方法,从而处理启动优化。

OBJC_PRINT_INITIALIZE_METHODS:打印所有所有实现了+initialize方法的类信息

objc[27950]: INITIALIZE: thread 0x1000dedc0: calling +[NSObject initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: finished +[NSObject initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: NSObject is fully +initialized
objc[27950]: INITIALIZE: thread 0x1000dedc0: setInitialized(NSObject)
objc[27950]: INITIALIZE: thread 0x1000dedc0: calling +[OS_dispatch_data initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: finished +[OS_dispatch_data initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: OS_dispatch_data is fully +initialized
objc[27950]: INITIALIZE: thread 0x1000dedc0: setInitialized(OS_dispatch_data)
objc[27950]: INITIALIZE: thread 0x1000dedc0: calling +[OS_object initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: finished +[OS_object initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: OS_object is fully +initialized
objc[27950]: INITIALIZE: thread 0x1000dedc0: setInitialized(OS_object)
objc[27950]: INITIALIZE: thread 0x1000dedc0: calling +[OS_xpc_object initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: finished +[OS_xpc_object initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: OS_xpc_object is fully +initialized
objc[27950]: INITIALIZE: thread 0x1000dedc0: setInitialized(OS_xpc_object)
objc[27950]: INITIALIZE: thread 0x1000dedc0: calling +[OS_xpc_string initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: finished +[OS_xpc_string initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: OS_xpc_string is fully +initialized
objc[27950]: INITIALIZE: thread 0x1000dedc0: setInitialized(OS_xpc_string)
objc[27950]: INITIALIZE: thread 0x1000dedc0: calling +[OS_xpc_data initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: finished +[OS_xpc_data initialize]
objc[27950]: INITIALIZE: thread 0x1000dedc0: OS_xpc_data is fully +initialized
  • 直接通过配置OBJC_PRINT_INITIALIZE_METHODS环境变量,就可以知道项目中哪些类实现了+initialize方法,从而处理启动优化。

1.2: 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
}

1.3: static_init函数

运行C++静态构造函数。libcdyld调用静态构造函数之前调用了_objc_init(),所以我们必须自己调。

/***********************************************************************
* 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()
{
    // 这两个是根据SECTION_TYPE不同读取不同的section,
    // 当为S_MOD_INIT_FUNC_POINTERS时读取__mod_init_funcs,
    // 当为S_INIT_FUNC_OFFSETS(0x16)时读取__init_offsets
    size_t count;
    // 查找__objc_init_func,对应macho文件中的 __DATA __mod_init_func。
    // 调用c++构造函数,这里是通过下标平移获取
    auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        inits[i]();
    }
    // 查找__TEXT __objc_init_offs,对应macho文件中的 __TEXT  __init_offsets
    // 调用c++构造函数,通过偏移值获取
    auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        UnsignedInitializer init(offsets[i]);
        init();
    }
}

GETSECT(getLibobjcInitializers,       UnsignedInitializer, "__objc_init_func");

#define GETSECT(name, type, sectname)                                   \
    type *name(const headerType *mhdr, size_t *outCount) {              \
        return getDataSection<type>(mhdr, sectname, nil, outCount);     \
    }                                                                   \
    type *name(const header_info *hi, size_t *outCount) {               \
        return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
    }

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++静态构造函数,这只是调用objc自己的。相当于objcC++静态构造函数是在dyld库的ImageLoaderMachO::doModInitFunctions函数调用之前自己调用的,因为dyld调用的时机太晚了。objc对这个section进行了替换,所以后续dyld不会调用到这块(下文分析)。
  • 内部有两个逻辑一个是通过__mod_init_funcs,一个是通过__init_offsetsmacho文件读取C++构造函数。目前暂不清楚什么情况下会走__init_offsets的逻辑。

直接在objc源码中添加一个C++构造函数来进行验证。

image.png

  • 可以看到在_objc_init函数调用_dyld_objc_notify_register函数注册回调之前,就已经调用了我们添加的名为objcFuncC++构造函数。

通过上面代码发现getLibobjcInitializers函数和getLibobjcInitializerOffsets函数对macho文件的读取与macho文件的实际内容是不相符的。

image.png

  • macho文件里只有名为__mod_init_funcSection,里面有我们自己添加的C++构造函数objcFunc
  • 但是getLibobjcInitializers函数读取的是名为__objc_init_funcSectiongetLibobjcInitializerOffsets函数读取的是名为__objc_init_offsSection

在源码里搜索__objc_init_func__objc_init_offs,终于在markgc.cpp文件里的dosect函数中找到了对macho文件对应的Section数据进行映射修改的代码:

template <typename P>
void dosect(uint8_t *start, macho_section<P> *sect)
{
    if (debug) printf("section %.16s from segment %.16s\n",
                      sect->sectname(), sect->segname());

    // Strip S_MOD_INIT/TERM_FUNC_POINTERS. We don't want dyld to call 
    // our init funcs because it is too late, and we don't want anyone to 
    // call our term funcs ever.
    if (segnameStartsWith(sect->segname(), "__DATA")  &&  
        sectnameEquals(sect->sectname(), "__mod_init_func"))
    {
        // section type 0 is S_REGULAR
        sect->set_flags(sect->flags() & ~SECTION_TYPE);
        sect->set_sectname("__objc_init_func");
        if (debug) printf("disabled __mod_init_func section\n");
    }
    if (segnameStartsWith(sect->segname(), "__TEXT")  &&
        sectnameEquals(sect->sectname(), "__init_offsets"))
    {
        // section type 0 is S_REGULAR
        sect->set_flags(sect->flags() & ~SECTION_TYPE);
        sect->set_sectname("__objc_init_offs");
        if (debug) printf("disabled __mod_init_func section\n");
    }
    if (segnameStartsWith(sect->segname(), "__DATA")  &&  
        sectnameEquals(sect->sectname(), "__mod_term_func"))
    {
        // section type 0 is S_REGULAR
        sect->set_flags(sect->flags() & ~SECTION_TYPE);
        sect->set_sectname("__objc_term_func");
        if (debug) printf("disabled __mod_term_func section\n");
    }
}
  • __mod_init_func被修改为了__objc_init_funcSECTION_TYPES_MOD_INIT_FUNC_POINTERS(0x9)

  • __init_offsets被修改为了__objc_init_offsSECTION_TYPES_INIT_FUNC_OFFSETS(0x16)

    查看dyld源码的ImageLoaderMachO::doModInitFunctions函数,你会发现它们获取C++构造函数的方式不同:

    // S_MOD_INIT_FUNC_POINTERS
    Initializer* inits = (Initializer*)(sect->addr + fSlide);
    ...
    for (size_t j=0; j < count; ++j) {
        Initializer func = inits[j];
        ...
        // 调用
        func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
        ...
    }
    
    // S_INIT_FUNC_OFFSETS
    const uint32_t* inits = (uint32_t*)(sect->addr + fSlide);
    ...
    for (size_t j=0; j < count; ++j) {
        uint32_t funcOffset = inits[j];
        ... 
        Initializer func = (Initializer)((uint8_t*)this->machHeader() + funcOffset);
        ...
        func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
        ...
    }
    
  • __mod_term_func被修改为了__objc_term_func。这个也就是TerminatorsSECTION_TYPES_MOD_TERM_FUNC_POINTERS(0xa)。这个实现了destructor函数就有了。

  • dosect函数最终是被objcmain函数调用的。此函数的注释中已经说明了不想被dyld调用,因为dyld调用的时机太晚了,所以进行了strip(调用流程:main->processFile->parse_fat->parse_macho->doseg->dosect)。

SECTION_TYPE相关的宏定义在mach-o/loader.h文件中可以找到(只列出相关的三个):

#define S_MOD_INIT_FUNC_POINTERS  0x9   /* section with only function pointers for initialization*/

#define S_INIT_FUNC_OFFSETS       0x16  /* 32-bit offsets to initializers */

#define S_MOD_TERM_FUNC_POINTERS  0xa   /* section with only function pointers for termination */

1.4: runtime_init函数

runtime相关allocatedClassesunattachedCategories两张储存表的初始化。

void runtime_init(void)
{
    //两张储存表的初始化
    objc::unattachedCategories.init(32);
    objc::allocatedClasses.init();
}

// ExplicitInit / LazyInit wrap doing it the hard way.
template <typename Type>
class ExplicitInit {
    alignas(Type) uint8_t _storage[sizeof(Type)];

public:
    template <typename... Ts>
    void init(Ts &&... Args) {
        new (_storage) Type(std::forward<Ts>(Args)...);
    }

    Type &get() {
        return *reinterpret_cast<Type *>(_storage);
    }
};
  • 此内容后续分析。

1.5: exception_init函数

初始化libobjc的异常处理系统。

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

static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;

/***********************************************************************
* _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.
        // 当前有一个异常。检查它是否是objc的异常
        @try {
            __cxa_rethrow();
        } @catch (id e) {
            // It's an objc object. Call Foundation's handler, if any.
            // 它是一个objc异常。呼叫 Foundation 的处理程序,如果有的话。
            //uncaught_handler exception回调
            (*uncaught_handler)((id)e);
            (*old_terminate)();
        } @catch (...) {
            // It's not an objc object. Continue to C++ terminate.
            // 它不是一个objc异常。继续调用old_terminate函数
            (*old_terminate)();
        }
    }
}
  • exception_init函数将_objc_terminate函数赋值给old_terminate函数进行异常处理。
  • 在发生objc异常时,会调用uncaught_handler函数。

uncaught_handler回调函数默认没有任何代码,可以通过objc_setUncaughtExceptionHandler函数来赋值。

/***********************************************************************
// 默认为空函数
* _objc_default_uncaught_exception_handler
* Default uncaught exception handler. Expected to be overridden by Foundation.
**********************************************************************/
static void _objc_default_uncaught_exception_handler(id exception)
{
}
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;


/***********************************************************************
// 通过此函数可以给uncaught_handler赋值
* objc_setUncaughtExceptionHandler
* Set a handler for uncaught Objective-C exceptions. 
* Returns the previous handler. 
**********************************************************************/
objc_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}

下面将通过案例来分析这部分流程。

1.5.1: exception异常捕获

在项目中添加简单的数组访问越界的代码。

self.dataArray = @[@"0",@"1",@"2",@"3",@"4"];
NSLog(@"%@",self.dataArray[5]);

运行时会发生crash,这时我们就可以通过注册异常回调的方式监听到exception,然后进行收集处理,方便我们对程序进行优化。

#include <libkern/OSAtomic.h>
#include <execinfo.h>
#include <stdatomic.h>

// 异常名称key
NSString * const XJUncaughtExceptionHandlerSignalExceptionName = @"XJUncaughtExceptionHandlerSignalExceptionName";
// 异常原因key
NSString * const XJUncaughtExceptionHandlerSignalExceptionReason = @"XJUncaughtExceptionHandlerSignalExceptionReason";
// 精简的函数调用栈key
NSString * const XJUncaughtExceptionHandlerAddressesKey = @"XJUncaughtExceptionHandlerAddressesKey";
// 异常文件key
NSString * const XJUncaughtExceptionHandlerFileKey = @"XJUncaughtExceptionHandlerFileKey";
// 异常符号key
NSString * const XJUncaughtExceptionHandlerCallStackSymbolsKey = @"XJUncaughtExceptionHandlerCallStackSymbolsKey";

atomic_int      XJUncaughtExceptionCount = 0;
const int32_t   XJUncaughtExceptionMaximum = 8;
const NSInteger XJUncaughtExceptionHandlerSkipAddressCount = 4;
const NSInteger XJUncaughtExceptionHandlerReportAddressCount = 5;

// 保存原先的handler
NSUncaughtExceptionHandler *originalUncaughtExceptionHandler = NULL;

@implementation XJUncaughtExceptionHandler

/// exception回调,由_objc_terminate调用
void XJExceptionHandlers(NSException *exception) {
    NSLog(@"%s",__func__);

    int32_t exceptionCount = atomic_fetch_add_explicit(&XJUncaughtExceptionCount,1,memory_order_relaxed);
    if (exceptionCount > XJUncaughtExceptionMaximum) {
        return;
    }
    // 获取堆栈信息
    NSArray *callStack = [XJUncaughtExceptionHandler xj_backtrace];
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
    [userInfo setObject:exception.name forKey:XJUncaughtExceptionHandlerSignalExceptionName];
    [userInfo setObject:exception.reason forKey:XJUncaughtExceptionHandlerSignalExceptionReason];
    [userInfo setObject:callStack forKey:XJUncaughtExceptionHandlerAddressesKey];
    [userInfo setObject:exception.callStackSymbols forKey:XJUncaughtExceptionHandlerCallStackSymbolsKey];
    [userInfo setObject:@"XJException" forKey:XJUncaughtExceptionHandlerFileKey];
    
    [[[XJUncaughtExceptionHandler alloc] init]
     performSelectorOnMainThread:@selector(xj_handleException:)
     withObject:
     [NSException
      exceptionWithName:[exception name]
      reason:[exception reason]
      userInfo:userInfo]
     waitUntilDone:YES];

    // 自定义处理完成之后,调用原先的
    if (originalUncaughtExceptionHandler) {
        originalUncaughtExceptionHandler(exception);
    }
}

+ (void)installUncaughtSignalExceptionHandler {
    // 可以通过 NSGetUncaughtExceptionHandler 先保存旧的,然后赋值自己新的。
    if (NSGetUncaughtExceptionHandler() != XJExceptionHandlers) {
        originalUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    }

    //XJExceptionHandlers 赋值给 uncaught_handler(),最终_objc_terminate 调用 XJExceptionHandlers
    //NSSetUncaughtExceptionHandler 是 objc_setUncaughtExceptionHandler()的上层实现
    NSSetUncaughtExceptionHandler(&XJExceptionHandlers);
}

+ (void)removeRegister:(NSException *)exception {
    NSSetUncaughtExceptionHandler(NULL);
    [exception raise];
}

- (void)xj_handleException:(NSException *)exception{
    // 保存奔溃信息或者上传
    NSDictionary *userinfo = [exception userInfo];
    [self saveCrash:exception file:[userinfo objectForKey:XJUncaughtExceptionHandlerFileKey]];
    
    // UI提示相关操作
    // 如果要做 UI 相关提示需要写runloop相关的代码
    
    // 移除注册
    [XJUncaughtExceptionHandler removeRegister:exception];
}

/// 保存奔溃信息或者上传
- (void)saveCrash:(NSException *)exception file:(NSString *)file{
    NSArray *stackArray = [[exception userInfo] objectForKey:XJUncaughtExceptionHandlerCallStackSymbolsKey];// 异常的堆栈信息
    NSString *reason = [exception reason];// 出现异常的原因
    NSString *name = [exception name];// 异常名称
    // 可以直接用代码,输入这个崩溃信息,以便在console中进一步分析错误原因
    // NSLog(@"crash: %@", exception);
    NSString * _libPath  = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:file];
    if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){
        [[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil];
    }
    NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0];
    NSTimeInterval interval = [date timeIntervalSince1970];
    NSString *timeString = [NSString stringWithFormat:@"%f", interval];
    NSString * savePath = [_libPath stringByAppendingFormat:@"/error%@.log",timeString];
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
    BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"save crash log sucess:%d, path:%@",sucess,savePath);

    // 保存之后可以做上传相关操作

}

/// 获取函数堆栈信息
+ (NSArray *)xj_backtrace{
    void* callstack[128];
    int frames = backtrace(callstack, 128);//用于获取当前线程的函数调用堆栈,返回实际获取的指针个数
    char **strs = backtrace_symbols(callstack, frames);//从backtrace函数获取的信息转化为一个字符串数组
    int i;
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (i = XJUncaughtExceptionHandlerSkipAddressCount;
         i < XJUncaughtExceptionHandlerSkipAddressCount+XJUncaughtExceptionHandlerReportAddressCount;
         i++)
    {
        [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
    return backtrace;
}

@end
  • 通过NSGetUncaughtExceptionHandler函数保留之前的回调(其他SDK可能实现了),通过NSSetUncaughtExceptionHandler函数进行异常回调的注册。自定义处理完之后,调用之前的回调。还可以对NSSetUncaughtExceptionHandler函数进行hook,添加上自定义的处理。
  • 在处理逻辑中,可以根据业务需要进行本地保存或者上传服务器,然后做相关的UI提示(需要自己创建runloop),最后需要移除注册的回调。

备注:

此方法只能捕获objc抛出的异常,无法捕获signal异常,signal异常需要设置signalhandler。 自定义的NSSetUncaughtExceptionHandler尽量放到didFinishLaunchingWithOptions方法的最后面调用,防止被其他SDK覆盖。过多SDK调用NSSetUncaughtExceptionHandler有时会混乱,表现是捕获到的crash没有符号表。

1.5.2: signal捕获

signal捕获需要注册sigaction/signal对应的信号回调,注册后可以构建异常信息进行处理,流程和exception差不多。

signal简单实现

+ (void)registerSignalHandler {
    signal(SIGHUP, XJSignalHandler);
    signal(SIGINT, XJSignalHandler);
    signal(SIGQUIT, XJSignalHandler);
    signal(SIGABRT, XJSignalHandler);
    signal(SIGILL, XJSignalHandler);
    signal(SIGSEGV, XJSignalHandler);
    signal(SIGFPE, XJSignalHandler);
    signal(SIGBUS, XJSignalHandler);
    signal(SIGPIPE, XJSignalHandler);
}

// signal处理
void XJSignalHandler(int signal) {

    NSLog(@"%s",__func__);

    int32_t exceptionCount = atomic_fetch_add_explicit(&XJUncaughtExceptionCount,1,memory_order_relaxed);
    if (exceptionCount > XJUncaughtExceptionMaximum) {
        return;
    }

    NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithInt:signal] forKey:XJUncaughtExceptionHandlerSignalKey];
    NSArray *callStack = [XJUncaughtExceptionHandler xj_backtrace];
    [userInfo setObject:callStack forKey:XJUncaughtExceptionHandlerAddressesKey];
    [userInfo setObject:@"XJSignalCrash" forKey:XJUncaughtExceptionHandlerFileKey];
    [userInfo setObject:callStack forKey:XJUncaughtExceptionHandlerCallStackSymbolsKey];

    [[[XJUncaughtExceptionHandler alloc] init]
     performSelectorOnMainThread:@selector(xj_handleException:) withObject:
         [NSException
          exceptionWithName:XJUncaughtExceptionHandlerSignalExceptionName
          reason:[NSString stringWithFormat:NSLocalizedString(@"Signal %d was raised.\n %@", nil),signal, getAppInfo()]
          userInfo:userInfo]
         waitUntilDone:YES];
}

sigaction简单实现

+ (void)registerSigactionHandler {
    struct sigaction old_action;
    sigaction(SIGABRT, NULL, &old_action);
    if (old_action.sa_flags & SA_SIGINFO) {
        if (old_action.sa_sigaction != XJAbrtSignalHandler) {
            //保存之前注册的handler
            originalAbrtSignalHandler = old_action.sa_sigaction;
        }
    }

    struct sigaction action;
    action.sa_sigaction = XJAbrtSignalHandler;
    action.sa_flags = SA_NODEFER | SA_SIGINFO;
    sigemptyset(&action.sa_mask);
    sigaction(SIGABRT, &action, 0);
}

// 保存原先的abrt handler
void (*originalAbrtSignalHandler)(int, struct __siginfo *, void *);
static void XJAbrtSignalHandler(int signal, siginfo_t *info, void *context) {
    XJSignalHandler(signal);

    // 自定义处理完成之后,调用原先的
    if (signal == SIGABRT && originalAbrtSignalHandler) {
        originalAbrtSignalHandler(signal, info, context);
    }
}

sigactionsignal函数的区别:

  • signal在系统调用的基础上实现,是库函数。只有两个参数,不支持信号传递信息,主要是用于前32个非实时信号。
  • sigaction是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction)。有三个参数,支持信号传递信息,主要用来与sigqueue系统调用配合使用。sigaction同样支持非实时信号。
  • sigaction支持信号带有参数。signal使用简单,sigaction使用相对复杂一些。

测试代码:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    //exception错误
//    NSLog(@"%@",self.dataList[5]);

    // signal 错误
    void *singal = malloc(1024);
    free(singal);
    free(singal);//SIGABRT的错误
}

⚠️在debug模式下,signal应用会直接崩溃到主函数,想看log信息需要在lldb中进行配置,以SIGABRT为例,先断点输入pro hand -p true -s false SIGABRT后继续运行,再执行到crash逻辑。这样就能断住断点了:

image.png

最终crash.log文件存入了沙盒,通过Xcode->Devices and Simulators选择运行时使用的设备和相关APP下载沙盒文件,右键选中显示包内容,然后根据控制台输出的路径就可以找到相关的.log文件了。

image.png

image.png

demo地址

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

1.7: _imp_implementationWithBlock_init函数

启动回调机制。通常情况下,这不起任何作用,因为一切都是惰性地初始化的,但是对于某些进程,我们急切地加载libobjc-trampolines.dylib动态库。

/// Initialize the trampoline machinery. Normally this does nothing, as
/// everything is initialized lazily, but for certain processes we eagerly load
/// the trampolines dylib.
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
}

1.8: _dyld_objc_notify_register

_dyld_objc_notify_register函数注册了map_imagesload_imagesunmap_image三个回调。相关实现在dyld中。

  • map_images:管理由dyld映射的给定镜像文件的所有符号(classProtocolselectorcategory)。map_images加了&符号,表示指针传递,因为这个函数负责类的加载,非常重要,需要跟着dyld里面的函数一起变化,防止可能发生的错乱。
  • load_images:执行由dyld映射的给定镜像文件的+load方法。
  • unmap_image:处理即将被dyld取消映射的给定镜像文件.

2: 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);
}
  • 调用map_images_nolock函数。

2.1: map_images_nolock函数

进入map_images_nolock函数查看,发现最重要的代码就是调用了_read_images函数。

image.png

  • 核心逻辑在_read_images函数中。

3: _read_images函数分析

_read_images函数有360多行代码,老规矩将所有{}先折叠起来查看主体流程,发现苹果已经给我们提供了相关注释。

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提示整理出主要流程模块:

  1. 条件控制,只进行一次加载。(doneOnce)。
  2. 修复预编译阶段的@selector的混乱问题。
  3. 发现类。修复错误混乱的类。标记捆绑类。
  4. 修复重映射一些没有被镜像文件加载进来的类 。
  5. 修复一些消息。
  6. 当类里面有协议的时候:readProtocol
  7. 修复没有被加载的协议。
  8. 分类处理。
  9. 类的加载处理。
  10. 没有被处理的类,优化那些被侵犯的类。

很显然89是核心。也就是load_categories_nolock函数与realizeClassWithoutSwift函数。下面就根据日志的提示重点的逐一分析。

3.1: doneOnce

doneOnce核心代码如下:

    // 条件控制,只进行一次加载
    if (!doneOnce) {
        doneOnce = YES;
        launchTime = YES;
        
        ...
        
        // 小对象类型的处理,比如NSNumber等
        initializeTaggedPointerObfuscator();
        
        ...
        
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
        // 总容量:x * 4 / 3
        // 添加的时候3 / 4扩容:x * 4 / 3 * 3 / 4,已用容量不能大于总容量
        // 相当于负载因子 3 / 4的逆过程
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        // 创建存放所有类的哈希表,不管是实现了,还是没有实现
        // gdb_objc_realized_classes表 与runtime_init两张表的(unattachedCategories allocatedClasses)的区别
        // gdb_objc_realized_classes是总表,allocatedClasses是已经实现的类的表。是包含关系。
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
        ts.log("IMAGE TIMES: first time tasks");
    }
  • 进入一次后doneOnce = YES,后续不满足判断条件,不会再进入。
  • 小对象类型的处理,比如NSNumber等。
  • 创建表gdb_objc_realized_classes,用于存放所有的类,不管是实现了的,还是没有实现的。
  • 表的负载因子是3 / 4,总容量为x * 4 / 3,添加的时候3 / 4扩容:x * 4 / 3 * 3 / 4,已用容量不能大于总容量。
  • gdb_objc_realized_classes是总表,runtime_init初始化的unattachedCategoriesallocatedClasses表中,allocatedClasses表是已经实现的类的表。

gdb_objc_realized_classes定义

// This is a misnomer: gdb_objc_realized_classes is actually a list of 
// named classes not in the dyld shared cache, whether realized or not.
// This list excludes lazily named classes, which have to be looked up
// using a getClass hook.
NXMapTable *gdb_objc_realized_classes;  // exported for debuggers in objc-gdb.h

allocatedClasses定义

/***********************************************************************
* allocatedClasses
* A table of all classes (and metaclasses) which have been allocated
* with objc_allocateClassPair.
**********************************************************************/
namespace objc {
static ExplicitInitDenseSet<Class> allocatedClasses;
}
  • gdb_objc_realized_classes是一张总表,无论类是否实现。

  • allocatedClasses包含的是所有allocated的类和元类。

  • 所以gdb_objc_realized_classes应该包含allocatedClasses

doneOnce流程的作用就是创建类的总表。

3.2: fix up selector references

核心代码如下:

    static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->hasPreoptimizedSelectors()) continue;

            bool isBundle = hi->isBundle();
            // 从macho文件中名为__objc_selrefs的section获取sel列表
            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是由名称和地址组成的,不同的类可能有相同的方法,但是就算名称相同,因为所属不同的类,所以地址就会不相同,这里就修复预编译阶段的@selector的混乱问题。

运行源码,打上断点验证:

image.png

  • 名称相同,地址不同,以dyld获取的sel的地址为准进行修复。

3.3: discover classes

核心代码如下:

for (EACH_HEADER) {
    ……
    // 从macho文件名为__objc_classlist的section获取classlist
    // 现在存的只是地址
    classref_t const *classlist = _getObjc2ClassList(hi, &count);
   ……

    for (i = 0; i < count; i++) {
        // 取出对应位置的class
        Class cls = (Class)classlist[i];
        // 地址与类进行关联,并插入两张类表
        Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

        if (newCls != cls  &&  newCls) {
            // Class was moved but not deleted. Currently this occurs 
            // only when the new class resolved a future class.
            // Non-lazily realize the class below.
            // 类已移动但未删除。当前,仅当新类解析了将来的类时,才会发生这种情况
            // 非懒加载的实现下面的类
            
            // 开辟内存
            resolvedFutureClasses = (Class *)
                realloc(resolvedFutureClasses, 
                        (resolvedFutureClassCount+1) * sizeof(Class));
            // 添加到resolvedFutureClasses列表里
            resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
        }
    }
}
  • macho文件名为__objc_classlistsection获取classlist,现在存的只是地址。
  • 调用readClass函数将地址与类进行了关联。
  • 如果有类发生了混乱(类已移动但未删除),就添加到resolvedFutureClasses列表中,然后在3.10流程中非懒加载的加载。

readClass处添加断点,运行源码验证:

image.png

  • 调用readClass函数后将地址与类进行了关联。

3.3.1: readClass

接下来分析下readClass函数。

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    // 获取类名
    const char *mangledName = cls->nonlazyMangledName();
    if (missingWeakSuperclass(cls)) { ... }
    cls->fixupBackwardDeployingStableSwift();
    Class replacing = nil;

    if (mangledName != nullptr) { ... }

    if (headerIsPreoptimized  &&  !replacing) {...
    } else {
        if (mangledName) { 
        //some Swift generic classes can lazily generate their names
            // 将类名和地址关联起来
            // 更新`gdb_objc_realized_classes`哈希表,`key`是`name`,`value`是`cls`
            addNamedClass(cls, mangledName, replacing);
        } else { ...}
        // 将关联的类插入到哈希表allocatedClasses中
        addClassTableEntry(cls);
    }
    // for future reference: shared cache never contains MH_BUNDLEs
    if (headerIsBundle) { ... }
    return cls;

}
  • 调用nonlazyMangledName函数获取类名。

    nonlazyMangledName函数

    const char *nonlazyMangledName() const {
        return bits.safe_ro()->getName();
    }
    

    safe_ro函数

    const class_ro_t *safe_ro() const {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            // maybe_rw is rw
            // rw有值 直接从rw中的ro获取
            return maybe_rw->ro();
        } else 
            // maybe_rw is actually ro
            // 直接从ro中获取,ro是macho中的数据
            return (class_ro_t *)maybe_rw;、
        }
    }
    
  • rwroisasuperclass等并不是在readClass函数里处理的。

    image.png

    • 打上断点运行源码,可以发现不会进入如图所示的代码区。
  • 调用addNamedClass函数将类名和地址关联起来。

    addNamedClass函数

    static void addNamedClass(Class cls, const char *name, Class replacing = nil)
    {
        runtimeLock.assertLocked();
        Class old;
        if ((old = getClassExceptSomeSwift(name))  &&  old != replacing) {
            inform_duplicate(name, old, cls);
            // getMaybeUnrealizedNonMetaClass uses name lookups.
            // Classes not found by name lookup must be in the
            // secondary meta->nonmeta table.
            addNonMetaClass(cls);
        } else {
            // 更新gdb_objc_realized_classes表,将key设置为 name value 设置为cls
            NXMapInsert(gdb_objc_realized_classes, name, cls);
        }
        ASSERT(!(cls->data()->flags & RO_META));
        // wrong: constructed classes are already realized when they get here
        // ASSERT(!cls->isRealized());
    }
    
    typedef struct _MapPair {
        const void  *key;
        const void  *value;
    } MapPair;
    
    • namekeyclsvalue生成MapPair,调用NXMapInsert函数插入gdb_objc_realized_classes哈希表。
  • 调用addClassTableEntry函数将关联的类插入到哈希表allocatedClasses中。

    addClassTableEntry函数

    static void
    addClassTableEntry(Class cls, bool addMeta = true)
    {
        runtimeLock.assertLocked();
        // This class is allowed to be a known class via the shared cache or via
        // data segments, but it is not allowed to be in the dynamic 
        //table already.
        // allocatedClasses
        auto &set = objc::allocatedClasses.get();
        ASSERT(set.find(cls) == set.end());
        if (!isKnownClass(cls))
            // 将类插入allocatedClasses表中
            set.insert(cls);
        if (addMeta)
            // 将元类插入allocatedClasses表中
            addClassTableEntry(cls->ISA(), false);
    }
    
    • allocatedClasses表是runtime_init函数中初始化的,将类和元类都插入allocatedClasses表中。

3.4: remap classes

if (!noClassesRemapped()) {
    for (EACH_HEADER) {
        Class *classrefs = _getObjc2ClassRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapClassRef(&classrefs[i]);
        }
        // fixme why doesn't test future1 catch the absence of this?
        classrefs = _getObjc2SuperRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapClassRef(&classrefs[i]);
        }
    }
}
  • 通过noClassesRemapped函数判断是否有类需要进行重映射。
  • 如有需要重新映射的类,读取macho文件中名为__objc_classrefs__objc_superrefssection,然后调用remapClassRef函数进行重映射。

3.5: objc_msgSend_fixup

#if SUPPORT_FIXUP
    for (EACH_HEADER) {
        // 从macho文件中名为__objc_msgrefs的section读取msg列表
        message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
        if (count == 0) continue;

        if (PrintVtables) {
            _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
                         "call sites in %s", count, hi->fname());
        }
        for (i = 0; i < count; i++) {
            //修复旧的vtable。比如alloc的imp改为直接调用objc_alloc,而不是走alloc方法的实现。
            fixupMessageRef(refs+i);
        }
    }
    
#endif
// message_ref_t定义
struct message_ref_t {
    IMP imp;
    SEL sel;
};
  • macho文件中名为__objc_msgrefssection读取msg列表。

  • 调用fixupMessageRef函数进行msg的修复,比如alloc方法的imp将被替换为objc_alloc函数。正常情况下不会走此流程,因为在llvm阶段已经进行相关的处理了。

    fixupMessageRef函数

    static void 
    fixupMessageRef(message_ref_t *msg)
    {    
        msg->sel = sel_registerName((const char *)msg->sel);
        if (msg->imp == &objc_msgSend_fixup) {
            if (msg->sel == @selector(alloc)) {
                //alloc替换为objc_alloc
                msg->imp = (IMP)&objc_alloc;
            } else if (msg->sel == @selector(allocWithZone:)) {
                msg->imp = (IMP)&objc_allocWithZone;
            } else if (msg->sel == @selector(retain)) {
                msg->imp = (IMP)&objc_retain;
            } else if (msg->sel == @selector(release)) {
                msg->imp = (IMP)&objc_release;
            } else if (msg->sel == @selector(autorelease)) {
                msg->imp = (IMP)&objc_autorelease;
            } else {
                msg->imp = &objc_msgSend_fixedup;
            }
        } 
        else if (msg->imp == &objc_msgSendSuper2_fixup) { 
            msg->imp = &objc_msgSendSuper2_fixedup;
        } 
        else if (msg->imp == &objc_msgSend_stret_fixup) { 
            msg->imp = &objc_msgSend_stret_fixedup;
        } 
        else if (msg->imp == &objc_msgSendSuper2_stret_fixup) { 
            msg->imp = &objc_msgSendSuper2_stret_fixedup;
        } 
    #if defined(__i386__)  ||  defined(__x86_64__)
        else if (msg->imp == &objc_msgSend_fpret_fixup) { 
            msg->imp = &objc_msgSend_fpret_fixedup;
        } 
    #endif
    #if defined(__x86_64__)
        else if (msg->imp == &objc_msgSend_fp2ret_fixup) { 
            msg->imp = &objc_msgSend_fp2ret_fixedup;
        } 
    #endif
    }
    

3.6: discover protocols

    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        ASSERT(cls);
        //创建表(只创建一次)
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->hasPreoptimizedProtocols();

        ……

        bool isBundle = hi->isBundle();
        // 从macho文件名为__objc_protolist的section中获取protolist
        protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
        // 循环调用readProtocol函数将protocol插入protocol_map表中
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }
  • 创建protocol_map表。protocol_map是一个NXMapTable,是通过NXCreateMapTable函数创建的。可以通过protocols()函数获取。
  • macho文件名为__objc_protolistsection中获取protolist
  • 循环调用readProtocol函数将protocol插入protocol_map表中。

根据代码看来与discover classes流程差不多,是将protocol插入相应的表中,核心逻辑应该在readProtocol中。

3.6.1: readProtocol

定义一个protocol,遵守并实现相关方法。

@protocol XJProtocol <NSObject>

- (void)protocolInstanceMethod;

+ (void)protocolClassMethod;

@end
static void
readProtocol(protocol_t *newproto, Class protocol_class,
             NXMapTable *protocol_map, 
             bool headerIsPreoptimized, bool headerIsBundle)
{
    // This is not enough to make protocols in unloaded bundles safe, 
    // but it does prevent crashes when looking up unrelated protocols.
    // NXMapKeyCopyingInsert:卸载bundle时使用,一般都是NXMapInsert
    auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;

    // 看看是否会有重复的protocol
    protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);

    // 调试代码 开始
    // 只看自定义的ptorocol
    if (strcmp(newproto->mangledName, "XJProtocol") == 0) {
        printf("%s %s\n",__func__,newproto->mangledName);
    }
    // 调试代码 结束

    if (oldproto) { // protocol 重复
        ...
    }
    else if (headerIsPreoptimized) { // 共享缓存
    
        ...
    }
    else {
        // New protocol from an un-preoptimized image. Fix it up in place.
        // fixme duplicate protocols from unloadable bundle
        // 关联isa
        newproto->initIsa(protocol_class);  // fixme pinned
        // 调用NXMapInsert函数,以newproto->mangledName为key,newproto为value
        // 插入protocol_map表中
        insertFn(protocol_map, newproto->mangledName, newproto);
        ...
    }
  • initIsa函数现在不会走,因为现在只是在加载类,还没有处理类与协议之间的关系。
  • newproto->mangledNamekeynewprotovalue生成MapPair,调用insertFn函数(这里就是NXMapInsert函数,readClass调用的也是此函数)插入protocol_map表中。

3.7: Fix up @protocol references

    for (EACH_HEADER) {

        if (launchTime && hi->isPreoptimized())
            continue;
        // 从macho文件名为__objc_protorefs的section读取protocol列表
        protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
        // 循环调用remapProtocolRef函数对protocol进行重映射
        for (i = 0; i < count; i++) {
            // 重映射protocol
            remapProtocolRef(&protolist[i]);
        }
    }
  • macho文件中名为__objc_protorefssection读取需要重映射的protocol列表(discover protocols是名为__objc_protolistsection)。
  • 循环调用remapProtocolRef函数对protocol进行重映射(自定义的协议不会走这里)。

3.8: Discover categories

// 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. rdar://problem/53119145
if (didInitialAttachCategories) {
    for (EACH_HEADER) {
        load_categories_nolock(hi);
    }
}

注释已经说明不会进入此流程,即使分类实现了+load方法,此时也不会进入。分类的加载必须在load_images之后。具体流程后续发文分析。

3.9: Realize non-lazy classes

非懒加载类的处理(实现了+load方法或者静态实例方法的类为非懒加载类,反之为懒加载类)。

// +load handled by prepare_load_methods()

// Realize non-lazy classes (for +load methods and static instances)
for (EACH_HEADER) {
    // 从macho文件中名为__objc_nlclslist的section获取no-lazy classes
    classref_t const *classlist = hi->nlclslist(&count);
    for (i = 0; i < count; i++) {
        // 重新映射类
        Class cls = remapClass(classlist[i]);
        if (!cls) continue;
        
        //调试代码 开始
        const char *mangledName = cls->nonlazyMangledName();
        if (strcmp(mangledName, "XJObject") == 0) {
            printf("%s %s\n",__func__,mangledName);
        }
        printf("%s %s\n",__func__,mangledName);
        //调试代码 结束
        
        // 加入allocatedClasses表中,已经加入不会再次加入。
        addClassTableEntry(cls);
    ……
        // 加载类,并递归加载父类和元类
        realizeClassWithoutSwift(cls, nil);
    }
}
  • macho文件中名为__objc_nlclslistsection获取非懒加载类(no-lazy classes)。
  • 循环调用remapClass函数重新映射类。
  • 调用addClassTableEntry函数将类加入allocatedClasses表中,已经加入不会再次加入。
  • 调用realizeClassWithoutSwift函数加载类,并递归加载父类和元类(此函数在之前的消息慢速查找流程就已经遇到过了)。

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

  1. 自己实现了+load方法。(会出现在__objc_nlclslist中)
  2. 子类实现了+load方法。(因为子类加载会加载父类,不会出现在__objc_nlclslist中)
  3. 分类实现了+load方法。(这里包括自己的分类以及子类的分类,不是在map_images流程而是在prepare_load_methods中实例化类的,不会出现在__objc_nlclslist中)

⚠️尽量避免在+load方法中进行逻辑处理,因为整个过程是一个连锁反映,会导致程序启动变慢。

实现了+load方法的类就会出现在macho文件名为__objc_nlclslistsection中了。

image.png

为什么有懒加载类和非懒加载类的区别呢?

为了按需加载,因为在程序启动过程中,加载的类越少,程序启动自然也就越快。

通过上面流程已经清楚了非懒加载类的加载流程,那么懒加载类的加载流程是怎样的呢?

既然要加载类肯定需要调用realizeClassWithoutSwift函数,那么在此函数加上断点,去掉+load方法,并分别调用对象方法和类方法,查看函数调用栈。

对象方法调用栈:

image.png

备注:笔者使用的是M1 版 iMac,所以alloc方法不会被替换为objc_alloc函数。

类方法调用栈:

image.png

从函数调用栈可以看出不管是对象方法,还是类方法,都是在消息慢速查找流程时调用realizeClassWithoutSwift函数进行加载的。所以就说明了懒加载类是在第一次调用方法时进行加载的。

  • 非懒加载类实现了+load方法(自己/子类/分类),类就会在程序启动时提前加载,为+load方法的调用做准备。
  • 懒加载类是在第一次调用方法(消息发送objc_msgSend),进行消息慢速查找(lookUpImpOrForward)流程时进行加载的。

_objc_init和map_images核心逻辑.png 懒加载和非懒加载类加载流程对比:

  • 懒加载类:加载推迟到第一次发送消息的时候。

    • lookUpImpOrForward
    • realizeAndInitializeIfNeeded_locked
    • realizeClassMaybeSwiftAndLeaveLocked
    • realizeClassMaybeSwiftMaybeRelock
    • realizeClassWithoutSwift
    • methodizeClass
  • 非懒加载类:map_images时加载。

    • readClass
    • _getObjc2NonlazyClassList
    • realizeClassWithoutSwift
    • methodizeClass

类的加载的核心逻辑在realizeClassWithoutSwift函数中,相关流程后续发文详细分析。

3.10: realize future classes

    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);
    }
  • 3.3流程发现的混乱的类进行加载。正常情况下不会进入此流程。

总结

_objc_initmap_images核心逻辑以思维导图的形式展示如下。

_objc_init和map_images核心逻辑.png