和谐学习!不急不躁!!我是你们的老朋友小青龙~
前面几篇文章,我们认识了类的本质,属性、协议等存放的位置,以及通过dyld链接,将Mach-O加载到内存里来。那么类的各种信息
又是如何加载的?这就是本次课题需要探索的内容- - - 类的加载
。
通过上一篇文章应用程序加载流程,
我们知道dyld跟objc是通过_dyld_objc_notify_register
函数进行关联的,objc里调用_dyld_objc_notify_register
的函数是_objc_init
,那么_objc_init
里又做什么事情呢?
void _objc_init(void)
{
...
// 环境变量初始化
environ_init();
// 设置析构函数
tls_init();
// C++函数静态函数初始化
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
}
environ_init
void environ_init(void)
{
...
if (PrintHelp || PrintOptions) {
...
//将这段for代码块复制一份到if (PrintHelp || PrintOptions)前面执行
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);
}
}
...
}
略作改动:
void environ_init(void)
{
...
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
// 去掉了前面的if判断
_objc_inform("%s: %s", opt->env, opt->help);
_objc_inform("%s is set", opt->env);
}
if (PrintHelp || PrintOptions) {
...
}
...
}
打印:
图片上控制台打印都是一些环境变量,就拿
OBJC_PRINT_LOAD_METHODS
来配置环境变量:
运行工程:
OBJC_PRINT_LOAD_METHODS
环境变量的配置,可以在控制台打印
所有调用到load方法
的地方,这样方便我们在做项目优化
的时候,快速定位到load
方法的位置。针对项目不大的情况,其实全局搜索也能定位到load方法,消耗的时间也不多,但是全局搜索
搜不到一些写好的库文件里的load方法
。
当然,除了控制台打印,我们也可以选择将它在终端打印显示,输入
$export OBJC_HELP=1
tls_init
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
}
进入pthread_key_init_np
:
/* setup destructor function for static key as it is not created with pthread_key_create() */
// 大概意思是,针对非pthread_key_create()创建的key键,设置析构函数
int pthread_key_init_np(int, void (*)(void *));
拓展
:什么是析构函数?
析构函数是特殊的类成员函数,它用来完成对象被删除前的一些清理工作,也就是专门的扫尾工作。
static_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.
**********************************************************************/
//注释可以得知,主要是做一些静态初始化操作,运行C++静态构造函数
static void static_init()
{
...
}
runtime_init
void runtime_init(void)
{
objc::unattachedCategories.init(32);
// 全局搜索allocatedClasses可以看到注释,
// 大概意思是初始化“已分配的所有类(和元类)的表”
objc::allocatedClasses.init();
}
exception_init
/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
//初始化libobjc异常,由map_images函数调起
void exception_init(void)
{
...
}
举个例子,数组越界错误:
- (IBAction)logArray{
NSArray *a = @[@"张三",@"李四",@"王五"];
NSLog(@"打印----%@",a[4]);
}
objc源码工程搜索
_objc_terminate
:
static void _objc_terminate(void)
{
...
@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)();
}
...
}
我们发现,在catch异常的时候会先调用uncaught_handler
,这个是用来告诉上层代码,这里报错了。
那么,如何设置uncaught_handler
呢?
objc源码全局搜索它:
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
...
uncaught_handler = fn;
...
}
发现uncaught_handler
的值来自于objc_setUncaughtExceptionHandler
函数的fn参数,也就是说我们可以通过调用objc_setUncaughtExceptionHandler函数来给uncaught_handler赋值。
Foundation
里面提供了一个NSSetUncaughtExceptionHandler
函数,可以设置一个顶层异常处理函数。
我们可以调用NSSetUncaughtExceptionHandler来接收底层反馈的异常信息:
// NdUncaughtExceptionHandler.h文件
#import <Foundation/Foundation.h>
void uncaughtExceptionHandler(NSException * exception);
@interface NdUncaughtExceptionHandler : NSObject
@end
// NdUncaughtExceptionHandler.m文件
@implementation NdUncaughtExceptionHandler
void uncaughtExceptionHandler(NSException * exception){
//异常的堆栈信息
NSArray *stackArray = [exception callStackSymbols];
//出现异常的原因
NSString *reason = [exception reason];
//异常名称
NSString *name = [exception name];
//拼接异常信息
NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason: %@\nException name: %@\nException call stack:%@\n", name, reason, stackArray];
NSLog(@"捕获异常----%@",exceptionInfo);
}
// AppDelegate.m文件里 部分代码
#import "NdUncaughtExceptionHandler.h"
...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 这样就可以在uncaughtExceptionHandler函数里,自己处理异常
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
return YES;
}
打个断点,运行看下效果:
通过这种方式,我们可以针对一些线上奔溃,做一个奔溃日志信息的收集。
接下来继续分析_objc_init
void _objc_init(void)
{
...
_imp_implementationWithBlock_init();
/** 第一个参数带&表示它是一个指针传递,与map_images同步发生变化
这样设计的目的是,map_images内部是做了镜像文件的映射,
这个过程比较耗时,一旦中间有某一步出现问题,会导致整个程序发生问题,所以需要同步设置。
_dyld_objc_notify_register一旦调用,map_images就会传递给dyld里的
*/
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
_imp_implementationWithBlock_init做了什么?
/// 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)
{
/**
在某些进程中急切地加载libobjc-trampolines.dylib。一些
程序(最著名的是早期版本的嵌入铬)启用高度限制的沙箱配置文件
阻止对那个动态库的访问。如果有什么事imp_实现WithBlock
(正如AppKit已经开始做的那样),然后我们将尝试加载时崩溃。
在这里加载将在沙箱之前设置它配置文件已启用并阻止它。
这修复了EA的起源(rdar://problem/50813789)
和蒸汽(rdar://problem/55286131)
*/
if (__progname &&
(strcmp(__progname, "QtWebEngineProcess") == 0 ||
strcmp(__progname, "Steam Helper") == 0)) {
Trampolines.Initialize();
}
}
_dyld_objc_notify_register做了什么?
_dyld_objc_notify_register内部调用了map_images
和load_images
。
带&
表示它是一个指针传递,为什么第一个参数带&
呢?
因为map_images
这个函数重要性
级别很高;
我们知道,_dyld_objc_notify_register一旦调用,参数map_images
就会传递给dyld
里的sNotifyObjCMapped
// 代码来自 dyld源码
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
// 进入registerObjCNotifiers
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
...
}
// 进入notifyBatchPartial
static void notifyBatchPartial(dyld_image_states state, bool orLater, dyld_image_state_change_handler onlyHandler, bool preflightOnly, bool onlyObjCMappedNotification)
{
...
// 这里调用了registerObjCNotifiers函数的第一个参数
(*sNotifyObjCMapped)(objcImageCount, paths, mhs);
...
}
我们再来看objc源码里map_images函数:
//处理dyld映射到的给定图像。
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{...}
map_images这个过程比较耗时,一旦中间有某一步出现问题,会导致整个程序发生问题,所以需要同步设置(sNotifyObjCMapped要跟map_images同步)。
map_images干了什么?
进入map_images
:
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
...
return map_images_nolock(count, paths, mhdrs);
}
进入map_images_nolock
:
void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
// 代码很长。。。我们只关心跟images有关系的部分
...
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
...
}
进入_read_images
:
大致流程如下
// 1、************ 条件控制进行一次加载 ************
if (!doneOnce){...}
// 2、************ 修复@selector混乱问题 ************
static size_t UnfixedSelectors;
...
ts.log("IMAGE TIMES: fix up selector references");
// 3、************ 修复类的混乱问题 ************
// 开始遍历头文件,进行类与元类的读取操作并标记(旧类改动后会生成新的类,并重映射到新的类上)
for (EACH_HEADER){...}
ts.log("IMAGE TIMES: discover classes");
// 4、************ 修复重映射没有被镜像文件加载进来的 类 ************
if (!noClassesRemapped()){...}
ts.log("IMAGE TIMES: remap classes");
// 5、************ 修复一些消息 ************
for (EACH_HEADER){...}
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
// 6、************ readProtocol读取协议 ************
for (EACH_HEADER){...}
ts.log("IMAGE TIMES: discover protocols");
// 7、************ 修复一些协议 ************
for (EACH_HEADER){...}
ts.log("IMAGE TIMES: fix up @protocol references");
// 8、************ load_categories_nolock分类的处理 ************
if (didInitialAttachCategories){…}
ts.log("IMAGE TIMES: discover categories");
// 9、************ 类的加载处理 ************
for (EACH_HEADER){…}
ts.log("IMAGE TIMES: realize non-lazy classes");
// 10、************ 实现未来类解析 ************
if (resolvedFutureClasses){…}
ts.log("IMAGE TIMES: realize future classes");
// 剩下的就是一些打印
下面开始针对性的分析_read_images
:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
...
static bool doneOnce;
...
if (!doneOnce) {
doneOnce = YES;
...
initializeTaggedPointerObfuscator();
...
// namedClasses
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
// gdb_objc_realized_classes是一个存放非共享缓存里的类名的列表,无论这些类是否已经被实现。
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
}
// 修复selector(SEL地址=段地址+偏移量,每次运行地址偏移量都是不一样的)
static size_t UnfixedSelectors;
{
...
// Mach-O里获取SEL地址
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
// dyld里获取SEL的真正地址,这块的探索放到后面“sel_registerNameNoLock探索”
SEL sel = sel_registerNameNoLock(name, isBundle);
// 以dyld地址为参考,如果不一样,就修改
if (sels[i] != sel) {
sels[i] = sel;
}
}
}
...
// 修复类
for (EACH_HEADER) {
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
}
...
}
进入readClass
:
readClass内部代码很多,为了搞清楚mangledName
是什么,我们这边printf
打印一下。
我们发现mangledName打印的是类名,我们只需要研究我们自己的类
Direction
,
代码略作改动:
接下来,我们给
printf
那一行打上断点,以便后面的操作都是针对Direction
类,
然后再通过断点一步步走,看它的内部代码走向:
那些没有进入的代码块我们不需要去关注,代码简化
之后:
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
const char *mangledName = cls->nonlazyMangledName();
...
cls->fixupBackwardDeployingStableSwift();
Class replacing = nil;
...
else{
if (mangledName) { //some Swift generic classes can lazily generate their names
// 将name添加到非元类映射表
addNamedClass(cls, mangledName, replacing);
}
...
/**
将类添加到所有类的那张表里;
如果第二个参数为true,则元类也加入到allocatedClasses表。
*/
addClassTableEntry(cls);
}
...
return cls;
}
针对自定义的普通类,readClass函数
是将类、元类
添加到实现表。
sel_registerNameNoLock探索
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
...
SEL sel = sel_registerNameNoLock(name, isBundle);
...
}
进入sel_registerNameNoLock
:
SEL sel_registerNameNoLock(const char *name, bool copy) {
return __sel_registerName(name, 0, copy); // NO lock, maybe copy
}
进入__sel_registerName
:
static SEL __sel_registerName(const char *name, bool shouldLock, bool copy)
{
...
// name非空判断
if (!name) return (SEL)0;
// 根据name查找SEL
result = search_builtins(name);
if (result) return result;
...
// 根据name找不到SEL才会进入这行代码,意思是把name插入到namedSelectors
//objc_allocateClassPair表
auto it = namedSelectors.get().insert(name);
...
// 返回带地址的SEL
return (SEL)*it.first;
}
进入search_builtins
:(看看内部大概流程)
static SEL search_builtins(const char *name)
{
...
if (SEL result = (SEL)_dyld_get_objc_selector(name))
...
}
// 进入_dyld_get_objc_selector,发现这是一个dyld的方法
extern const char* _dyld_get_objc_selector(const char* selName);
打开dyld源码
搜索_dyld_get_objc_selector
:
const char* _dyld_get_objc_selector(const char* selName){
// Check the shared cache table if it exists.
// 如果类已经存在,就去共享缓存查找。
...
// 因为系统写的方法是在共享缓存里的,我们自己写的方法不在共享缓存里面,
// 所以需要再走一遍dyld3的_dyld_get_objc_selector函数
if ( gUseDyld3 )
_dyld_get_objc_selector
...
}
进入dyld3
的_dyld_get_objc_selector
函数:
const char* _dyld_get_objc_selector(const char* selName){
...
return gAllImages.getObjCSelector(selName);
}
进入getObjCSelector
:
const char* AllImages::getObjCSelector(const char *selName) const {
...
return _objcSelectorHashTable->getString(selName, _objcSelectorHashTableImages.array());
}
走到这里,发现不能进入_objcSelectorHashTable
,也不能进入getString
,改为全局搜索_objcSelectorHashTable
:
发现_objcSelectorHashTable被赋值为
selectorHashTable
,而selectorHashTable是ObjCSelectorOpt
类型,所以进入ObjCSelectorOpt
:
输入法有毛病,截图的时候突然间不能打中文了~
// Get a string if it has an entry in the table
const char* getString(const char* selName, const Array<uintptr_t>& baseAddresses) const;
进入getString
:
const char* ObjCStringTable::getString(const char* selName, const Array<uintptr_t>& baseAddresses) const {
// 读取当前段地址
uintptr_t sectionBaseAddress = baseAddresses[imageAndOffset.imageIndex];
// sel地址 = 段地址+二进制文件偏移量
const char* value = (const char*)(sectionBaseAddress + imageAndOffset.imageOffset);
// strcmp是C语言里,用来比较两个参数是否相等的函数
if (!strcmp(selName, value))
return value;
// 不等就返回空
return nullptr;
}
看到这里,我们也可以有所感知:SEL不仅仅是一个字符串,它还带有地址。
sel_registerNameNoLock
返回一个SEL,它地址=段地址+二进制文件偏移量。
下篇预告
本文,从dyld
链接到objc_init
,分析了objc_init内部做了哪些事情:
- 环境变量初始化
- 设置析构函数
- C++函数静态函数初始化
- 两张表初始化
- 异常信息初始化
- 缓存条件初始化
- _dyld_objc_notify_register
到目前为止,我们只看到类、元类被加载到表里,那么类的ro、rw又是在哪里加载的呢?
探究内容将会放到下一篇章:
iOS底层分析之类的加载(中)
----更新时间:2021-8-04
代码
百度网盘
链接: pan.baidu.com/s/1iDuSZcqY… 密码: r5g0
(包含dyld源码、objc4-818.2源码、类的加载原理Demo)