iOS 类加载流程分析(上)

821 阅读16分钟

前言

  上篇文章我们已经分析了应用程序的加载流程,对于应用程序的加载流程我们已经很熟悉了,我们已经清楚的明白了在ObjC源码中_objc_init函数中调用dyld库的函数_dyld_objc_notify_register时所传入的两个函数map_images以及load_images的调用流程,load_images其实就是调用了OC类及其分类中重写的load类方法,但是map_images这个函数的调用做了些什么呢?我们目前并不清楚,因此接下来的重点就是要去探究map_images这个函数中的代码逻辑,让我们一起来看看吧!

本节重点

ObjC环境变量的打印以及使用

ObjC异常处理

read_Images函数流程探究

1. _objc_init函数代码解析

  在探究map_images函数代码逻辑之前,首先来探究一下_objc_init中的函数调用都做了些什么吧,说不定与map_images函数调用有关,其代码如下所示:

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

  _objc_init函数中首先定义了一个局部的静态变量,如果初始化过,下次再次调用_objc_init这个函数时就会直接返回,保证_objc_init函数中调用的其他函数只会被执行一次。

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 (issetugid()) {
        // All environment variables are silently ignored when setuid or setgid
        // This includes OBJC_HELP and OBJC_PRINT_OPTIONS themselves.
        return;
    } 

    // Turn off autorelease LRU coalescing by default for apps linked against
    // older SDKs. LRU coalescing can reorder releases and certain older apps
    // are accidentally relying on the ordering.
    // rdar://problem/63886091
//    if (!dyld_program_sdk_at_least(dyld_fall_2020_os_versions))
//        DisableAutoreleaseCoalescingLRU = true;

    bool PrintHelp = false;
    bool PrintOptions = false;
    bool maybeMallocDebugging = false;

    // Scan environ[] directly instead of calling getenv() a lot.
    // This optimizes the case where none are set.
    for (char **p = *_NSGetEnviron(); *p != nil; p++) {
        if (0 == strncmp(*p, "Malloc", 6)  ||  0 == strncmp(*p, "DYLD", 4)  ||  
            0 == strncmp(*p, "NSZombiesEnabled", 16))
        {
            maybeMallocDebugging = true;
        }

        if (0 != strncmp(*p, "OBJC_", 5)) continue;
        
        if (0 == strncmp(*p, "OBJC_HELP=", 10)) {
            PrintHelp = true;
            continue;
        }
        if (0 == strncmp(*p, "OBJC_PRINT_OPTIONS=", 19)) {
            PrintOptions = true;
            continue;
        }
        
        if (0 == strncmp(*p, "OBJC_DEBUG_POOL_DEPTH=", 22)) {
            SetPageCountWarning(*p + 22);
            continue;
        }

        const char *value = strchr(*p, '=');
        if (!*value) continue;
        value++;
        
        for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
            const option_t *opt = &Settings[i];
            if ((size_t)(value - *p) == 1+opt->envlen  &&  
                0 == strncmp(*p, opt->env, opt->envlen))
            {
                *opt->var = (0 == strcmp(value, "YES"));
                break;
            }
        }
    }

    // Special case: enable some autorelease pool debugging
    // when some malloc debugging is enabled 
    // and OBJC_DEBUG_POOL_ALLOCATION is not set to something other than NO.
    if (maybeMallocDebugging) {
        const char *insert = getenv("DYLD_INSERT_LIBRARIES");
        const char *zombie = getenv("NSZombiesEnabled");
        const char *pooldebug = getenv("OBJC_DEBUG_POOL_ALLOCATION");
        if ((getenv("MallocStackLogging")
             || getenv("MallocStackLoggingNoCompact")
             || (zombie && (*zombie == 'Y' || *zombie == 'y'))
             || (insert && strstr(insert, "libgmalloc")))
            &&
            (!pooldebug || 0 == strcmp(pooldebug, "YES")))
        {
            DebugPoolAllocation = true;
        }
    }

//    if (!os_feature_enabled_simple(objc4, preoptimizedCaches, true)) {
//        DisablePreoptCaches = true;
//    }

    // Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
    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);
        }
    }
}

  根据注释信息我们可知这个函数是用来初始化环境变量的,可以读取影响运行时的环境变量,如果发送相关请求,也可以打印环境变量帮助,但是我们并不知道如何发送这些请求,此时就可以将下面的代码段(也就是最后一个for循环的代码复制出来将其中的if判断语句去除后的代码)复制到if语句外面,如下图所示:

image.png

  编译运行ObjC源码,就可以看到如下的打印信息:

objc[9899]: OBJC_PRINT_IMAGES: log image and library names as they are loaded
objc[9899]: OBJC_PRINT_IMAGES is set
objc[9899]: OBJC_PRINT_IMAGE_TIMES: measure duration of image loading steps
objc[9899]: OBJC_PRINT_IMAGE_TIMES is set
objc[9899]: OBJC_PRINT_LOAD_METHODS: log calls to class and category +load methods
objc[9899]: OBJC_PRINT_LOAD_METHODS is set
objc[9899]: OBJC_PRINT_INITIALIZE_METHODS: log calls to class +initialize methods
objc[9899]: OBJC_PRINT_INITIALIZE_METHODS is set
objc[9899]: OBJC_PRINT_RESOLVED_METHODS: log methods created by +resolveClassMethod: and +resolveInstanceMethod:
objc[9899]: OBJC_PRINT_RESOLVED_METHODS is set
objc[9899]: OBJC_PRINT_CLASS_SETUP: log progress of class and category setup
objc[9899]: OBJC_PRINT_CLASS_SETUP is set
objc[9899]: OBJC_PRINT_PROTOCOL_SETUP: log progress of protocol setup
objc[9899]: OBJC_PRINT_PROTOCOL_SETUP is set
objc[9899]: OBJC_PRINT_IVAR_SETUP: log processing of non-fragile ivars
objc[9899]: OBJC_PRINT_IVAR_SETUP is set
objc[9899]: OBJC_PRINT_VTABLE_SETUP: log processing of class vtables
objc[9899]: OBJC_PRINT_VTABLE_SETUP is set
objc[9899]: OBJC_PRINT_VTABLE_IMAGES: print vtable images showing overridden methods
objc[9899]: OBJC_PRINT_VTABLE_IMAGES is set
objc[9899]: OBJC_PRINT_CACHE_SETUP: log processing of method caches
objc[9899]: OBJC_PRINT_CACHE_SETUP is set
objc[9899]: OBJC_PRINT_FUTURE_CLASSES: log use of future classes for toll-free bridging
objc[9899]: OBJC_PRINT_FUTURE_CLASSES is set
objc[9899]: OBJC_PRINT_PREOPTIMIZATION: log preoptimization courtesy of dyld shared cache
objc[9899]: OBJC_PRINT_PREOPTIMIZATION is set
objc[9899]: OBJC_PRINT_CXX_CTORS: log calls to C++ ctors and dtors for instance variables
objc[9899]: OBJC_PRINT_CXX_CTORS is set
objc[9899]: OBJC_PRINT_EXCEPTIONS: log exception handling
objc[9899]: OBJC_PRINT_EXCEPTIONS is set
objc[9899]: OBJC_PRINT_EXCEPTION_THROW: log backtrace of every objc_exception_throw()
objc[9899]: OBJC_PRINT_EXCEPTION_THROW is set
objc[9899]: OBJC_PRINT_ALT_HANDLERS: log processing of exception alt handlers
objc[9899]: OBJC_PRINT_ALT_HANDLERS is set
objc[9899]: OBJC_PRINT_REPLACED_METHODS: log methods replaced by category implementations
objc[9899]: OBJC_PRINT_REPLACED_METHODS is set
objc[9899]: OBJC_PRINT_DEPRECATION_WARNINGS: warn about calls to deprecated runtime functions
objc[9899]: OBJC_PRINT_DEPRECATION_WARNINGS is set
objc[9899]: OBJC_PRINT_POOL_HIGHWATER: log high-water marks for autorelease pools
objc[9899]: OBJC_PRINT_POOL_HIGHWATER is set
objc[9899]: OBJC_PRINT_CUSTOM_CORE: log classes with custom core methods
objc[9899]: OBJC_PRINT_CUSTOM_CORE is set
objc[9899]: OBJC_PRINT_CUSTOM_RR: log classes with custom retain/release methods
objc[9899]: OBJC_PRINT_CUSTOM_RR is set
objc[9899]: OBJC_PRINT_CUSTOM_AWZ: log classes with custom allocWithZone methods
objc[9899]: OBJC_PRINT_CUSTOM_AWZ is set
objc[9899]: OBJC_PRINT_RAW_ISA: log classes that require raw pointer isa fields
objc[9899]: OBJC_PRINT_RAW_ISA is set
objc[9899]: OBJC_DEBUG_UNLOAD: warn about poorly-behaving bundles when unloaded
objc[9899]: OBJC_DEBUG_UNLOAD is set
objc[9899]: OBJC_DEBUG_FRAGILE_SUPERCLASSES: warn about subclasses that may have been broken by subsequent changes to superclasses
objc[9899]: OBJC_DEBUG_FRAGILE_SUPERCLASSES is set
objc[9899]: OBJC_DEBUG_NIL_SYNC: warn about @synchronized(nil), which does no synchronization
objc[9899]: OBJC_DEBUG_NIL_SYNC is set
objc[9899]: OBJC_DEBUG_NONFRAGILE_IVARS: capriciously rearrange non-fragile ivars
objc[9899]: OBJC_DEBUG_NONFRAGILE_IVARS is set
objc[9899]: OBJC_DEBUG_ALT_HANDLERS: record more info about bad alt handler use
objc[9899]: OBJC_DEBUG_ALT_HANDLERS is set
objc[9899]: OBJC_DEBUG_MISSING_POOLS: warn about autorelease with no pool in place, which may be a leak
objc[9899]: OBJC_DEBUG_MISSING_POOLS is set
objc[9899]: OBJC_DEBUG_POOL_ALLOCATION: halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
objc[9899]: OBJC_DEBUG_POOL_ALLOCATION is set
objc[9899]: OBJC_DEBUG_DUPLICATE_CLASSES: halt when multiple classes with the same name are present
objc[9899]: OBJC_DEBUG_DUPLICATE_CLASSES is set
objc[9899]: OBJC_DEBUG_DONT_CRASH: halt the process by exiting instead of crashing
objc[9899]: OBJC_DEBUG_DONT_CRASH is set
objc[9899]: OBJC_DEBUG_POOL_DEPTH: log fault when at least a set number of autorelease pages has been allocated
objc[9899]: OBJC_DEBUG_POOL_DEPTH is set
objc[9899]: OBJC_DISABLE_VTABLES: disable vtable dispatch
objc[9899]: OBJC_DISABLE_VTABLES is set
objc[9899]: OBJC_DISABLE_PREOPTIMIZATION: disable preoptimization courtesy of dyld shared cache
objc[9899]: OBJC_DISABLE_PREOPTIMIZATION is set
objc[9899]: OBJC_DISABLE_TAGGED_POINTERS: disable tagged pointer optimization of NSNumber et al.
objc[9899]: OBJC_DISABLE_TAGGED_POINTERS is set
objc[9899]: OBJC_DISABLE_TAG_OBFUSCATION: disable obfuscation of tagged pointers
objc[9899]: OBJC_DISABLE_TAG_OBFUSCATION is set
objc[9899]: OBJC_DISABLE_NONPOINTER_ISA: disable non-pointer isa fields
objc[9899]: OBJC_DISABLE_NONPOINTER_ISA is set
objc[9899]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY: disable safety checks for +initialize after fork
objc[9899]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY is set
objc[9899]: OBJC_DISABLE_FAULTS: disable os faults
objc[9899]: OBJC_DISABLE_FAULTS is set
objc[9899]: OBJC_DISABLE_PREOPTIMIZED_CACHES: disable preoptimized caches
objc[9899]: OBJC_DISABLE_PREOPTIMIZED_CACHES is set
objc[9899]: OBJC_DISABLE_AUTORELEASE_COALESCING: disable coalescing of autorelease pool pointers
objc[9899]: OBJC_DISABLE_AUTORELEASE_COALESCING is set
objc[9899]: OBJC_DISABLE_AUTORELEASE_COALESCING_LRU: disable coalescing of autorelease pool pointers using look back N strategy
objc[9899]: OBJC_DISABLE_AUTORELEASE_COALESCING_LRU is set

  通过打印我们可以看到这些环境变量的名字,如果你在主工程中设置这些环境变量就可以根据提示获取到相关信息或者改变某些数据结构,例如:我们随意创建一个iOS工程,其中创建一个继承自NSObjectPerson类,实现Person类中的类方法load,并设置2个环境变量OBJC_DISABLE_NONPOINTER_ISA(是否禁用NONPOINTER_ISA)设置为NOOBJC_PRINT_LOAD_METHODS(是否打印输出LOAD方法调用时的信息)设置为NO,如下图所示:

image.png

  在ViewControllerViewDidLoad方法中编写代码并打上断点,链接真机,运行程序,打印p对象的isa,发现此isaNONPOINTER_ISA,过掉断点,程序崩溃,如下图所示:

image.png

  将OBJC_DISABLE_NONPOINTER_ISA以及OBJC_PRINT_LOAD_METHODS设置为YES,如下图所示:

image.png

  编译运行程序,打印输出p对象的isa,如下图所示:

image.png

  OBJC_PRINT_LOAD_METHODS这个环境变量可以输出所有调用load类方法的类信息,帮助我们排查主程序以及framework中调用load的类都有哪些,也可以使用终端命令(export OBJC_HELP=1)打印输出ObjC中的环境变量,如下图所示:

image.png

1.2 environ_init与static_init函数

  tls_init():关于线程key的绑定(比如每个线程数据的析构函数),其代码如下所示:

void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

  static_init():运行C++静态构造函数,在dyld调用我们的静态构造函数之前,libc会调用_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()
{
    size_t count;
    auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        inits[i]();
    }
    auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        UnsignedInitializer init(offsets[i]);
        init();
    }
}

1.3 exception_init函数

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

  而_objc_terminate这个函数的代码如下所示:

/***********************************************************************
* _objc_terminate
* Custom std::terminate handler.
*
* The uncaught exception callback is implemented as a std::terminate handler. 
* 1. Check if there's an active exception
* 2. If so, check if it's an Objective-C exception
* 3. If so, call our registered callback with the object.
* 4. Finally, call the previous terminate handler.
**********************************************************************/
static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
    if (PrintExceptions) {
        _objc_inform("EXCEPTIONS: terminating");
    }

    if (! __cxa_current_exception_type()) {
        // No current exception.
        (*old_terminate)();
    }
    else {
        // There is a current exception. Check if it's an objc exception.
        @try {
            __cxa_rethrow();
        } @catch (id e) {
            // It's an objc object. Call Foundation's handler, if any.
            (*uncaught_handler)((id)e);
            (*old_terminate)();
        } @catch (...) {
            // It's not an objc object. Continue to C++ terminate.
            (*old_terminate)();
        }
    }
}

  根据代码及其注释,我们知道如果发生了OC异常,就会调用uncaught_handler这个句柄函数来处理异常程序,因此我们全局搜索uncaught_handler这个关键字,会发现如下的代码:

/***********************************************************************
* _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;

  根据代码以及注释我们可知,如果不使用Foundation库中的函数对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;
}

  我们在objc_setUncaughtExceptionHandler函数中会对uncaught_handler这个句柄赋值,看到objc_setUncaughtExceptionHandler这个函数,我们能够很自然想到OCNSSetUncaughtExceptionHandler这个函数,而objc_setUncaughtExceptionHandler这个函数会不会就是NSSetUncaughtExceptionHandler函数的下层实现呢?

  首先我们在自己随意创建的iOS工程中的ViewController.m中编写如下代码:

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSArray *dataArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.dataArray = @[@"哈哈", @"呜呜", @"呦呦", @"嘎嘎"];
    
}

- (IBAction)buttonClick:(id)sender {
    
    NSLog(@"%@", self.dataArray[5]);
}

@end

  此时如果我们点击按钮,就会抛出数组访问越界的异常并且崩溃,如下图所示:

image.png

  但是我们也可以自己来捕获处理这些异常的信息,创建一个用来捕获处理异常的类UncaughtExceptionHandle,如下所示:

image.png

  其代码如下所示:

//UncaughtExceptionHandle.h 文件中代码如下
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface UncaughtExceptionHandle : NSObject

+ (void)installUncaughtSignalExceptionHandler;

@end

NS_ASSUME_NONNULL_END



//UncaughtExceptionHandle.m 文件中代码如下
#import "UncaughtExceptionHandle.h"
#include <libkern/OSAtomic.h>
#include <execinfo.h>
#include <stdatomic.h>

//异常名
NSString * const kUncaughtExceptionHandlerSignalExceptionName = @"LGUncaughtExceptionHandlerSignalExceptionName";
//异常原因
NSString * const kUncaughtExceptionHandlerSignalExceptionReason = @"LGUncaughtExceptionHandlerSignalExceptionReason";

//异常签名
NSString * const kUncaughtExceptionHandlerSignalKey = @"UncaughtExceptionHandlerSignalKey";
//异常产生地址(唯一)
NSString * const kUncaughtExceptionHandlerAddressesKey = @"UncaughtExceptionHandlerAddressesKey";
//异常保存文件名
NSString * const kUncaughtExceptionHandlerFileKey = @"UncaughtExceptionHandlerFileKey";
//异常函数调用符号信息
NSString * const kUncaughtExceptionHandlerCallStackSymbolsKey = @"UncaughtExceptionHandlerCallStackSymbolsKey";

//atomic_int在库stdatomic中定义
atomic_int      kUncaughtExceptionCount = 0; // 未捕获的异常数量
//
const int32_t   kUncaughtExceptionMaximum = 8; //未捕获的异常数量的最大值

//从堆栈信息读取信息的起始位置,值为4是因为要跳过UncaughtExceptionHandle调用方法以及函数的堆栈信息
const NSInteger kUncaughtExceptionHandlerSkipAddressCount = 4;
//从堆栈信息读取信息的数量,获取5个就可以了
const NSInteger kUncaughtExceptionHandlerReportAddressCount = 5;

@implementation UncaughtExceptionHandle

//异常处理的函数
void ExceptionHandlers(NSException *exception) {
    NSLog(@"%s", __func__);
    
    //获取异常的数量
    int32_t exceptionCount = atomic_fetch_add_explicit(&kUncaughtExceptionCount, 1, memory_order_relaxed);
    
    //异常数量大于最大值,就不处理了。
    if (exceptionCount > kUncaughtExceptionMaximum) {
        return;
    }
    
    //获取堆栈信息 - model 编程思想
    NSArray *callStack = [UncaughtExceptionHandle backtrace];
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
    
    NSLog(@"异常名: %@", exception.name);
    NSLog(@"异常原因: %@", exception.reason);
    NSLog(@"异常调用栈信息: %@", exception.callStackSymbols);
    
    [userInfo setObject:exception.name forKey:kUncaughtExceptionHandlerSignalExceptionName];
    
    [userInfo setObject:exception.callStackReturnAddresses forKey:kUncaughtExceptionHandlerSignalExceptionReason];
    
    [userInfo setObject:callStack forKey:kUncaughtExceptionHandlerAddressesKey];
    
    [userInfo setObject:exception.callStackSymbols forKey:kUncaughtExceptionHandlerCallStackSymbolsKey];
    
    [userInfo setObject:@"DemoApp_Exceptions" forKey:kUncaughtExceptionHandlerFileKey];
    
    //保存崩溃日志或上传到服务器
    [[[UncaughtExceptionHandle alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject:[NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo] waitUntilDone:YES];
}


- (void)handleException:(NSException *)exception {
    
    NSDictionary *userInfo = [exception userInfo];
    //保存本地并上传到服务器
    [self saveCrash:exception file:[userInfo objectForKey:kUncaughtExceptionHandlerFileKey]];
    
    //在这里也可弹出提示框提示用户相关信息
}

- (void)saveCrash:(NSException *)exception file:(NSString *)fileName {
    
    //获取发送异常的堆栈信息
    NSArray *stackArray = [[exception userInfo] objectForKey:kUncaughtExceptionHandlerCallStackSymbolsKey];
    
    //异常名称
    NSString *exceptionName = [exception name];
    //异常原因
    NSString *exceptionReason = [exception reason];
    
    //直接使用代码输出崩溃信息日志
    NSLog(@"crash: %@", exception);
    //获取保存到本地的文件路径(沙盒缓存路径)
    NSString *exceptionFilePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:fileName];
    
    //如果文件路径不存在,就创建这个路径
    if (![[NSFileManager defaultManager] fileExistsAtPath:exceptionFilePath]) {
        [[NSFileManager defaultManager] createDirectoryAtPath:exceptionFilePath withIntermediateDirectories:YES attributes:nil error:nil];
    }
    
    //获取异常出现时的时间戳,写到文件名后缀中,以便排序及查阅
    NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0];
    NSTimeInterval timeInterval = [date timeIntervalSince1970];
    NSString *timeString = [NSString stringWithFormat:@"%f", timeInterval];
    
    NSString *savePath = [exceptionFilePath stringByAppendingFormat:@"/crash-error-%@.log", timeString];
    
    //获取异常信息
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception Reason: %@\nException Name: %@\nException stack: %@", exceptionName, exceptionReason, stackArray];
    
    //将异常信息保存到路径中
    BOOL isSuccess = [exceptionInfo writeToFile:savePath atomically:YES encoding:(NSUTF8StringEncoding) error:nil];
    
    NSLog(@"保存崩溃日志 success: %d, %@", isSuccess, savePath);
}

+ (void)installUncaughtSignalExceptionHandler {
    
    //objc_setUncaughtExceptionHandler()
    NSSetUncaughtExceptionHandler(&ExceptionHandlers);
}

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

@end



//在应用启动的时候进行调用
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [LGUncaughtExceptionHandle installUncaughtSignalExceptionHandler];
    return YES;
}

  这样,在我们的程序崩溃的时候,就会执行到我们所编写的crash异常捕获的代码了,虽然我们知道了了如何捕捉异常,但是还必须知道它是如何捕获的,其实关键就在于installUncaughtSignalExceptionHandler这个类方法中的代码是如何实现的,我们查看这个函数的方法详情,会发现如下信息:

image.png

  除了以上这些信息,再也找不到其他更多信息了,因此我们在ExceptionHandlers中打上断点,再次编译运行程序,点击按钮,执行到断点,打印函数调用堆栈信息,如下图所示:

image.png

  根据打印的信息,整个流程就很清晰了,ObjC中的异常系统首先会在_objc_init中进行初始化,这样每次有异常的时候都会调用ObjC异常系统中的_objc_terminate(终止运行)函数,如果是OC异常就会调用uncaught_handler这个句柄函数,而uncaught_handler这个句柄函数默认设置的是_objc_default_uncaught_exception_handler这个没有函数体的函数,如果主工程想要对uncaught_handler这个句柄进行赋值,就需要调用NSSetUncaughtExceptionHandler这个函数进行传参赋值,而实际上NSSetUncaughtExceptionHandler这个函数是CoreFoundation库中所暴露的接口,而这个接口在CoreFoundation中实际上调用的是ObjC库中的函数objc_setUncaughtExceptionHandler,最后根据传入的参数对uncaught_handler进行赋值,这个过程也称为下句柄。

1.4 其他初始化函数

  lock_init():没有重写,采用C++特性。

  runtime_init()runtime运行时环境初始化,里面主要是unattchedCategories(独立的分类表)初始化、allocatedClasses(已分配空间的类表)初始化,这个在之后的文章中会探讨,其代码如下所示:

void runtime_init(void)
{
    objc::unattachedCategories.init(32);
    objc::allocatedClasses.init();
}

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

  _imp_implementationWithBlock_init():启动回调机制,通常这不会做什么,因为所有的初始化都是惰性的,但是对于默写进程,我们会迫不及待地加载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
}

2. read_images函数代码流程解析

  经过以上的探究我们对_objc_init中的代码逻辑已经有了很透彻的了解,紧接着我们就来探究一下map_images函数中的逻辑实现,其代码如下所示:

/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
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函数,其代码如下图所示:

image.png

  在这个函数中有157行代码,还是比较长的,并且这个函数没有返回值,那么我们只能大概浏览一下整个函数,发现了_read_images(读取程序二进制文件)这个函数,我们猜测这个函数中应该包含了重要的功能,因此我们点击查看这个函数的实现,这个函数中的代码也是非常长的,有360行代码,并且同样没有返回值,其代码概要如下图所示:

image.png

  在这个函数中,前面都是对于架构的分类处理,而我们真正需要关心的就是每个FixUp以及关于类的加载的代码,因此我们来进行逐一分析。

2.1 条件控制进行一次加载

代码如下:

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

  NXMapTable实际上是一个表,其中存放了所有非懒加载的class,也就是已经实现的类,并且使用getClass进行了Hook

    if (!doneOnce) {
        doneOnce = YES;
        launchTime = YES;
        ...
        ...
        ...
        if (PrintConnecting) {
            _objc_inform("CLASS: found %d classes during launch", totalClasses);
        }

        // namedClasses
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
        //根据当前已经初始化的类的数量计算出NXCreateMapTable的容量
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);

        ts.log("IMAGE TIMES: first time tasks");
    }

  在这个函数中调用了NXCreateMapTable函数,其代码如下所示:

NXMapTable *NXCreateMapTable(NXMapTablePrototype prototype, unsigned capacity) {
    return NXCreateMapTableFromZone(prototype, capacity, malloc_default_zone());
}

  在这个函数中创建并返回了NXMapTable类型的表,用来存储非懒加载的类,NXMapTable表的创建写在这里是为了保证这中类型的表只会被创建一次,而之前在_objc_init函数中调用的runtime_init函数中创建的表allocatedClasses实际上存储的是所有已分配的类(包括元类),其代码及注释如下所示:

/***********************************************************************
* allocatedClasses
* A table of all classes (and metaclasses) which have been allocated
* with objc_allocateClassPair.
**********************************************************************/
namespace objc {
static ExplicitInitDenseSet<Class> allocatedClasses;
}

2.2 修复预编译阶段@selector的混乱问题

  代码如下所示:

// Fix up @selector references
    static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->hasPreoptimizedSelectors()) continue;

            bool isBundle = hi->isBundle();
            SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                SEL sel = sel_registerNameNoLock(name, isBundle);
                if (sels[i] != sel) {
                    sels[i] = sel;
                }
            }
        }
    }

    ts.log("IMAGE TIMES: fix up selector references");

  首先在代码打上断点,运行ObjC源码,打印输出下图信息:

image.png

  你会发现sel[i]sel虽然方法编号都是一样的,但是它们的地址却不一样,但是这里为什么会将sel[i]赋值为sel呢?原因就在于使用_getObjc2SelectorRefs这个函数获取到的sel列表中的每个sel的地址数据实际上是镜像文件中的存储的数据,而镜像文件中的每个sel的地址都是相对与当前镜像文件首地址的偏移值,当这个镜像文件经过ALSR等安全机制加载到内存中后,镜像文件的首地址就会产生随机改变,其中每个sel的地址其实也都发生了改变,就需要对从镜像文件中获取到的sel列表中的sel进行rebase以获取其此时此刻在内存中的实际地址,因此sel_registerNameNoLock这个函数就是用来对镜像文件中的sel的地址信息进行rebase操作的,然后将sel赋值给sel[i]

  而sel_registerNameNoLock这个函数实际上调用的是dyld库中的_dyld_get_objc_selector函数来获取sel在内存中的地址的,如下所示:

SEL sel_registerNameNoLock(const char *name, bool copy) {
    return __sel_registerName(name, 0, copy);  // NO lock, maybe copy
}


static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
{
    SEL result = 0;

    if (shouldLock) selLock.assertUnlocked();
    else selLock.assertLocked();

    if (!name) return (SEL)0;

    result = search_builtins(name);
    if (result) return result;
    
    conditional_mutex_locker_t lock(selLock, shouldLock);
	auto it = namedSelectors.get().insert(name);
	if (it.second) {
		// No match. Insert.
		*it.first = (const char *)sel_alloc(name, copy);
	}
	return (SEL)*it.first;
}

static SEL search_builtins(const char *name) 
{
#if SUPPORT_PREOPT
  if (SEL result = (SEL)_dyld_get_objc_selector(name))
    return result;
#endif
    return nil;
}

2.3 错误混乱的类处理

  代码如下所示:

 // Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();

    for (EACH_HEADER) {
        if (! mustReadClasses(hi, hasDyldRoots)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }

        classref_t const *classlist = _getObjc2ClassList(hi, &count);

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->hasPreoptimizedClasses();

        for (i = 0; i < count; i++) {
            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[resolvedFutureClassCount++] = newCls;
            }
        }
    }

    ts.log("IMAGE TIMES: discover classes");

  这一部分是对一些在内存中已经移动但是还没有删除的类(类似于野指针)进行的处理,在这一段代码中我们发现了一个函数readClass,这个函数可能有我们想要了解的代码逻辑,因此我们来看看这个函数中的代码,如下所示:

/***********************************************************************
* readClass
* Read a class and metaclass as written by a compiler.
* Returns the new class pointer. This could be: 
* - cls
* - nil  (cls has a missing weak-linked superclass)
* - something else (space for this class was reserved by a future class)
*
* Note that all work performed by this function is preflighted by 
* mustReadClasses(). Do not change this function without updating that one.
*
* Locking: runtimeLock acquired by map_images or objc_readClassPair
**********************************************************************/
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->nonlazyMangledName();
    
    if (missingWeakSuperclass(cls)) {
        // No superclass (probably weak-linked). 
        // Disavow any knowledge of this subclass.
        if (PrintConnecting) {
            _objc_inform("CLASS: IGNORING class '%s' with "
                         "missing weak-linked superclass", 
                         cls->nameForLogging());
        }
        addRemappedClass(cls, nil);
        cls->setSuperclass(nil);
        return nil;
    }
    
    cls->fixupBackwardDeployingStableSwift();

    Class replacing = nil;
    if (mangledName != nullptr) {
        if (Class newCls = popFutureNamedClass(mangledName)) {
            // This name was previously allocated as a future class.
            // Copy objc_class to future class's struct.
            // Preserve future's rw data block.

            if (newCls->isAnySwift()) {
                _objc_fatal("Can't complete future class request for '%s' "
                            "because the real class is too big.",
                            cls->nameForLogging());
            }

            class_rw_t *rw = newCls->data();
            const class_ro_t *old_ro = rw->ro();
            memcpy(newCls, cls, sizeof(objc_class));

            // Manually set address-discriminated ptrauthed fields
            // so that newCls gets the correct signatures.
            newCls->setSuperclass(cls->getSuperclass());
            newCls->initIsa(cls->getIsa());

            rw->set_ro((class_ro_t *)newCls->data());
            newCls->setData(rw);
            freeIfMutable((char *)old_ro->getName());
            free((void *)old_ro);

            addRemappedClass(cls, newCls);

            replacing = cls;
            cls = newCls;
        }
    }
    
    if (headerIsPreoptimized  &&  !replacing) {
        // class list built in shared cache
        // fixme strict assert doesn't work because of duplicates
        // ASSERT(cls == getClass(name));
        ASSERT(mangledName == nullptr || getClassExceptSomeSwift(mangledName));
    } else {
        if (mangledName) { //some Swift generic classes can lazily generate their names
            addNamedClass(cls, mangledName, replacing);
        } else {
            Class meta = cls->ISA();
            const class_ro_t *metaRO = meta->bits.safe_ro();
            ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass.");
            ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class.");
        }
        addClassTableEntry(cls);
    }

    // for future reference: shared cache never contains MH_BUNDLEs
    if (headerIsBundle) {
        cls->data()->flags |= RO_FROM_BUNDLE;
        cls->ISA()->data()->flags |= RO_FROM_BUNDLE;
    }
    
    return cls;
}

  首先这个函数中代码的返回值为cls,因此我们看看这个函数中有哪些地方对这个cls进行了创建以及操作,我们发现了我们所熟悉的部分,就是对clsrorwrwe进行赋值操作,并且设置了clsisa以及superclass,如下图所示:

image.png

  但这真的是对class进行加载的代码部分吗?我们还需要验证一下,我们在源码中创建一个Person类,并在main函数中创建一个Person对象,如下所示:

@interface Person : NSObject

- (void)say;

@end

@implementation Person

- (void)say {
    
}

@end

int main(int argc, const char * argv[]) {
    Person *p = [[Person alloc] init];
    
    @autoreleasepool {
        NSLog(@"执行main函数");
    }
    return 0;
}

  为了排除系统类产生的影响,我们在代码中readClass函数调用之前编写一些代码并设置上断点,如下图所示:

image.png

  编译运行程序,来到断点,打印当前cls,如下所示:

image.png

  然后在readClass函数中打下如下断点,并单步执行程序,如下所示:

image.png

  单步运行程序,看程序都执行了哪些代码语句,结果如下所示:

image.png

  而是执行了下面所示的分支:

image.png

  执行前后打印cls,输出结果如下:

image.png

  单步运行,然后执行了addClassTableEntry这个函数中的代码,如下图所示:

image.png

  这个函数代码如下所示:

/***********************************************************************
* addClassTableEntry
* Add a class to the table of all classes. If addMeta is true,
* automatically adds the metaclass of the class as well.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
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.
    auto &set = objc::allocatedClasses.get();

    ASSERT(set.find(cls) == set.end());

    if (!isKnownClass(cls))
        set.insert(cls);
    if (addMeta)
        addClassTableEntry(cls->ISA(), false);
}

  其逻辑是将cls插入到已分配类表中,并且还会通过递归调用将其元类也插入到已分配类表中,最后就直接返回了cls,可以发现,对于我们创建的Person类来说,readclass函数只是对其类名进行了赋值,并没有为其rorwrwe等其他内容进行赋值。

2.4 修复重映射一些没有被镜像文件加载进来的类

  代码如下所示:

// Fix up remapped classes
    // Class list and nonlazy class list remain unremapped.
    // Class refs and super refs are remapped for message dispatching.
    
    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]);
            }
        }
    }

    ts.log("IMAGE TIMES: remap classes");

2.5 修复一些消息

  代码如下所示:

#if SUPPORT_FIXUP
    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) {
        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++) {
            fixupMessageRef(refs+i);
        }
    }

    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif

3.总结

  异常处理流程图:

异常处理流程图

  本篇文章主要探究了在项目开发工程中我们可能会遇到的一些环境变量以及异常处理的流程,也探究了map_images函数中的代码流程,而map_images流程中重要的函数就是read_images函数,而在read_images这个函数中做了很多的修复工作,我们本来猜测可能是在read_class加载的类的信息,但是我们调试运行代码的时候,实际情况却是仅在read_class函数中对类名进行了赋值,并且将类加入到了已分配类的表中,由于内容过多,因此将read_images这个函数中的下半部分代码的探究过程放入到下篇文章之中了,感谢您的阅读!