前言
前文iOS底层原理之dyld应用程序加载分析了dyld
的整个流程以及dyld
和objc
的交互流程。本文将接着分析dyld
调用map_images
究竟做了什么操作。
准备工作
- dyld源码。
- objc4-818.2源码。
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
自己的。libc
在dyld
调用静态构造函数之前调用了_objc_init()
函数,所以我们必须自己调。runtime_init
:runtime相关两张表的初始化(类表和未附加到类的分类表)。exception_init
:初始化libobjc
的异常处理系统。cache_t::init
:缓存条件初始化。_imp_implementationWithBlock_init
:启动trampoline
机制。通常情况下,这不起任何作用,因为一切都是惰性地初始化的,但是对于某些进程,我们急切地加载libobjc-trampolines.dylib
动态库。_dyld_objc_notify_register
:注册map_images
、load_images
、unmap_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) {
...
}
打印输出:
1.1.2: 终端输出
使用终端命令export OBJC_HELP=1
输出环境变量。
- 先使用
export OBJC_HELP=1
命令导出OBJC_HELP
,然后执行一个终端命令就可以打印出环境变量了。
1.1.3: 查看源码文件
在objc
源码里搜索前两步打印出来的相关环境配置,定位到objc-env.h
文件中查看相关源码。
1.1.4: Xcode
设置环境变量辅助调试
在Xcode
->Edit Scheme
->Arguments
->Environment Variables
中配置OBJC_DISABLE_NONPOINTER_ISA
、OBJC_PRINT_LOAD_METHODS
、OBJC_PRINT_INITIALIZE_METHODS
三个环境变量,依次勾选测试。
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++
静态构造函数。libc
在dyld
调用静态构造函数之前调用了_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
自己的。相当于objc
的C++
静态构造函数是在dyld
库的ImageLoaderMachO::doModInitFunctions
函数调用之前自己调用的,因为dyld
调用的时机太晚了。objc
对这个section
进行了替换,所以后续dyld
不会调用到这块(下文分析)。 - 内部有两个逻辑一个是通过
__mod_init_funcs
,一个是通过__init_offsets
从macho
文件读取C++
构造函数。目前暂不清楚什么情况下会走__init_offsets
的逻辑。
直接在objc
源码中添加一个C++
构造函数来进行验证。
- 可以看到在
_objc_init
函数调用_dyld_objc_notify_register
函数注册回调之前,就已经调用了我们添加的名为objcFunc
的C++
构造函数。
通过上面代码发现getLibobjcInitializers
函数和getLibobjcInitializerOffsets
函数对macho
文件的读取与macho
文件的实际内容是不相符的。
macho
文件里只有名为__mod_init_func
的Section
,里面有我们自己添加的C++
构造函数objcFunc
。- 但是
getLibobjcInitializers
函数读取的是名为__objc_init_func
的Section
;getLibobjcInitializerOffsets
函数读取的是名为__objc_init_offs
的Section
。
在源码里搜索__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_func
。SECTION_TYPE
为S_MOD_INIT_FUNC_POINTERS(0x9)
。 -
__init_offsets
被修改为了__objc_init_offs
。SECTION_TYPE
为S_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
。这个也就是Terminators
。SECTION_TYPE
为S_MOD_TERM_FUNC_POINTERS(0xa)
。这个实现了destructor
函数就有了。 -
dosect
函数最终是被objc
的main
函数调用的。此函数的注释中已经说明了不想被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
相关allocatedClasses
和unattachedCategories
两张储存表的初始化。
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
异常需要设置signal
的handler
。 自定义的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);
}
}
sigaction
和signal
函数的区别:
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
逻辑。这样就能断住断点了:
最终crash
的.log
文件存入了沙盒,通过Xcode
->Devices and Simulators
选择运行时使用的设备和相关APP
下载沙盒文件,右键选中显示包内容,然后根据控制台输出的路径就可以找到相关的.log
文件了。
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_images
、load_images
、unmap_image
三个回调。相关实现在dyld
中。
map_images
:管理由dyld
映射的给定镜像文件的所有符号(class
,Protocol
,selector
,category
)。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
函数。
- 核心逻辑在
_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
提示整理出主要流程模块:
- 条件控制,只进行一次加载。(
doneOnce
)。 - 修复预编译阶段的
@selector
的混乱问题。 - 发现类。修复错误混乱的类。标记捆绑类。
- 修复重映射一些没有被镜像文件加载进来的类 。
- 修复一些消息。
- 当类里面有协议的时候:
readProtocol
。 - 修复没有被加载的协议。
- 分类处理。
- 类的加载处理。
- 没有被处理的类,优化那些被侵犯的类。
很显然8
和9
是核心。也就是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
初始化的unattachedCategories
、allocatedClasses
表中,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
的混乱问题。
运行源码,打上断点验证:
- 名称相同,地址不同,以
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_classlist
的section
获取classlist
,现在存的只是地址。 - 调用
readClass
函数将地址与类进行了关联。 - 如果有类发生了混乱(类已移动但未删除),就添加到
resolvedFutureClasses
列表中,然后在3.10
流程中非懒加载的加载。
在readClass
处添加断点,运行源码验证:
- 调用
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;、 } }
-
rw
、ro
、isa
、superclass
等并不是在readClass
函数里处理的。- 打上断点运行源码,可以发现不会进入如图所示的代码区。
-
调用
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;
- 以
name
为key
,cls
为value
生成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_superrefs
的section
,然后调用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_msgrefs
的section
读取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_protolist
的section
中获取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->mangledName
为key
,newproto
为value
生成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_protorefs
的section
读取需要重映射的protocol
列表(discover protocols
是名为__objc_protolist
的section
)。 - 循环调用
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_nlclslist
的section
获取非懒加载类(no-lazy classes
)。 - 循环调用
remapClass
函数重新映射类。 - 调用
addClassTableEntry
函数将类加入allocatedClasses
表中,已经加入不会再次加入。 - 调用
realizeClassWithoutSwift
函数加载类,并递归加载父类和元类(此函数在之前的消息慢速查找流程就已经遇到过了)。
非懒加载类分为三种情况:
- 自己实现了
+load
方法。(会出现在__objc_nlclslist
中) - 子类实现了
+load
方法。(因为子类加载会加载父类,不会出现在__objc_nlclslist
中) - 分类实现了
+load
方法。(这里包括自己的分类以及子类的分类,不是在map_images
流程而是在prepare_load_methods
中实例化类的,不会出现在__objc_nlclslist
中)
⚠️尽量避免在
+load
方法中进行逻辑处理,因为整个过程是一个连锁反映,会导致程序启动变慢。
实现了+load
方法的类就会出现在macho
文件名为__objc_nlclslist
的section
中了。
为什么有懒加载类和非懒加载类的区别呢?
为了按需加载,因为在程序启动过程中,加载的类越少,程序启动自然也就越快。
通过上面流程已经清楚了非懒加载类的加载流程,那么懒加载类的加载流程是怎样的呢?
既然要加载类肯定需要调用realizeClassWithoutSwift
函数,那么在此函数加上断点,去掉+load
方法,并分别调用对象方法和类方法,查看函数调用栈。
对象方法调用栈:
备注:笔者使用的是
M1 版 iMac
,所以alloc
方法不会被替换为objc_alloc
函数。
类方法调用栈:
从函数调用栈可以看出不管是对象方法,还是类方法,都是在消息慢速查找流程
时调用realizeClassWithoutSwift
函数进行加载的。所以就说明了懒加载类是在第一次调用方法时进行加载的。
- 非懒加载类实现了
+load
方法(自己/子类/分类),类就会在程序启动时提前加载,为+load
方法的调用做准备。 - 懒加载类是在第一次调用方法(消息发送
objc_msgSend
),进行消息慢速查找(lookUpImpOrForward
)流程时进行加载的。
懒加载和非懒加载类加载流程对比:
-
懒加载类:加载推迟到第一次发送消息的时候。
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_init
和map_images
核心逻辑以思维导图的形式展示如下。