前言
上篇文章我们已经分析了应用程序的加载流程,对于应用程序的加载流程我们已经很熟悉了,我们已经清楚的明白了在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
语句外面,如下图所示:
编译运行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
工程,其中创建一个继承自NSObject
的Person
类,实现Person
类中的类方法load
,并设置2
个环境变量OBJC_DISABLE_NONPOINTER_ISA
(是否禁用NONPOINTER_ISA
)设置为NO
,OBJC_PRINT_LOAD_METHODS
(是否打印输出LOAD
方法调用时的信息)设置为NO
,如下图所示:
在ViewController
的ViewDidLoad
方法中编写代码并打上断点,链接真机,运行程序,打印p
对象的isa
,发现此isa
是NONPOINTER_ISA
,过掉断点,程序崩溃,如下图所示:
将OBJC_DISABLE_NONPOINTER_ISA
以及OBJC_PRINT_LOAD_METHODS
设置为YES
,如下图所示:
编译运行程序,打印输出p
对象的isa
,如下图所示:
OBJC_PRINT_LOAD_METHODS
这个环境变量可以输出所有调用load
类方法的类信息,帮助我们排查主程序以及framework
中调用load
的类都有哪些,也可以使用终端命令(export OBJC_HELP=1
)打印输出ObjC
中的环境变量,如下图所示:
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
这个函数,我们能够很自然想到OC
中NSSetUncaughtExceptionHandler
这个函数,而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
此时如果我们点击按钮,就会抛出数组访问越界的异常并且崩溃,如下图所示:
但是我们也可以自己来捕获处理这些异常的信息,创建一个用来捕获处理异常的类UncaughtExceptionHandle
,如下所示:
其代码如下所示:
//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
这个类方法中的代码是如何实现的,我们查看这个函数的方法详情,会发现如下信息:
除了以上这些信息,再也找不到其他更多信息了,因此我们在ExceptionHandlers
中打上断点,再次编译运行程序,点击按钮,执行到断点,打印函数调用堆栈信息,如下图所示:
根据打印的信息,整个流程就很清晰了,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
函数,其代码如下图所示:
在这个函数中有157
行代码,还是比较长的,并且这个函数没有返回值,那么我们只能大概浏览一下整个函数,发现了_read_images
(读取程序二进制文件)这个函数,我们猜测这个函数中应该包含了重要的功能,因此我们点击查看这个函数的实现,这个函数中的代码也是非常长的,有360
行代码,并且同样没有返回值,其代码概要如下图所示:
在这个函数中,前面都是对于架构的分类处理,而我们真正需要关心的就是每个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
源码,打印输出下图信息:
你会发现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
进行了创建以及操作,我们发现了我们所熟悉的部分,就是对cls
的ro
、rw
、rwe
进行赋值操作,并且设置了cls
的isa
以及superclass
,如下图所示:
但这真的是对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
函数调用之前编写一些代码并设置上断点,如下图所示:
编译运行程序,来到断点,打印当前cls
,如下所示:
然后在readClass
函数中打下如下断点,并单步执行程序,如下所示:
单步运行程序,看程序都执行了哪些代码语句,结果如下所示:
而是执行了下面所示的分支:
执行前后打印cls
,输出结果如下:
单步运行,然后执行了addClassTableEntry
这个函数中的代码,如下图所示:
这个函数代码如下所示:
/***********************************************************************
* 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
函数只是对其类名进行了赋值,并没有为其ro
、rw
、rwe
等其他内容进行赋值。
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
这个函数中的下半部分代码的探究过程放入到下篇文章之中了,感谢您的阅读!