经过前面几篇论文关于Runtime部分已经介绍差不多了,这篇算是一个总结性的文章吧,其实+initialize 和 +load 已经在之前的几篇博客中都做了十分详细的介绍,这里放在一起无非就是对比一下,增加下记忆,所以这篇博客不会写得很长很复杂,好了言归正传:

+initialize
  • initialize的特点是惰性调用方式,它的调用时机是在对某个对象发送第一条消息的时候,也就是说如果我们不对某个对象发送消息,那么这个对象的initialize是不会被调用的。

  • 在分类中也可以实现initialize方法,只不过分类中的initialize方法会覆盖主类中的initialize方法,如果在多个分类中同时实现了initialize方法,那么只有最后被编译的那个分类的initialize方法会被调用。它会强制先向父类先发送 +initialize。

  • 与 load 不同,initialize 方法调用时,所有的类都已经加载到了内存中。

  • 其作用也非常局限,一般我们只会在 initialize 方法中进行一些常量的初始化。

+ (void)initialize {
if (self == [FLAnimatedImage class]) {
//......
}
}
+load

+load 方法相对于 +initialize 来说更为常见它是我们开发过程中可以接触到的调用时机最靠前的方法,和initialize不同的是它的调用不是惰性的,在主函数运行之前,load 方法就会被调用,并且它只会在程序调用期间调用一次,最重要的是如果在类与分类中都实现了 load 方法,它们都会被调用,不像+initialize 在分类中实现的方法会被覆盖,但是在使用load方法的时候需要注意,由于load方法的运行时间过早,所以可能不是一个理想的环境,因为它不能保证某些类可能需要在在其它类之前加载,但是在这个时间点,所有的 framework 都已经加载到了运行时中,所以调用 framework 中的方法都是安全的,同时需要注意的是重载Class 的 +load 方法时不能调父类super。

load / initialize 对比

方法 load initialize
是否惰性 否,都会被调用,并且只会被调用一次 是,只有在第一次向某个对象发送消息的时候调用
调用时机 main之前 main之后,第一次向某个对象发送消息的时候
在分类中的行为 在分类中和在主类中都会被调用,不会发生覆盖,并且先父类,再主类,再父类分类,主类分类 分类中的会覆盖主类中的initialize,所有分类中只有最后编译的那个生效,会强制先向父类先发送 +initialize
在调用的时候是否全部类已经加载完毕
attribute

关于__attribute__有很多很好的文档,大家如果看了还不大明白的话可以在下面文档中找答案,这里只想列出在开发过程中比较常用的几个:

  • attribute((objc_requires_super))

有时候我们自己编写的框架需要给别人用的时候,某个方法在使用的时候必须先调用父类的方法,一种做法是写注释,但是如果使用者不看注释就gg了,所以比较好的方式是使用__attribute__((objc_requires_super)) 如果我们没有调用父类的这个方法编译器就会发出警告。

  • attribute((unavailable(“not available”)))

告诉编译器方法不可用,如果使用了就会编译失败,提示错误。比如自定义了初始化方法,为了防止别人调用init初始化,就可以使用它来提示错误,这个在开发中也很常见,但是一般写成:

- (instancetype)init NS_UNAVAILABLE; 
  • attribute((deprecated(“该方法已经废弃”)))

用于在编译时提示方法过时:

- (void)deprecatedMethod:(NSString *)string __attribute__((deprecated("该方法已经废弃,请使用xxx方法")));
- (void)deprecatedMethod DEPRECATED_ATTRIBUTE;
  • attribute((cleanup))

声明到一个变量或者block上,当这个变量作用域结束时,调用指定的一个函数. 这个个人在时机工作中用得不多,大家可以看sunnyxx的黑魔法__attribute__((cleanup))这篇博客。

  • attribute((overloadable))

这个个人在时机工作中也用得不多,它主要用于C语言函数,可以定义若干个函数名相同,但参数不同的方法,调用时编译器会自动根据参数选择函数原型.

__attribute__((overloadable)) void print(NSString *string){
NSLog(@"%@",string);
}

__attribute__((overloadable)) void print(int num){
NSLog(@"%d",num);
}
  • attribute((objc_runtime_name(“NSObject”)))

这个用于将类或协议的名字在编译时指定成另一个,这个没用过。

__attribute__((objc_runtime_name("NSObject")))
@interface IDLObject :NSObject
  • attribute((objc_subclassing_restricted))

这个用于禁止被继承的时候使用,很少用到。

  • attribute((constructor)) / attribute((destructor))

最后介绍的是我们开发过程中经常用到的两个,在用之前需要和load区分开来,我们在将到load方法的时候有提到它的不足之处是它调用的时候某些类可能还没有被加载完成,所以不能在load方法中使用我们自己的类,这样很可能类都没被加载起来。那么__attribute__((constructor))的好处是什么呢?在__attribute__((constructor))标志的方法执行的时候,所有 Class 都已经加载完成,所以可以使用任意的类,并且它可以单独放在一个文件中,不用挂载在一个 Class 中,它和load的方法一样都是在main还未执行的时候调用。如果有多个 constructor 并且想按照指定顺序执行的话,可以写成 attribute((constructor(101)))这种形式,1 ~ 100 为系统保留,所以一般以101 为开始,里面的数字越小优先级越高。

[推荐文档]

我们知道Objective C 是一门动态语言,它是在编译器和Runtime共同协作下完成的,编译时期决定了向哪个对象发送哪个消息,但是这个对象在收到具体消息的时候如何处理是由Runtime决定的,Runtime对消息的处理包括了消息的发送和消息的转发。我们接下来就来看下这部分处理:

在Objective C代码编译期间当遇到一个方法调用的时候,编译器会产生一个对objc_msgSendobjc_msgSend_stretobjc_msgSendSuperobjc_msgSendSuper_stret的函数调用

  • 如果调用的是[super xxxx] 那么将会调用objc_msgSendSuper objc_msgSendSuper_stret 也就是有带super的函数
  • 如果调用的是[obj xxxxx] 那么将会调用objc_msgSend objc_msgSend_stret
  • 如果返回的值是带有结构的将会调用 objc_msgSendSuper_stret objc_msgSend_stret 也就是带有stret的。

为了简单起见我们这里只研究objc_msgSend的情况:

OBJC_EXPORT id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这里的第一个参数为消息的接收对象,第二个参数为SEL,可以看成是一个消息。

objc_msgSend底层代码是通过汇编实现的:

    ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//判空 如果检测方法的接受者是nil,那么系统会自动clean并且return。
NilTest NORMAL
//可以快速地获取到对象的 isa 指针地址放到 r11 寄存器
GetIsaFast NORMAL // r10 = self->isa
//在缓存中查找IMP 如果查找成功直接调用 IMP
CacheLookup NORMAL, CALL // calls IMP on success
//检查是否返回值为空
NilTestReturnZero NORMAL

GetIsaSupport NORMAL

//查找缓存失败,在方法列表中查找
LCacheMiss:
// isa still in r10
jmp __objc_msgSend_uncached
END_ENTRY _objc_msgSend

上面的汇编代码会通过isa找到消息接收者的objc_class,然后通过CacheLookup汇编代码在objc_class 方法缓存中查找,是否有和当前SEL匹配的方法缓存,如果有直接调用对应的IMP,如果没有则走LCacheMiss分支,它会跳转到__objc_msgSend_uncached中:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band r10 is the searched class

// 在方法表中查找对应的IMP
// r10 is already the class to search
MethodTableLookup NORMAL // r11 = IMP
// 跳到对应的IMP去执行
jmp *%r11 // goto *imp

END_ENTRY __objc_msgSend_uncached

在__objc_msgSend_uncached会调用MethodTableLookup从方法列表中查找:

.macro MethodTableLookup
//......

.if $0 == NORMAL
// receiver already in a1
// selector already in a2
.else
movq %a2, %a1
movq %a3, %a2
.endif
movq %r10, %a3
// 调用__class_lookupMethodAndLoadCache3
call __class_lookupMethodAndLoadCache3

// IMP is now in %rax
movq %rax, %r11

//......

.endmacro

这里会调用****__class_lookupMethodAndLoadCache3**** 进行查找。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

_class_lookupMethodAndLoadCache3 是专门提供给汇编调用的不需要查找方法缓存,直接查找objc_class方法列表的方法。所以这里调用lookUpImpOrForward时候cache传的是NO.

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;

//确保进入的时候是没锁住的
runtimeLock.assertUnlocked(); //这个是加一个读写锁,保证线程安全。

//优化缓存查找传入的参数cache 用于表示是否找到cache的布尔量,
//从_class_lookupMethodAndLoadCache3进来的是在缓存中已经找过并且没找到的情景,这时候cache为NO
if (cache) {
//如果传入的是YES,那么就会调用cache_getImp方法去找到缓存里面的IMP,注意cache_getImp是通过汇编实现的,cache_getImp会把找到的IMP放在r11中
imp = cache_getImp(cls, sel);
if (imp) return imp;
}

//锁住读写锁
runtimeLock.lock();
//.......
//如果已经初始化会在objc_class对应的标识位设置为true
if (!cls->isRealized()) {
//实例化类结构
realizeClass(cls);
}

// +initialize就是在这个阶段调用的
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
//_class_initialize是类初始化的过程。它会发送一个initialize消息给当前类
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
//如果我们发送的sel就是initialize那么这里的_class_initialize会发送一次+initialize,后续还会发送一次+initialize,但是这种情况很少见
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}


retry:
runtimeLock.assertLocked();

// 从缓存中查找方法实现
imp = cache_getImp(cls, sel);
if (imp) goto done;

//尝试在本类的方法列表中查找
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
//找到的情况下添加到缓存并返回方法实现
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
//尝试在父类的缓存和方法列表中查找
{
//如果以上尝试都失败了,接下来就会循环尝试父类的缓存和方法列表。一直找到NSObject为止。因为NSObject的superclass为nil,才跳出循环。
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
//没有找到实现方法,尝试寻找方法的解决者
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
//如果父类找到NSObject还没有找到,那么就会开始尝试_class_resolveMethod方法。
//注意,这些需要打开读锁,因为开发者可能会在这里动态增加方法实现,所以不需要缓存结果。
//此处虽然锁被打开,可能会出现线程问题,所以在执行完_class_resolveMethod方法之后,会goto retry,重新执行一遍之前查找的过程。
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
//寻找用户指定的方法的解决者
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
//重新查找
goto retry;
}

// No implementation found, and method resolver didn't help.
// Use forwarding.
// 消息转发
imp = (IMP)_objc_msgForward_impcache;
//---> __objc_msgForward_impcache --> __objc_msgForward --> __objc_forward_handler --> objc_defaultForwardHandler
//在cache_fill中还会去调用cache_fill_nolock函数,如果缓存中的内容大于容量的 3/4就会扩充缓存,使缓存的大小翻倍。找到第一个空的 bucket_t,以 (SEL, IMP)的形式填充进去。
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();

return imp;
}

lookUpImpOrForward 会先根据实际情况看要不要从缓存中查找,然后再从objc_class的方法列表中查找,如果还没有找到再从父类的缓存和方法列表中找,一直找到NSObject如果发现还没有,那么就调用_class_resolveMethod,这时候_class_resolveMethod可能会通过代码为该类增加方法实现,所以在_class_resolveMethod结束之后还需要通过goto retry走一遍上面流程。上面的resolve过程有些地方称为动态方法决议,下面给出一个简单例子:

void customMethodIMP(id self, SEL _cmd) {
NSLog("customMethodIMP running");
}

@implementation IDLResolveTestClass

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if (aSEL == @selector(unimplementSelector)) {
class_addMethod([self class], aSEL, (IMP) customMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end

上面例子中会判断如果向当前对象发送unimplementSelector消息的时候,resolveInstanceMethod会通过class_addMethod动态完类中添加unimplementSelector的IMP customMethodIMP,并返回YES,由于前面介绍了这个流程走完还会走一遍goto retry,所以就会在goto retry中找到customMethodIMP并执行。

如果方法决议还没有找到就准备走消息转发机制了,也就是:

imp = (IMP)_objc_msgForward_impcache;

先结合下面这张图有个大致的认知下,后面会整理个比较全面的流程图:

这里同时需要注意一点:初始化方法+initialize就是在这个阶段调用的。也就是在第一次往某个对象发送消息的时候如果没有初始化就会调用+initialize。

前面我们了解到缓存查找cache_getImp是汇编实现的,我们接着来看下类方法列表的查找是怎样的:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();

assert(cls->isRealized());
//在getMethodNoSuper_nolock方法中,会遍历一次methodList链表,从begin一直遍历到end。遍历过程中会调用search_method_list函数。
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}

return nil;
}
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
//在search_method_list函数中,会去判断当前methodList是否有序,如果有序,会调用findMethodInSortedMethodList方法,这个方法里面的实现是一个二分搜索
//如果非有序,就调用线性的傻瓜式遍历搜索。
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
//.....
return nil;
}

这里需要注意的是search_method_list方法,它会去判断当前methodList是否有序,如果有序,会调用findMethodInSortedMethodList方法,这个方法里面的实现是一个二分搜索,如果是无序的,就调用线性的傻瓜式遍历搜索。

消息查找已经大致介绍完了,如果这时候没有找到方法,就会进入下个阶段消息转发阶段了,消息转发顾名思义就是在当前对象不能处理当前消息的情况下将消息转给能够处理的对象进行处理。

我们紧接着上面_objc_msgForward_impcache来讲:

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b __objc_msgForward

END_ENTRY __objc_msgForward_impcache


ENTRY __objc_msgForward

adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward

__objc_msgForward_impcache 内部很简单就是调用了__objc_msgForward,而在__objc_msgForward中也只是简单得转调了__objc_forward_handler。但是期间还有一系列的调用,我们没有办法看到源码,这部分大家可以看下杨萧玉的 Objective-C 消息发送与转发机制原理,这里为避免复杂化我们直接介绍结论:

在我们通过动态方法决议给类添加缺失的方法无效的情况下,Runtime就会继续调用****-(id)forwardingTargetForSelector:(SEL)aSelector,这个方法会根据传入的selector,返回一个备用的target,这个消息会被转发给它进行处理,如果-(id)forwardingTargetForSelector:(SEL)aSelector**** 返回nil 或者self,就会进入下一步。运行时系统首先会调用****-(NSMethodSignature )methodSignatureForSelector:(SEL)aSelector方法来获得记录了方法的参数和返回值的信息的方法签名。如果methodSignatureForSelector*** 返回的是nil, 运行时系统会抛出unrecognized selector exception
程序到这里就结束了否则会继续调用 -(void)forwardInvocation:(NSInvocation )anInvocation。我们这里要看下NSInvocation*有哪些功能,才能知道forwardInvocation 可以做哪些工作,NSInvocation定义如下:

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end

这里最关键的属性包括methodSignaturetargetselector,方法部分主要可以分成三类,参数设置获取部分,返回值设置获取部分,以及触发部分,触发可以通过invokeWithTarget交给某个具体目标对象执行。

接着我们再来看下NSMethodSignature:

@interface NSMethodSignature : NSObject

+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;

@property (readonly) NSUInteger numberOfArguments;
- (const char *)getArgumentTypeAtIndex:(NSUInteger)idx NS_RETURNS_INNER_POINTER;

@property (readonly) NSUInteger frameLength;

- (BOOL)isOneway;

@property (readonly) const char *methodReturnType NS_RETURNS_INNER_POINTER;
@property (readonly) NSUInteger methodReturnLength;

@end

它主要包括了方法参数和返回值的信息获取。最后以一个流程图来做个总结:

整个过程可以分成如下三个阶段:

  1. 消息查找

这个阶段开始之前会进行一系列的检查,比如检查当前selector是否是忽略的selector,检查target是否为空。然后查找当前对象的缓存是否有当前的要查找的对象,如果没有则进入方法列表的查找阶段,这个阶段会先在当前对象的方法列表中查找是否有待查找的方法,如果没有则顺着继承链往上找,先找父类的方法缓存,再找方法列表。一直找到NSObject都没找到的时候,就进入消息动态决议阶段。

  1. 消息动态决议

消息动态决议实际上是通过resolveInstanceMethod针对当前的消息,添加一个补充的IMP,然后再重新走消息查找的流程。如果还没找到则进入消息转发流程。

  1. 消息转发

消息转发可以分成两种,第一种通过forwardingTargetForSelector它不对消息参数和返回值类型做处理单纯转发给某个对象,这种比较简单,但是灵活性不如后者。

第二种通过forwardInvocation它可以通过methodSignatureForSelector改写方法签名,并在forwardInvocation中对参数数值进行修改,然后再调用invokeWithTarget将消息转发给某个特定的对象,在拿到返回值的时候还可以对返回值进行处理,可以说这是最灵活的一种方式了,JSPatch以及Aspect都是基于这种方式实现的。

前面我们介绍了Mach O的结构,App的启动,dyld的加载,以及通过dyld将镜像加载进来,经过rebase/Bind处理后,找到main入口,以及Runtime相关的数据结构,有了前面的一系列铺垫这里介绍Runtime初始化就显得相对轻松了,这篇博客我们从《iOS Runtime源码解析之dyld》结尾处接着介绍,《iOS Runtime源码解析之dyld》中介绍了从dyld入口__dyld_start作为起点,到找到并跳转到主函数入口期间的一系列工作:

1. 将主程序初始化为imageLoader
2. 加载共享库到内存
3. 加载插入的动态库
4. 链接主程序,链接插入库
5. 初始化主程序,插入库
6. 寻找主程序入口点

这篇博客和《iOS Runtime源码解析之dyld》的衔接点就在于第5步初始化主程序,插入库这一步,在这个阶段,会调用使用attribute((constructor) 进行修饰的方法,其中有个十分重要的动态库初始化方法,libSystem_initializer

static __attribute__((constructor)) 
void libSystem_initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct ProgramVars* vars) {

//......

_libkernel_init(libkernel_funcs);

bootstrap_init();
mach_init();
pthread_init();
__libc_init(vars, libSystem_atfork_prepare, libSystem_atfork_parent, libSystem_atfork_child, apple);
__keymgr_initializer();
_dyld_initializer();
libdispatch_init();
_libxpc_initializer();

__stack_logging_early_finished();
//.......
errno = 0;
}

在libSystem.dylib的初始化方法libSystem_initializer中初始化了多了dylib库,比如:liblaunch.dylib,libc.a,libdispatch.a等等,这里最关键的是****libdispatch_init****:

void libdispatch_init(void) {
//......
_os_object_init();
}

在libdispatch_init的最后部分会调用****_os_object_init****:

void
_os_object_init(void)
{
return _objc_init();
}

_os_object_init方法只有一行代码就是转调_objc_init,这就是我们十分关注的Runtime 初始化的入口。


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();
lock_init();
exception_init();

//_dyld_objc_notify_register(&map_2_images, load_images, unmap_image);
//向dyld注册了回调函数,当imagemap到内存中,当初始化完成image时和卸载image的时候都会回调注册者
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

_objc_init向dyld注册了map_images,load_images,unmap_image三个关键的回调函数,各个关键阶段节点常量定义如下:

enum dyld_image_states
{
dyld_image_state_mapped = 10, // No batch notification for this
dyld_image_state_dependents_mapped = 20, // Only batch notification for this
dyld_image_state_rebased = 30,
dyld_image_state_bound = 40,
dyld_image_state_dependents_initialized = 45, // Only single notification for this
dyld_image_state_initialized = 50,
dyld_image_state_terminated = 60 // Only single notification for this
};

我们看下****_dyld_objc_notify_register****这个方法的注释,通过注释我们可以了解到,在runtime中可以通过这个方法可以向dyld注册用于处理镜像完成映射,取消映射和初始化之后的处理方法。

//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
// 通过这个方法可以向dyld注册用于处理镜像完成映射,取消映射和初始化之后的处理方法。

void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);

我们重点关注下****_dyld_objc_notify_mapped以及_dyld_objc_notify_init****,

  • _dyld_objc_notify_mapped

map_images会在主程序以及其他库加载进来后调用ImageLoader::setMapped发出通知,调用map_images:

void ImageLoader::setMapped(const LinkContext& context)
{
fState = dyld_image_state_mapped;
context.notifySingle(dyld_image_state_mapped, this); // note: can throw exception
}

ImageLoader::setMapped会在instantiateMainExecutable实例话主程序,instantiateFromFile实例化镜像的时候被调用:

  • _dyld_objc_notify_init

会在ImageLoader::runInitializers 方法中被调用,而ImageLoader::runInitializers 则会在 initializeMainExecutable中调用。

也即是说在实例化主程序或者其他dylib库的时候都会发出_dyld_objc_notify_mapped通知,这时候会调用runtime 的 map_images方法进行后续处理,在这些初始化完成的时候会发出_dyld_objc_notify_init通知调用load_images方法进行后续处理。

我们紧接着来看下map_images以及load_images的处理,其实在介绍分类的时候已经介绍过了map_images这里为保证整个文章的完整性再过一遍这部分内容:

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

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
//......
if (hCount > 0) {
//读取镜像信息
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
}

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//......

// =================================查找classes=================================
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
for (EACH_HEADER) {
//从Mach-O的 __DATA区 __objc_classlist 获取所有类,并加入gdb_objc_realized_classes list中
classref_t *classlist = _getObjc2ClassList(hi, &count);
//.......
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");

// 修复重新映射的类
// 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");

// Fix up @selector references
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->isPreoptimized()) 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]);
// 3. 注册Sel,并存储到全局变量namedSelectors的list中
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
}

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

//.....

// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
//找到所有Protocol并处理引用
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}

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) {
protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
for (i = 0; i < count; i++) {
remapProtocolRef(&protolist[i]);
}
}

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

// Realize non-lazy classes (for +load methods and static instances)
for (EACH_HEADER) {
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
addClassTableEntry(cls);
realizeClass(cls);
}
}

ts.log("IMAGE TIMES: realize non-lazy classes");

// Realize newly-resolved future classes, in case CF manipulates them
if (resolvedFutureClasses) {
for (i = 0; i < resolvedFutureClassCount; i++) {
realizeClass(resolvedFutureClasses[i]);
resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
}
free(resolvedFutureClasses);
}

ts.log("IMAGE TIMES: realize future classes");

// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
//查看是否包含属性
bool hasClassProperties = hi->info()->hasCategoryClassProperties();

for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
//获取到所属的类
Class cls = remapClass(cat->cls);

if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}

// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
//如果有实例方法,协议或者实例属性
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
//将分类添到属性,方法,协议添加到对应的类中
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
//如果有类方法,协议或者类属性
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}

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

// Category discovery MUST BE LAST to avoid potential races
// when other threads call the new category code before
// this thread finishes its fixups.

// +load handled by prepare_load_methods()

if (DebugNonFragileIvars) {
realizeAllClasses();
}
//......
}

map_images 最关键的代码在_read_images方法中,****_read_images故名思议就是读取镜像,在这个方法中会从镜像的_DATA区域通过_getObjc2XXXX****将该镜像的类列表,分类列表,协议列表读取出来,对应的方法以及读取的session部分可以查看如下声明,这里需要提一下_DATA区域中的这些session数据是由编译器写入的。

//      function name                 content type     section name
GETSECT(_getObjc2SelectorRefs, SEL, "__objc_selrefs");
GETSECT(_getObjc2MessageRefs, message_ref_t, "__objc_msgrefs");
GETSECT(_getObjc2ClassRefs, Class, "__objc_classrefs");
GETSECT(_getObjc2SuperRefs, Class, "__objc_superrefs");
GETSECT(_getObjc2ClassList, classref_t, "__objc_classlist");
GETSECT(_getObjc2NonlazyClassList, classref_t, "__objc_nlclslist");
GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
GETSECT(_getObjc2NonlazyCategoryList, category_t *, "__objc_nlcatlist");
GETSECT(_getObjc2ProtocolList, protocol_t *, "__objc_protolist");
GETSECT(_getObjc2ProtocolRefs, protocol_t *, "__objc_protorefs");
GETSECT(getLibobjcInitializers, UnsignedInitializer, "__objc_init_func");

这个方法的每个阶段结束都会调用ts.log(“IMAGE TIMES:XXXXXX)来提示每个阶段结束,在这些阶段中有我们之前分析过的Catogies 加载,以及协议加载。我们重点看下最末尾的realizeAllClasses方法:

static void realizeAllClasses(void)
{
runtimeLock.assertLocked();
header_info *hi;
for (hi = FirstHeader; hi; hi = hi->getNext()) {
realizeAllClassesInImage(hi);
}
}
static void realizeAllClassesInImage(header_info *hi) {

runtimeLock.assertLocked();

size_t count, i;
classref_t *classlist;

if (hi->areAllClassesRealized()) return;

classlist = _getObjc2ClassList(hi, &count);

for (i = 0; i < count; i++) {
realizeClass(remapClass(classlist[i]));
}

hi->setAllClassesRealized(YES);
}

realizeAllClasses会对镜像中的所有class调用realizeClass方法。

static Class realizeClass(Class cls) {

//.....
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;

if (!cls) return nil; //cls 不能为空
if (cls->isRealized()) return cls; //cls 如果已经初始化直接返回
assert(cls == remapClass(cls)); //cls 没有重新分配,remapClass 返回指向cls的实时指针
// 【✨】获取只读结构体
ro = (const class_ro_t *)cls->data();

// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); //分配读写数据
rw->ro = ro; //只写数据
rw->flags = RW_REALIZED|RW_REALIZING; //设置已经初始化标志
cls->setData(rw); //为cls设置data数据

isMeta = ro->flags & RO_META; //判断是否是元类
rw->version = isMeta ? 7 : 0; // old runtime went up to 6 //版本信息,旧版本的版本信息为6

//.....
// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
// 【✨】为supercls,metacls 分配空间
supercls = realizeClass(remapClass(cls->superclass));
metacls = realizeClass(remapClass(cls->ISA()));

//......

// 【✨】 Update superclass and metaclass in case of remapping
cls->superclass = supercls; //将supercls赋给 cls->superclass
//将上面分配的metacls赋给cls
cls->initClassIsa(metacls);

// 调整实例变量的偏移和布局,这个将会重新分配class_ro_t
// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);

//设置对象尺寸
// Set fastInstanceSize if it wasn't set already.
cls->setInstanceSize(ro->instanceSize);

//【✨】从ro中拷贝部分标志位到rw 字段
// Copy some flags from ro to rw
if (ro->flags & RO_HAS_CXX_STRUCTORS) {
cls->setHasCxxDtor();
if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
cls->setHasCxxCtor();
}
}

// 【✨】 Connect this class to its superclass's subclass lists
// 将当前class与父类相关连
if (supercls) {
//将当前类作为supercls的子类添加到父类的子类列表
addSubclass(supercls, cls);
} else {
//将当前类作为根类
addRootClass(cls);
}

//【✨】实例化类结构
// Attach categories
// 使得类有条理
methodizeClass(cls);

return cls;
}

realizeClass 方法实际上是为类的class_rw_t,superclass,metacls等分配空间,并初始化。在最后的时候会调用methodizeClass进行进一步的初始化:

static void methodizeClass(Class cls)
{
//检查锁
runtimeLock.assertLocked();
//是否是元类
bool isMeta = cls->isMetaClass();
//获取可读写字段
auto rw = cls->data();
//获取只读字段
auto ro = rw->ro;

// Install methods and properties that the class implements itself.
// 加载类自身实现的方法和属性
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list/*自身实现的方法列表*/, 1, YES, isBundleClass(cls));
//将ro->baseMethods方法添加到rw->methods
rw->methods.attachLists(&list, 1);
}

property_list_t *proplist = ro->baseProperties;
if (proplist) {
//将ro->baseProperties方法添加到rw->properties
rw->properties.attachLists(&proplist, 1);
}

protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
//将ro->baseProtocols方法添加到rw->protocols
rw->protocols.attachLists(&protolist, 1);
}

// Root classes get bonus method implementations if they don't have
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}

// Attach categories.
// 获取未添加的分类
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
// 添加分类
attachCategories(cls, cats, false /*don't flush caches*/);

//.....

if (cats) free(cats);
//.....
}

methodizeClass方法则进一步初始化rw,这一步将ro中的属性,协议,方法拷贝到rw,以及将分类中对应的属性,协议,方法也拷贝到class中的rw对应字段。

回顾整个map_image阶段就是在镜像实例话结束后,通过读取_DATA中的指定session数据,来读出整个镜像中各个class的关键数据,并构建objc_class数据结构对象,通过从_DATA中读取的数据来填充objc_class来完成整个类的实例化。

  • load_images

load方法是我们在日常开发中可以接触到的调用时间最靠前的方法,它的调用不是惰性的,在主函数运行之前,load 方法就会调用,并且它只会在程序调用期间调用一次,最重要的是,如果在类与分类中都实现了 load 方法,它们都会被调用,
不像其它的在分类中实现的方法会被覆盖,但是在使用load方法的时候需要注意,由于load方法的运行时间过早,所以可能不是一个理想的环境,因为它不能保证某些类可能需要在在其它类之前加载,但是在这个时间点,所有的 framework 都已经加载到了运行时中,所以调用 framework 中的方法都是安全的,同时需要注意的是重载Class 的 +load 方法时不能调父类super,我们来看下这部分逻辑:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
//......
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
//prepare_load_methods方法中对load方法进行了前期准备
prepare_load_methods((const headerType *)mh);
}
// 调用+load方法
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}

load_images 方法中主要调用了prepare_load_methods 以及 call_load_methods

void prepare_load_methods(const headerType *mhdr)
{
//....
//获取类
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}

//添加分类
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize

if (cls->data()->flags & RW_LOADED) return;

//先加载父类
// Ensure superclass-first ordering
//分析这段代码,可以知道,在将子类添加到加载列表之前,其父类一定会优先加载到列表中。
//这也是为何父类的+load方法在子类的+load方法之前调用的根本原因。
schedule_class_load(cls->superclass);
//将类添加到可加载列表
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}

prepare_load_methods 以及 schedule_class_load方法会按照父类,子类,分类的顺序将需要调用load方法的class添加到loadable_list中去

void add_class_to_loadable_list(Class cls)
{
IMP method;

loadMethodLock.assertLocked();

//该类是否有load方法
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method

//........
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}

loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
void add_category_to_loadable_list(Category cat)
{
IMP method;

loadMethodLock.assertLocked();

//该分类是否有load方法
method = _category_getLoadMethod(cat);

if (!method) return;

if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}

loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}

调用类以及分类中的load方法

/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
* Class +load methods are called superclass-first.
* Category +load methods are not called until after the parent class's +load.
*
* This method must be RE-ENTRANT, because a +load could trigger
* more image mapping. In addition, the superclass-first ordering
* must be preserved in the face of re-entrant calls. Therefore,
* only the OUTERMOST call of this function will do anything, and
* that call will handle all loadable classes, even those generated
* while it was running.
*
* The sequence below preserves +load ordering in the face of
* image loading during a +load, and make sure that no
* +load method is forgotten because it was added during
* a +load call.
* Sequence:
* 1. Repeatedly call class +loads until there aren't any more
* 2. Call category +loads ONCE.
* 3. Run more +loads if:
* (a) there are more classes to load, OR
* (b) there are some potential category +loads that have
* still never been attempted.
* Category +loads are only run once to ensure "parent class first"
* ordering, even if a category +load triggers a new loadable class
* and a new loadable category attached to that class.
*
* Locking: loadMethodLock must be held by the caller
* All other locks must not be held.
**********************************************************************/
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
//调用类的load方法
call_class_loads();
}

// 2. Call category +loads ONCE
//调用分类的load方法
more_categories = call_category_loads();

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}
static void call_class_loads(void)
{
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;/*这是preppare阶段构造的*/
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;

load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;

if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
//调用+load方法
(*load_method)(cls, SEL_load);
}

// Destroy the detached list.
if (classes) free(classes);
}
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;

// Detach current loadable list.
struct loadable_category *cats = loadable_categories;/*数据来源 在prepare阶段构建*/
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;

cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}

// Compact detached list (order-preserving)
shift = 0;
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i-shift] = cats[i];
} else {
shift++;
}
}
used -= shift;

// Copy any new +load candidates from the new list to the detached list.
new_categories_added = (loadable_categories_used > 0);
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) {
allocated = allocated*2 + 16;
cats = (struct loadable_category *)
realloc(cats, allocated *
sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[i];
}

// Destroy the new list.
if (loadable_categories) free(loadable_categories);

// Reattach the (now augmented) detached list.
// But if there's nothing left to load, destroy the list.
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else {
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}

if (PrintLoading) {
if (loadable_categories_used != 0) {
_objc_inform("LOAD: %d categories still waiting for +load\n",
loadable_categories_used);
}
}

return new_categories_added;
}

我们在这篇博客的最后对整个过程进行一个简单的总结:

在应用启动的时候会从镜像中查找dyld的地址,而后将dyld加载进来,找到dyld的入口地址__dyld_start,并将后续工作交给 dyld 负责:

  1. dyld 开始将程序二进制文件实例化为一个ImageLoader
  2. 交由 ImageLoader 读取 image 其中包含了我们的类、方法等各种符号,以及根据Mach-O的Load Commands段加载所有依赖的动态库并完成链接,初始化工作。在主程序初始化阶段,runtime会向dyld绑定回调。
  3. 当image加载到内存后,dyld 会通知 runtime 进行处理,runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,
    遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法
  4. 当map_images以及load_images执行完毕之后可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理。
  5. 所有初始化工作结束后,dyld 调用真正的 main 函数,紧接着dyld 会清理现场,将调用栈回归,只剩下main函数。

很早之前写过图解Runtime系列,那时候比较忙只是画了流程图,当然对自己理解已经够用了,但是对于没有看过源码对同学可能会看不懂,想着一个系列的博客都快完成了,少了这块总觉得缺了什么,并且Runtime是iOS的核心对理解底层有很大帮助,所以找了个空余时间将这块补上了。

我们知道编程语言有静态语言和动态语言之分,静态语言在编译的时候就已经明确了每行代码最终执行哪些代码,Objective C 作为动态语言它的底层是由编译器和Runtime构成,编译时期只是决定向某个对象发送某个消息,但是最终这个对象怎么处理这个消息取决于这个对象而不是固定的,也就是说在编译之后还可以针对发过来的消息对这个消息进行一系列处理最终决定执行哪些代码,这部分工作都交由Runtime处理。在介绍Runtime细节之前我们先看下相关的数据结构,熟悉这些数据结构对理解Runtime来说是必不可少的。本文将以Objc 2.0 中的数据结构为研究对象。

我们上层最经常接触的应该算是Classid这两个类型了,它实际上分别是****objc_class * objc_object * ****的重定义类型。

typedef struct objc_class *Class;
typedef struct objc_object *id;
  • objc_object 结构

objc_object里面有个重要的成员对象isa_t isa,因为objc_object是用来存储对象数据用的,所以不宜将无用的数据存储在objc_object上,所以这里只存放了isa_t isa,通过isa_t isa可以找到objc_class,objc_class中存放的才是这些对象公用的数据。

struct objc_object {
private:
isa_t isa;
//.....
};
  • objc_class 结构

objc_class结构如下所示:

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
//.....
};

我们从它的定义上看,它其实也是一个objc_object,所以objc_class也会有一个isa,它其实指向的是Meta class,Meta class 也是一个objc_class它和objc_object指向objc_class区别在于它的cache以及bits中存放的是类静态方法等数据。这些会在后面介绍。

  • isa_t 结构
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

isa_t 是一个共用体,在64位机器中会启用NONPOINTER_ISA优化,具体见谈谈iOS的内存管理方式的理解,这时候isa就不单纯用于存储指向某个类的指针。那么当启用NONPOINTER_ISA优化的时候作用域是怎样的呢?我们这里看到****#if defined(ISA_BITFIELD)****,这个条件编译,ISA_BITFIELD是在isa.h中定义的我们来看下它的具体定义:

# if __arm64__
//.........
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19

下面是这些字段的意义:

  • nonpointer (1位) 标记是否是纯的ISA指针,还是非指针型的NOPOINTER_ISA指针indexed,0表示普通的isa,1表示NOPOINTER_ISA指针

  • has_assoc (1位) 表示该对象是否包含 associated object,如果没有,则析构时会更快

  • has_cxx_dtor (1位) 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快

  • shiftcls (33位) 类的指针

  • magic(6位) 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化

  • weakly_referenced(1位) 标记对象是否有弱引用指针weakly_referenced

  • deallocating (1位) 该对象是否正在析构

  • has_sidetable_rc (1位) 是否使用了引用计数表sideTable

  • extra_rc(19位) 存储引用计数值减一后的值 (首先会存储在该字段中,当到达上限后,has_sidetable_rc 等于1,对应的引用计数值存入相应的引用计数表中)

  • cache_t 结构

cache_t主要用于存储某个类中使用过方法的缓存,一般一个类会有很多方法,这些方法并不都是常用的,如果每次调用都需要从该类的所有方法列表中查询明显会降低效率,所以这里引入了cache_t,通过这种以空间换时间的方式来加快方法查找效率。

struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
//......
};

cache_t中包含三个属性:

_buckets 指向bucket_t结构体,它实际上是一个可动态扩展的哈希表。在存储的数据超过3/4的时候就会调用expend方法进行扩展。****_mask**** 表示整个_buckets链表的大小,****_occupied****表示当前_buckets链表里面缓存的bucket_t节点数。

struct bucket_t {
private:
MethodCacheIMP _imp;
cache_key_t _key;
}

bucket_t 结构也十分简单,就是一个以selector映射而成的cache_key_t以及方法指针_imp。

  • class_data_bits_t 结构

我们再继续看objc_class中的class_data_bits_t类型:

struct class_data_bits_t {

uintptr_t bits;

//.....
public:

class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}

//......
}

这里最关键的数据为class_rw_t,它才是包含类最有用信息的部分,我们从名字上可以看出它是可读写的,为什么要强调它是可读写的,因为它里面还有一个十分重要的class_ro_t类型。这个我们后面再介绍,我们先将重心放在class_rw_t中:

struct class_rw_t {

uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}

//........
};

  • methods对象或者类的方法,这部分是可读写的,在Runtime阶段,会将分类的中的方法追加到这部分。
  • properties对象或者类的属性
  • protocols对象或者类的协议
  • ro对象或者类中只读的部分,这部分在编译时期就已经确定了。

我们再来看下class_ro_t结构:

struct class_ro_t {

const char * name;
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;

const ivar_list_t * ivars;
const uint8_t * ivarLayout;
const uint8_t * weakIvarLayout;

method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
property_list_t *baseProperties;

//......
};

它包括实例起始位置instanceStart,以及实例大小instanceSize
成员变量相关:ivarsivarLayoutweakIvarLayout
基础成分:baseMethodListbaseProtocolsbaseProperties

  • SEL 定义

SEL和id Class 一样都是objc对应类型的结构体指针的便捷定义。

typedef struct objc_selector *SEL;

objc_selector是一个映射到方法的C字符串,不同类中相同名字的方法所对应的selector是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的selector。由于这点特性,导致了OC不支持函数重载。简单说SEL不包含返回值类型,参数类型,以及所属类的信息。只包含方法名对应的信息,实际上它在Runtime中作为消息发送给对象,对象根据它来查找对应的 IMP进行执行。

  • IMP 定义

IMP是函数指针,即函数执行的入口。该函数使用标准的C调用。

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

它包含两个参数:

  • 第一个参数指向 self(它代表当前类实例的地址,如果是类则指向的是它的元类),作为消息的接受者;

  • 第二个参数代表方法的选择子;

  • … 代表可选参数,前面的 id 代表返回值。

  • Method 定义
typedef struct method_t *Method;
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
//.....
};

我们知道一个方法的组成包括方法返回值,方法参数,方法名,方法实现体,对应于method_t分别是typesnameimp,关于types涉及到类型编码,如果大家不了解可以通过如下链接进行扩展阅读:

在实际编程中可以通过method_getTypeEncoding方法来获得:

const char * _Nullable method_getTypeEncoding(Method _Nonnull m)

  • protocol_t 定义

protocol_t 主要用于存放协议相关的数据(注意它其实也是一个objc_object),由于协议也可以遵循协议,所以协议内部有协议列表用于存放当前协议所遵循的协议。而实例方法和类方法都包含必须实现和可选实现的部分,这两部是分开存放的,同时需要注意的是协议内部也为实例方法中提供了属性的存储,实际上协议也可以声明属性的,在介绍协议的使用的时候会给予介绍。

struct protocol_t : objc_object {

//....
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
//.......
};
  • property_t 定义
struct property_t {
const char *name;
const char *attributes;
};

对于属性结构比较简单就只有属性名字,以及对应的属性字符串,至于属性至于属性字符串的介绍可以看下面官方的文档介绍:

struct ivar_t {

int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;
//.....
};

如果我们使用下面方式声明实例变量,那么这些实例变量就会转换为ivar_t

@interface TestClass : NSObject {
@public
NSString *_name;
@private
NSString *_ID;
}

但是实际开发中大多数使用属性而不使用这种方法,使用这种写法,对象布局在编译器就已经固定了。只要碰到访问_name变量的代码,编译器就把其替换为偏移量ivar_t 中的(offset),这个偏移量是硬编码,表示该变量距离存放对象的内存区域的起始地址有多远。但是如果在运行过程中,又新增了一个实例变量,硬编码于其中的变量就会读到错误的值,这也是为什么OC无法动态添加成员变量的原因。

最后使用一个图来总结下全文的内容:

1. draveness
2. 南峰子
3. halfrost
4. 雷纯锋
5. sunnyxx
6. ibireme
7. 唐巧
8. CornBallast
9. 杨萧玉

源码地址

源码分析

上一篇博客已经介绍到dyld被加载,以及设置dyld的入口点为__dyld_start,这篇博客就从__dyld_start开始看下应用执行main之前都经历了哪些过程,在开始之前我们需要先对iOS中对动态库有一定对了解,如果你之前接触过其他平台你可能会听说过Windows系统下的DLL文件,Linux 系统下的so文件,iOS系统则是使用dylib作为动态库,至于iOS动态库与静态库的对比我们会专门用一篇博客进行介绍,dylib和普通的可执行文件在文件结构上大同小异,都是Mach O格式文件,只不过在文件类型上,一个是MH_DYLIB,一个是MH_EXECUTE,但是动态库不能像可执行文件那样直接运行,而是需要通过dyld加载到内存与主程序链接后才可以执行,在主程序启动后会通过dyld将它所依赖的全部动态链接库链接起来,最终组成可运行的应用。

如果想查看某个可执行文件所依赖的动态链接库可以通过命令:jtool -L IDLFundation 来查看,我们接下来就来看下整个过程:

__dyld_start:
popq %rdi # param1 = mh of app
pushq $0 # push a zero for debugger end of frames marker
movq %rsp,%rbp # pointer to base of kernel frame
andq $-16,%rsp # force SSE alignment
subq $16,%rsp # room for local variables

//调用dyldbootstrap::start
# call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
movl 8(%rbp),%esi # param2 = argc into %esi
leaq 16(%rbp),%rdx # param3 = &argv[0] into %rdx
movq __dyld_start_static(%rip), %r8
leaq __dyld_start(%rip), %rcx
subq %r8, %rcx # param4 = slide into %rcx
leaq ___dso_handle(%rip),%r8 # param5 = dyldsMachHeader
leaq -8(%rbp),%r9
call __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
movq -8(%rbp),%rdi
cmpq $0,%rdi
jne Lnew

在__dyld_start会通过汇编调用dyldbootstrap::start方法

uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
{
//.........
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

dyldbootstrap::start方法中会进入dyld的入口main函数。

_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;

// 设置ImageLoader的context
setContext(mainExecutableMH, argc, argv, envp, apple);
//......
try {
//...
// instantiate ImageLoader for main executable
// 加载Image(代表镜像)
// 为主程序初始化imageLoader,用于后续的链接等过程
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.processIsRestricted = sProcessIsRestricted;
gLinkContext.processRequiresLibraryValidation = sProcessRequiresLibraryValidation;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

//.....
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
// 将共享库加载到内存中
mapSharedCache();
// .......
// 如果有插入的库,加载
// load any inserted libraries
if( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
//// 加载环境变量DYLD_INSERT_LIBRARIES中的动态库,使用loadInsertedDylib进行加载
loadInsertedDylib(*lib);//把环境变量DYLD_INSERT_LIBRARIES 中的动态库调用loadInsertedDylib进行加载
}
// record count of inserted libraries so that a flat search will look at
// inserted libraries, then main, then others.
sInsertedDylibCount = sAllImages.size()-1;

// link main executable
// 链接主程序
gLinkContext.linkingMainExecutable = true;
//继续回到dyld的_main函数中来,继续加载根目录Framework目录下的其他动态库. 加载完所有的dylibs之后,每个dylib之间还是没有关联,
//不知道怎么调用,这时候就该进行link操作了,link操作分成rebase和binding辆部分.

//ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,
//通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。
//据研究表明ASLR可以有效的降低缓冲区溢出攻击的成功率,如今Linux、FreeBSD、Windows等主流操作系统都已采用了该技术。

//rebase: 由于ASLR访问地址被随机化,所以rebase在动态库内部进行修正访问地址,并创建访问地址存储在__DATA段,这个期间可能会产生缺页并进行IO操作
//binding: 主要负责动态库之间的调用地址的修正和创建

//initialize
//这里就比较简单了, 这个时候各个库都已经load完成,访问地址指针也已经修正过,就可以初始化所有dylib了, 会调用C++的初始化构造方法,
//也就是用__attribute__((constructor))修饰的方法,被修饰的方法都会在main()方法之前调用
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}

// link any inserted libraries
// 链接任何插入的库
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
image->setNeverUnloadRecursive();
}
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing();
}
}

// <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
for (int i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image->inSharedCache() )
continue;
image->registerInterposing();
}

// apply interposing to initial set of images
for(int i=0; i < sImageRoots.size(); ++i) {
sImageRoots[i]->applyInterposing(gLinkContext);
}
gLinkContext.linkingMainExecutable = false;

sMainExecutable->weakBind(gLinkContext);

initializeMainExecutable();

// 寻找主程序的入口
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// 主可执行文件使用lc_main,需要返回libdyld.dylib中的glue
//调用main()
//当执行完dyld::_main方法之后,返回了main()函数地址,这个时候所有初始化工作都已经完成了,正式进入Objc声明周期
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}
}
catch(const char* message) {
syncAllImages();
halt(message);
}
catch(...) {
dyld::log("dyld: launch failed\n");
}
return result;
}

在main函数中会执行如下操作:

  1. 将主程序初始化为imageLoader,用于后续的链接等操作
  2. 加载共享库到内存
  3. 加载插入的动态库
  4. 链接主程序,链接插入库
  5. 初始化主程序
  6. 寻找主程序入口点

接下来我们将按照上面的顺序进行展开:

将主程序初始化为imageLoader

在dyld main方法中会通过instantiateFromLoadedImage创建ImageLoader,其中第一个参数传入的是mainExecutableMH为主程序的Mach O Header,有了mainExecutableMH,dyld就可以从头开始遍历整个Mach O文件信息了。在开始创建ImageLoader之前会先调用isCompatibleMachO来查看mainExecutableMH中的cputype与cpusubtype是否与当前设备兼容,只有兼容的情况下才会继续创建ImageLoader,创建后的ImageLoader会通过addImage添加到sAllImages,然后调用addMappedRange()申请内存,更新主程序镜像映射的内存区。

static ImageLoader* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path) {
// 检测mach-o header的cputype与cpusubtype是否与当前系统兼容
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
//初始化镜像加载器
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return image;
}
throw "main executable not a known format";
}

我们看下ImageLoader的创建方法:

ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context) {
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
// sniffLoadCommands主要获取加载命令中compressed的值(压缩还是传统)以及segment的数量、libCount(需要加载的动态库的数量)
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}

instantiateMainExecutable会首先调用sniffLoadCommands从加载信息中获取到当前的主程序是Compressed还是Classic类型的,以及segment的数量,需要加载的动态库的数量。然后根据主程序的类型来创建ImageLoaderMachOCompressed或者ImageLoaderMachOClassic类型的ImageLoader.

void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
const linkedit_data_command** codeSigCmd,
const encryption_info_command** encryptCmd)
{
//......
for (uint32_t i = 0; i < cmd_count; ++i) {
uint32_t cmdLength = cmd->cmdsize;
struct macho_segment_command* segCmd;
//.....
switch (cmd->cmd) {
case LC_DYLD_INFO:
case LC_DYLD_INFO_ONLY:
*compressed = true;
break;
case LC_SEGMENT_COMMAND:
segCmd = (struct macho_segment_command*)cmd;
//.........
// ignore zero-sized segments
if ( segCmd->vmsize != 0 )
*segCount += 1;
//......
break;
//.....
case LC_LOAD_DYLIB:
case LC_LOAD_WEAK_DYLIB:
case LC_REEXPORT_DYLIB:
case LC_LOAD_UPWARD_DYLIB:
*libCount += 1;
break;
case LC_CODE_SIGNATURE:
*codeSigCmd = (struct linkedit_data_command*)cmd; // only support one LC_CODE_SIGNATURE per image
break;
case LC_ENCRYPTION_INFO:
case LC_ENCRYPTION_INFO_64:
*encryptCmd = (struct encryption_info_command*)cmd; // only support one LC_ENCRYPTION_INFO[_64] per image
break;
}
cmd = nextCmd;
}

//.......

// fSegmentsArrayCount is only 8-bits
if ( *segCount > 255 )
dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);

// fSegmentsArrayCount is only 8-bits
if ( *libCount > 4095 )
dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);

if ( needsAddedLibSystemDepency(*libCount, mh) )
*libCount = 1;
}

sniffLoadCommands 会根据Mach 文件中是否有LC_DYLD_INFOLC_DYLD_INFO_ONLY来判断当前Mach O文件是否是compressed的,并且获取segment 数量以及所依赖的动态库数量libCount,还有代码签名命令codeSigCmd以及加密命令encryptCmd。我们这里以compressed类型作为例子。那么上面返回的就是ImageLoaderMachOCompressed,

ImageLoaderMachOCompressed* ImageLoaderMachOCompressed::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, 
unsigned int segCount, unsigned int libCount, const LinkContext& context)
{
ImageLoaderMachOCompressed* image = ImageLoaderMachOCompressed::instantiateStart(mh, path, segCount, libCount);
//.....
image->disableCoverageCheck();
image->instantiateFinish(context);
image->setMapped(context);

//....
return image;
}

加载共享库到内存

dyld加载的时候会启动共享缓存技术对这个过程进行优化,共享缓存会在进程启动的时候被dyld映射到内存中,不同App间访问的共享库最终都会映射到同一块物理内存,之后在加载任何Mach O镜像的时候,都会先去检查该Mach O镜像以及所需的动态库文件是否已经存在在共享缓存中,如果存在则直接将它在共享内存中的内存地址映射到进程的内存地址空间,从而加快动态库的加载的速度,对启动性能会有较大的提高。在Mac OS系统中,动态库共享缓存以文件的形式存放在/var/db/dyld目录下,我们需要根据当前设备的CPU架构类型去匹配要打开的缓存文件,接着读取缓存文件的前8192字节,解析并加载缓存头dyld_cache_header信息到内存,我们可以通过/usr/bin/update_dyld_shared_cache来更新共享库。

static void mapSharedCache()
{
uint64_t cacheBaseAddress = 0;
// 检查当前共享缓存是否已经映射到共享区域
if ( _shared_region_check_np(&cacheBaseAddress) == 0 ) {
/// 共享库已经被映射到内存中
sSharedCache = (dyld_cache_header*)cacheBaseAddress;
// 检查共享库的兼容性,如果已经映射到内存中的共享库但是不兼容,这时候我们将忽略它
if ( strcmp(sSharedCache->magic, magic) != 0 ) {
sSharedCache = NULL;
//.....
}
//.........
}
else {
// 如果共享库没有加载到内存中,进行加载
int fd = openSharedCacheFile();
if ( fd != -1 ) {
uint8_t firstPages[8192];
// 获取共享库文件的句柄,然后进行读取解析
if ( ::read(fd, firstPages, 8192) == 8192 ) {
dyld_cache_header* header = (dyld_cache_header*)firstPages;
//共享缓存合法性检查
if ( strcmp(header->magic, magic) == 0 ) {
//.......
// 校验缓存文件的完整性
//.......
if ( goodCache ) {
//.......
// 将共享缓存映射到共享区域
if (_shared_region_map_and_slide_np(fd, mappingCount, mappings, codeSignatureMappingIndex, cacheSlide, slideInfo, slideInfoSize) == 0) {
// successfully mapped cache into shared region
sSharedCache = (dyld_cache_header*)mappings[0].sfm_address;
sSharedCacheSlide = cacheSlide;
dyld::gProcessInfo->sharedCacheSlide = cacheSlide;
if ( header->mappingOffset >= 0x68 ) {
memcpy(dyld::gProcessInfo->sharedCacheUUID, header->uuid, 16);
}
}
else {
//........
}
}
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file is invalid\n");
}
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file cannot be read\n");
}
close(fd);
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file cannot be opened\n");
}
}
//......
// tell gdb where the shared cache is
//........
}

加载插入的动态库

插入的动态库会存储在DYLD_INSERT_LIBRARIES,在这时候会遍历DYLD_INSERT_LIBRARIES环境变量中指定的动态库列表,并调用loadInsertedDylib()将其加载。

if( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
// 加载环境变量DYLD_INSERT_LIBRARIES中的动态库,使用loadInsertedDylib进行加载
loadInsertedDylib(*lib);//把环境变量DYLD_INSERT_LIBRARIES 中的动态库调用loadInsertedDylib进行加载
}
sInsertedDylibCount = sAllImages.size()-1;

loadInsertedDylib会调用load方法完成加载工作。load方法会先调用loadPhase0尝试从文件加载,然后一直从loadPhase0一直到loadPhase6,查找动态库的路径,每个阶段都会为下一阶段生成搜索的路径。整个查找顺序会顺着: ****DYLD_ROOT_PATH -> LD_LIBRARY_PATH -> DYLD_FRAMEWORK_PATH -> raw path - >DYLD_FALLBACK_LIBRARY_PATH **** 如果找到则调用ImageLoaderMachO::instantiateFromFile来实例化一个ImageLoader,之后调用checkandAddImage验证映像并将其加入到全局映像列表中。如果loadPhase0返回空,表示在路径中没有找到动态库,就尝试从共享缓存中查找,找到就调用ImageLoaderMachO::instantiateFromCache()从缓存中加载,否则就抛出没找到映像的异常。


ImageLoader* load(const char* path, const LoadContext& context)
{
//.....
// 尝试所有路径排列并检查现有加载的镜像
// try all path permutations and check against existing loaded images
ImageLoader* image = loadPhase0(path, orgPath, context, NULL);
if ( image != NULL ) {
return image;
}

//......
image = loadPhase0(path, orgPath, context, &exceptions);
if ( (image == NULL) && cacheablePath(path) && !context.dontLoad ) {
//......
if ( (myerr == ENOENT) || (myerr == 0) ) {
// see if this image is in shared cache
//......
if ( findInSharedCacheImage(resolvedPath, false, NULL, &mhInCache, &pathInCache, &slideInCache) ) {
//......
image = ImageLoaderMachO::instantiateFromCache(mhInCache, pathInCache, slideInCache, stat_buf, gLinkContext);
image = checkandAddImage(image, context);
}
}
}
}
static ImageLoader* loadPhase6(int fd, const struct stat& stat_buf, const char* path, const LoadContext& context)
{
//.......
// try mach-o loader
if ( shortPage )
throw "file too short";
if ( isCompatibleMachO(firstPage, path) ) {
// 只有MH_BUNDLE,MH_DYLIB,以及一些MH_EXECUTE 才能被动态加载
switch ( ((mach_header*)firstPage)->filetype ) {
case MH_EXECUTE:
case MH_DYLIB:
case MH_BUNDLE:
break;
default:
throw "mach-o, but wrong filetype";
}
//实例化镜像
ImageLoader* image = ImageLoaderMachO::instantiateFromFile(path, fd, firstPage, fileOffset, fileLength, stat_buf, gLinkContext);
// validate
return checkandAddImage(image, context);
}
//.....
}

链接主程序

link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
//......
// 4.link any inserted libraries
// 链接任何插入的库
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
image->setNeverUnloadRecursive();
}
//......
}
//......
gLinkContext.linkingMainExecutable = false;
sMainExecutable->weakBind(gLinkContext);
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths)
{
//......
//递归加载所有依赖库进内存。
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths);
context.notifyBatch(dyld_image_state_dependents_mapped);

//.....
uint64_t t1 = mach_absolute_time();
context.clearAllDepths();
this->recursiveUpdateDepth(context.imageCount());

uint64_t t2 = mach_absolute_time();
//递归对自己以及依赖库进行复基位操作。在以前,程序每次加载其在内存中的堆栈基地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现ASLR(Address space layout randomization,地址空间配置随机加载),
//程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行计算修复才能正常访问
// 递归修正自己和依赖库的基地址,因为ASLR的原因,需要根据随机slide修正基地址
this->recursiveRebase(context);
context.notifyBatch(dyld_image_state_rebased);

uint64_t t3 = mach_absolute_time();
//对库中所有nolazy的符号进行bind,一般的情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind。
// recursiveBind对于noLazy的符号进行绑定,lazy的符号会在运行时动态绑定
this->recursiveBind(context, forceLazysBound, neverUnload);

uint64_t t4 = mach_absolute_time();
if ( !context.linkingMainExecutable )
this->weakBind(context);
uint64_t t5 = mach_absolute_time();

context.notifyBatch(dyld_image_state_bound);
//........
}

recursiveLoadLibraries会先获取当前镜像所需要的库列表,然后会先从缓存库中加载所需要的库,如果没在缓存库中找到则调用loadLibrary进行加载,loadLibrary 实际上调用的是之前介绍的load方法。将依赖的库加载进来并装载。recursiveUpdateDepth 会对镜像及其依赖库进行排序。

接下来我们看下recursiveRebase

void ImageLoader::recursiveRebase(const LinkContext& context)
{
if ( fState < dyld_image_state_rebased ) {
// break cycles
fState = dyld_image_state_rebased;
try {
// rebase lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL )
dependentImage->recursiveRebase(context);
}
// rebase this image
doRebase(context);
// notify
context.notifySingle(dyld_image_state_rebased, this);
}
//......
}
}

在之前的步骤中我们都在完成库的加载,但是这些dylibs之间是没有关联的,需要rebase,binding对地址修正,不知道大家看过高达之类的动画片没有,在变身过程中装备会从四面八方飞过来,然后会一个个安装到身上,嗯,link就是这个过程,动态库中的地址是相对的,因为它需要保证它内部逻辑的独立性,同时为了降低缓冲区溢出攻击的成功率主流的操作系统都会采用ASLR(Address space layout randomization)技术,它通过对堆、栈、共享库映射等线性区布局的随机化来增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。

****Rebase:在镜像内部调整指针的指向 ****

就像上面提到的未加载到内存的动态库镜像里面的地址都是相对的,是距第一个Segement的偏移量,而当加载到内存中的时候,起始地址就是申请的内存的起始地址,就不再是0,那么如何再能够找到这些引用的正确内存位置呢?这个就是rebase需要解决的问题,rebase这一步就是将相对地址调整为绝对地址,以及修正ASLR带来的地址不确定性:在没有使用ASLR技术之前,动态库加载的时候会把 dylib 加载到指定地址,所有指针和数据对于代码来说都是确定的,使用了ASLR后每次加载都会将dylib 加载到新的随机地址,这个随机的地址跟代码和数据指向的旧地址会有偏差,dyld 在rebase这个阶段需要修正这个偏差,做法就是将 dylib 内部的指针地址都加上这个偏移量,然后重复不断地对 __DATA 段中需要 rebase 的指针加上这个偏移量。也就是说经过rebase处理后镜像内部在内存中的映射已经确定下来了,不再是相对于动态库内部的相对地址,并创建访问地址存储在__DATA段。总的来说经过rebase处理后我们会在内存中获得一个有明确入口的,并且内部地址明确的动态库,但是对其他外部依赖库调用的地址在这步还没修正,需要在binding阶段进行处理。我们先继续往下看:

recursiveRebase 里面调用的是doRebase,而doRebase会调用ImageLoaderMachOCompressed::rebase

void ImageLoaderMachO::doRebase(const LinkContext& context)
{
//.......
// do actual rebasing
this->rebase(context);
}
void ImageLoaderMachOCompressed::rebase(const LinkContext& context)
{
//......
try {
//.....
bool done = false;
while ( !done && (p < end) ) {
uint8_t immediate = *p & REBASE_IMMEDIATE_MASK;
uint8_t opcode = *p & REBASE_OPCODE_MASK;
++p;
switch (opcode) {
case REBASE_OPCODE_DONE:
done = true;
break;
case REBASE_OPCODE_SET_TYPE_IMM:
type = immediate;
break;
case REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
segmentIndex = immediate;
//.....
address = segActualLoadAddress(segmentIndex) + read_uleb128(p, end);
segmentEndAddress = segActualEndAddress(segmentIndex);
break;
case REBASE_OPCODE_ADD_ADDR_ULEB:
address += read_uleb128(p, end);
break;
case REBASE_OPCODE_ADD_ADDR_IMM_SCALED:
address += immediate*sizeof(uintptr_t);
break;
case REBASE_OPCODE_DO_REBASE_IMM_TIMES:
for (int i=0; i < immediate; ++i) {
if ( address >= segmentEndAddress )
throwBadRebaseAddress(address, segmentEndAddress, segmentIndex, start, end, p);
rebaseAt(context, address, slide, type);
address += sizeof(uintptr_t);
}
fgTotalRebaseFixups += immediate;
break;
case REBASE_OPCODE_DO_REBASE_ULEB_TIMES:
count = read_uleb128(p, end);
for (uint32_t i=0; i < count; ++i) {
rebaseAt(context, address, slide, type);
address += sizeof(uintptr_t);
}
fgTotalRebaseFixups += count;
break;
case REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB:
rebaseAt(context, address, slide, type);
address += read_uleb128(p, end) + sizeof(uintptr_t);
++fgTotalRebaseFixups;
break;
case REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB:
count = read_uleb128(p, end);
skip = read_uleb128(p, end);
for (uint32_t i=0; i < count; ++i) {
rebaseAt(context, address, slide, type);
address += skip + sizeof(uintptr_t);
}
fgTotalRebaseFixups += count;
break;
default:
dyld::throwf("bad rebase opcode %d", *p);
}
}
}
catch (const char* msg) {
//......
}
CRSetCrashLogMessage2(NULL);
}

在ImageLoaderMachOCompressed::rebase方法中就是对这些地址进行修正,从而获得明确的绝对地址。

接下来我们看下Bind 过程的代码recursiveBind

void ImageLoader::recursiveBind(const LinkContext& context, bool forceLazysBound, bool neverUnload)
{
// Normally just non-lazy pointers are bound immediately.
// The exceptions are:
// 1) DYLD_BIND_AT_LAUNCH will cause lazy pointers to be bound immediately
// 2) some API's (e.g. RTLD_NOW) can cause lazy pointers to be bound immediately
if ( fState < dyld_image_state_bound ) {
// break cycles
fState = dyld_image_state_bound;
try {
// bind lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL )
dependentImage->recursiveBind(context, forceLazysBound, neverUnload);
}
// bind this image
this->doBind(context, forceLazysBound);
// ......
context.notifySingle(dyld_image_state_bound, this);
}
catch (const char* msg) {
//......
}
}
}
void ImageLoaderMachOCompressed::doBind(const LinkContext& context, bool forceLazysBound)
{
CRSetCrashLogMessage2(this->getPath());

// if prebound and loaded at prebound address, and all libraries are same as when this was prebound, then no need to bind
// note: flat-namespace binaries need to have imports rebound (even if correctly prebound)
if ( this->usablePrebinding(context) ) {
// don't need to bind
}
else {
#if TEXT_RELOC_SUPPORT
// if there are __TEXT fixups, temporarily make __TEXT writable
if ( fTextSegmentBinds )
this->makeTextSegmentWritable(context, true);
#endif

// run through all binding opcodes
eachBind(context, &ImageLoaderMachOCompressed::bindAt);
#if TEXT_RELOC_SUPPORT
// if there were __TEXT fixups, restore write protection
if ( fTextSegmentBinds )
this->makeTextSegmentWritable(context, false);
#endif
// if this image is in the shared cache, but depends on something no longer in the shared cache,
// there is no way to reset the lazy pointers, so force bind them now
if ( forceLazysBound || fInSharedCache )
this->doBindJustLazies(context);

// this image is in cache, but something below it is not. If
// this image has lazy pointer to a resolver function, then
// the stub may have been altered to point to a shared lazy pointer.
if ( fInSharedCache )
this->updateOptimizedLazyPointers(context);

// tell kernel we are done with chunks of LINKEDIT
if ( !context.preFetchDisabled )
this->markFreeLINKEDIT(context);
}
// set up dyld entry points in image
// do last so flat main executables will have __dyld or __program_vars set up
this->setupLazyPointerHandler(context);
CRSetCrashLogMessage2(NULL);
}

****Bind:将指针指向镜像外部的内容 ****

我们前面提到了经过rebase处理后我们将会在内存中获取该库的一个明确的入口地址,并且库内部的相对地址会被确定为一个明确的地址,但是对其他库引用的地方还是没明确,这部分就需要Binding阶段进行处理了,Binding阶段主要的任务就是根据被导出符号重定位表项,去符号表中找出符号的基本信息,再去其他库符号表中resolve符号,将resolve结果bind到需要重定位地址上。说句人话就是,在其他库的符号表中去查找明确地址,然后在当前动态库中使用这个地址。一般我们使用另一个动态库的某个方法的时候会有一个间接引用,这个地址中存放的就是我们需要跳转的地址,在binding阶段会将其他库中查找的地址这需要很多计算,去符号表里查找。找到后会将内容存储到 __DATA 段中的那个指针中,下次调用的时候就会从这个地方去拿这个地址去跳转到要调用其他动态库的函数地址。

这里还有两种特殊的绑定:

Lazy Bind:

我们启动优化中常常会考虑将某些动态依赖库改为静态依赖,虽然静态依赖会增大包的体积,但是会改善启动缓慢的问题,上面看到Bind的过程,发现Bind的过程需要查到对应的符号再进行bind. 如果在启动的时候,所有的符号都立即Bind成功,那么势必拖慢启动速度,那么我们可以将某些不必要的符号绑定延后吗?嗯,肯定可以的,Lazy Bind就是第一次调用到才会进行真正的Bind.这里其实用到dyld_stub_binder,在需要绑定的时候会将调用信息传递给它,由它来找到最终要跳转的地址,从而完成绑定。

Weak Bind:

Weak Bind这一步是将相同的弱符号统一化,如果有强符号则统一成强符号,否则统一成按装载顺序的首个弱符号,在这个过程中会去寻找,找到一个相同的符号后,如果是强符号则不需要再查找,否则从装载顺序找到首个弱符号地址,将所有库中的相同符号都覆盖为该地址。

初始化主程序:

void initializeMainExecutable()
{

// 初始化任意插入的dylibs
// run initialzers for any inserted dylibs
if ( rootCount > 1 ) {
//这里需要注意的是sImageRoots 中的第一个变量是主程序镜像, 因此这里初始化的时候需要跳过第一个数据, 对其他后面插入的dylib进行调用ImageLoader::runInitializers进行初始化
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// 最后运行主程序的初始化器
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

//.........
}

我们看到initializeMainExecutable方法中会先对插入的动态链接库进行初始化,然后再对主程序进行初始化,初始化都是调用runInitializers

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
//......
//处理初始化器
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized);
//......
}

runInitializers方法中就做两件事情,一个就是调用processInitializers,以及在初始化结束发出通知,告诉监听者,当前已经初始化完毕。

void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
//在镜像列表中的所有镜像进行递归初始化
for (uintptr_t i=0; i < images.count; ++i) {
images.images[i]->recursiveInitialization(context, thisThread, timingInfo, ups);
}
//......
}
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);

if ( fState < dyld_image_state_dependents_initialized-1 ) {
//......
// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
// 通知 runtime, 当前状态发生变化 -- image的依赖已经完全加载. 如果在runtime中注册了状态监听,当状态发送变化时, 会触发回调函数.
context.notifySingle(dyld_image_state_dependents_initialized, this);

//递归的调用当前image的依赖的dylib动态库的初始化函数进行初始化
// initialize this image
bool hasInitializers = this->doInitialization(context);

// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
// 通知runtime, 当前镜像初始化完成
context.notifySingle(dyld_image_state_initialized, this);
//.....
}
recursiveSpinUnLock();
}
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
// 初始化镜像
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
return (fHasDashInit || fHasInitializers);
}
void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
if ( fHasDashInit ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_ROUTINES_COMMAND:
Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
//.......
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}

doImageInit方法主要获取Mach O的init方法的地址并调用它,完成初始化操作。

void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
if ( fHasInitializers ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
//初始化器
Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
for (size_t i=0; i < count; ++i) {
//......
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
}
}
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}

doModInitFunctions方法主要获取Mach O的static initializer的地址并调用

这里需要注意的是这里的initalizer并非指的是名为initalizer的方法, 而是使用 attribute((constructor) 进行修饰的方法, 在ImageLoader类中initializer函数指针所指向该初始化方法的地址。

这里有一个十分重要的动态库初始化方法,libSystem_initializer就是在这个阶段被调用的。

static __attribute__((constructor)) 
void libSystem_initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct ProgramVars* vars)
{
//......
_libkernel_init(libkernel_funcs);

bootstrap_init();
mach_init();
pthread_init();
__libc_init(vars, libSystem_atfork_prepare, libSystem_atfork_parent, libSystem_atfork_child, apple);
__keymgr_initializer();
_dyld_initializer();

libdispatch_init();

_libxpc_initializer();
//......
errno = 0;
}

libSystem_initializer会调用libdispatch_init再到_objc_init初始化runtime在_objc_init中通过注册了几个关键通知, 从dyld这里接手了关键时机的处理,这个方法会在下一篇介绍runtime的博客中进行介绍,我们先继续往下看。对于Libsystem代码地址可以在该地址进行下载.

获取主程序入口

result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// 主可执行文件使用lc_main,需要返回libdyld.dylib中的glue
//调用main()
//当执行完dyld::_main方法之后,返回了main()函数地址,这个时候所有初始化工作都已经完成了,正式进入Objc声明周期
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}

main 地址有两种方式获取,一种是存放在LC_MAIN命令中,这时候需要调用getThreadPC

void* ImageLoaderMachO::getThreadPC() const
{
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
// 遍历loadCommand,加载loadCommand中的'LC_MAIN'所指向的偏移地址
if ( cmd->cmd == LC_MAIN ) {
entry_point_command* mainCmd = (entry_point_command*)cmd;
// 偏移量 + header所占的字节数,就是main的入口
void* entry = (void*)(mainCmd->entryoff + (char*)fMachOData);
if ( this->containsAddress(entry) )
return entry;
else
throw "LC_MAIN entryoff is out of range";
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
return NULL;
}

getThreadPC主要逻辑就是遍历loadCommand,找到’LC_MAIN’指令,得到该指令所指向的偏移地址,经过处理后,就得到了main函数的地址,将此地址返回给__dyld_start。__dyld_start中将main函数地址保存在寄存器后,跳转到对应的地址,开始执行main函数,另一种Mach O不支持LC_MAIN仅支持LC_UNIXTHREAD,这时候就需要调用getMain方法。

void* ImageLoaderMachO::getMain() const
{
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_UNIXTHREAD:
{
#if __i386__
const i386_thread_state_t* registers = (i386_thread_state_t*)(((char*)cmd) + 16);
void* entry = (void*)(registers->eip + fSlide);
#elif __x86_64__
const x86_thread_state64_t* registers = (x86_thread_state64_t*)(((char*)cmd) + 16);
void* entry = (void*)(registers->rip + fSlide);
//.....
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
throw "no valid entry point";
}

总结

上一篇博客我们主要介绍了从点击应用到加载dyld,再将地址设置到dyld到入口地址,这篇博客就从dyld入口地址__dyld_start作为起点,在这个阶段中主要做了如下工作:

1. 将主程序初始化为imageLoader
2. 加载共享库到内存
3. 加载插入的动态库
4. 链接主程序,链接插入库
5. 初始化主程序,插入库
6. 寻找主程序入口点

而下一篇博客要给大家介绍的是十分关键的runtime,它的初始化就发生在初始化主程序,插入库这个阶段,这个阶段会调用使用 attribute((constructor) 进行修饰的方法, 其中这里有一个十分重要的动态库初始化方法,libSystem_initializer就是在这个阶段被调用的libSystem_initializer会调用libdispatch_init再到_objc_init初始化runtime在_objc_init中通过注册了几个关键通知, 从dyld这里接手了关键时机的处理,包括镜像映射,镜像加载,镜像卸载,这些都是下一篇博客将要介绍的。从下一篇开始就要正式介入介绍Runtime了。

上一篇博客介绍了Mach O文件的格式,以及每个部分的作用,这一部分开始将要介绍的是在上一个基础上如何将Mach O文件加载并运行起来。我们会从点击执行应用开始,到加载dyld,初始化dyld,再到dyld将镜像加载到内存,Rebase/Bind,Runtime初始化,其它的初始化代码,最后运行main函数为止。整个过程如下图所示:

在开始这篇博客内容规划之前,最难的是从哪里开头讲,后面查看了XNU代码后发现个人能hold住的起点应该是load_init_program,当然后面随着技术的提高肯定能够从更深层次给大家讲明白这个问题,都是时间问题,对自己有信心,好了不说太多无关的内容了,直接切入主题:

我们知道XNU内核启动后,启动的第一个进程是launchd。launchd启动之后会启动其他守护进程。我们可以看XNU源码中有如下一句注释:

// Description:	Load the "init" program; in most cases, this will be "launchd"

我们先来看下load_init_program

void
load_init_program(proc_t p)
{
//......
/*
* Copy out program name.
*/
init_addr = VM_MIN_ADDRESS;
(void) vm_allocate(current_map(), &init_addr, PAGE_SIZE,VM_FLAGS_ANYWHERE);
if (init_addr == 0)
init_addr++;

//将/sbin/launchd拷贝到 init_addr = 1
(void) copyout((caddr_t) init_program_name, CAST_USER_ADDR_T(init_addr),(unsigned) sizeof(init_program_name)+1);

argv[argc++] = (char *) init_addr; //argv[0] = "/sbin/launchd"
init_addr += sizeof(init_program_name);
init_addr = (vm_offset_t)ROUND_PTR(char, init_addr);

if (boothowto & RB_SINGLE) {
const char *init_args = "-s";
copyout(init_args, CAST_USER_ADDR_T(init_addr),strlen(init_args));
argv[argc++] = (char *)init_addr; //argv[1] = "-s"
init_addr += strlen(init_args);
init_addr = (vm_offset_t)ROUND_PTR(char, init_addr);
}
/*
* Null-end the argument list
*/
argv[argc] = NULL;

/*
* Copy out the argument list.
*/
//参数拷贝到指定到位置
(void) copyout((caddr_t) argv, CAST_USER_ADDR_T(init_addr),(unsigned) sizeof(argv));

//到此位置 argv[0] = "/sbin/launchd"
// argv[1] = "-s"
// argv[2] = 参数列表

/*
* Set up argument block for fake call to execve.
*/
init_exec_args.fname = CAST_USER_ADDR_T(argv[0]); //argv[0] = "/sbin/launchd"
init_exec_args.argp = CAST_USER_ADDR_T((char **)init_addr);
init_exec_args.envp = CAST_USER_ADDR_T(0);

//将init_exec_args作为参数传递给execve
error = execve(p,&init_exec_args,retval);
if (error)
panic("Process 1 exec of %s failed, errno %d\n",
init_program_name, error);
}

我们接下来看下execve

int execve(proc_t p, struct execve_args *uap, register_t *retval) {
struct __mac_execve_args muap;
int err;
muap.fname = uap->fname; //程序执行路径
muap.argp = uap->argp; //参数列表
muap.envp = uap->envp; //环境变量
muap.mac_p = USER_ADDR_NULL;
//这里只是调用了__mac_execve将执行文件名,参数列表,环境变量传入
err = __mac_execve(p, &muap, retval);
return(err);
}

在execve主要将execve_args中的程序执行文件路径,参数列表,环境变量通过__mac_execve传入到后续流程:

int __mac_execve(proc_t p, struct __mac_execve_args *uap, register_t *retval) {
//.....
task = current_task(); //获取当前的任务
//.......
//激活镜像:主要是为加载镜像进行数据的初始化,以及资源相关的操作
error = exec_activate_image(imgp);
//.......
return(error);
}

__mac_execve 主要是调用exec_activate_image来激活镜像

/*
* exec_activate_image
* 遍历可用镜像激活器,并激活与imgp 结构相关的镜像
* 主要是拷贝可执行文件到内存中,并根据不同的可执行文件类型选择不同的加载函数,
* 所有的镜像的加载要么终止在一个错误上,要么最终完成加载镜像。
*/
static int exec_activate_image(struct image_params *imgp) {
//.......
again:
// 检查镜像是否是普通文件,是否可执行,是否可读
error = exec_check_permissions(imgp);
//......
encapsulated_binary:
//根据不同的可执行文件类型选择不同的加载函数
//OS X有三种可执行文件,mach-o由exec_mach_imgact处理,fat binary由exec_fat_imgact处理,
//interpreter(解释器)由exec_shell_imgact处理
for(i = 0; error == -1 && execsw[i].ex_imgact != NULL; i++) {
//遍历镜像激活函数,寻找可以激活当前镜像的激活器
error = (*execsw[i].ex_imgact)(imgp);
//...........
}
//........
return (error);
}

exec_activate_image主要任务是遍历可用镜像激活器,按照可执行文件的格式,执行不同的函数。目前有三种格式,单指令集可执行文件(Mach-o Binary),多指令集可执行文件(Fat Binary),shell 脚本(Interpreter Script)。
exec_activate_image首先会对镜像文件进行一次校验,检查镜像是否是普通文件,是否可执行,是否可读,然后遍历镜像激活函数,寻找可以激活当前镜像的激活器。execsw是当前能够支持的镜像激活表。它是一个结构体指针,每项结构体的结构如下所示:

struct execsw {
int (*ex_imgact)(struct image_params *); //激活镜像的方法指针
const char *ex_name; //镜像名
}

包含激活镜像的方法,以及镜像名。

struct execsw {
int (*ex_imgact)(struct image_params *); //激活镜像的方法指针
const char *ex_name; //镜像名
} execsw[] = {
{ exec_mach_imgact, "Mach-o Binary" },
{ exec_fat_imgact, "Fat Binary" },
#ifdef IMGPF_POWERPC
{ exec_powerpc32_imgact, "PowerPC binary" },
#endif /* IMGPF_POWERPC */
{ exec_shell_imgact, "Interpreter Script" },
{ NULL, NULL}
};

我们这里只以Mach-o Binary的激活器为例子来看下是怎么激活的。

static int exec_mach_imgact(struct image_params *imgp) {

struct mach_header *mach_header = (struct mach_header *)imgp->ip_vdata;
//........
/*
* 首先确保这是个Mach-O 1.0 或者Mach-O 2.0二进制文件
* make sure it's a Mach-O 1.0 or Mach-O 2.0 binary; the difference
* is a reserved field on the end, so for the most part, we can
* treat them as if they were identical.
* magic检查
*/
if ((mach_header->magic != MH_MAGIC/*32位架构*/) &&
(mach_header->magic != MH_MAGIC_64/*64位架构*/)) {
error = -1;
goto bad;
}

// 如果文件类型为MH_DYLIB 或者MH_BUNDLE 返回错误
//为什么MH_DYLIB,MH_BUNDLE 要认定为error
switch (mach_header->filetype) {
case MH_DYLIB:
case MH_BUNDLE:
error = -1;
goto bad;
}

//cpu 类型检查
if (!imgp->ip_origcputype) {
imgp->ip_origcputype = mach_header->cputype;
imgp->ip_origcpusubtype = mach_header->cpusubtype;
}

task = current_task();
thread = current_thread();
uthread = get_bsdthread_info(thread);
//.....
/*
* 加载 Mach-O 文件
* Load the Mach-O file.
*/

/*
* NOTE: An error after this point indicates we have potentially
* destroyed or overwrote some process state while attempting an
* execve() following a vfork(), which is an unrecoverable condition.
*/

/*
* We reset the task to 64-bit (or not) here. It may have picked up
* a new map, and we need that to reflect its true 64-bit nature.
*/

task_set_64bit(task,
((imgp->ip_flags & IMGPF_IS_64BIT) == IMGPF_IS_64BIT));

/*
* 实际加载我们之前想要加载的镜像
* Actually load the image file we previously decided to load.
*/
lret = load_machfile(imgp, mach_header, thread, map, &load_result);

//.....
/* load_machfile() maps the vnode */
(void)ubc_map(imgp->ip_vp, PROT_READ | PROT_EXEC);

/*
* Set up the system reserved areas in the new address space.
*/
vm_map_exec(get_task_map(task),
task,
(void *) p->p_fd->fd_rdir,
cpu_type());
//.......
}

exec_mach_imgact 其实会对Mach O文件的header信息校验,确保当前的文件是Mach O文件,并且校验它的cpu类型是否符合当前设备平台。当这些都校验通过后就通过load_machfile加载Mach O文件。

load_return_t load_machfile(
struct image_params *imgp,
struct mach_header *header,
thread_t thread,
vm_map_t new_map,
load_result_t *result
)
{
//......
//解析mach文件
lret = parse_machfile(vp, map, thread, header, file_offset, macho_size,0, result);
//.....
}

load_machfile会加载Mach-O中的各种load monmand命令。在其内部会禁止数据段执行防止溢出漏洞攻击,还会设置地址空间布局随机化(ASLR),还有一些映射的调整。load_machfile内部直接调用了parse_machfile方法。

static load_return_t parse_machfile(
struct vnode *vp,
vm_map_t map,
thread_t thread,
struct mach_header *header,
off_t file_offset,
off_t macho_size,
int depth,
load_result_t *result
)
{
//.....
//类型校验
if (header->magic == MH_MAGIC_64 || header->magic == MH_CIGAM_64) {
mach_header_sz = sizeof(struct mach_header_64);
}
/*
* Break infinite recursion
* 打破因为层级较深导致的无限递归
*/
if (depth > 6) {
return(LOAD_FAILURE);
}
task = (task_t)get_threadtask(thread);

//深度计数值递增
// depth负责parse_machfile 遍历次数,第一次是解析mach-o, 第二次'load_dylinker'会调用此函数来进行dyld的解析
depth++;

/*
* 检查是否是正确的计算机类型
* Check to see if right machine type.
*/
if (((cpu_type_t)(header->cputype & ~CPU_ARCH_MASK) != cpu_type()) ||
!grade_binary(header->cputype,
header->cpusubtype & ~CPU_SUBTYPE_MASK))
return(LOAD_BADARCH);

abi64 = ((header->cputype & CPU_ARCH_ABI64) == CPU_ARCH_ABI64);

//主要是用来对Mach-O做检测,会检测Mach-O头部,解析其架构、检查imgp等内容,
//并拒绝接受Dylib和Bundle这样的文件,这些文件会由dyld负责加载
switch (header->filetype) {
case MH_OBJECT:
case MH_EXECUTE:
case MH_PRELOAD:
//第一次的时候这里会走通
if (depth != 1) {
return (LOAD_FAILURE);
}
break;
case MH_FVMLIB:
case MH_DYLIB:
if (depth == 1) {
return (LOAD_FAILURE);
}
break;
case MH_DYLINKER:
//第二次的时候这里会走通
if (depth != 2) {
return (LOAD_FAILURE);
}
break;
default:
return (LOAD_FAILURE);
}
//.....
/*
* Round size of Mach-O commands up to page boundry.
*/
size = round_page(mach_header_sz + header->sizeofcmds);
if (size <= 0)
return(LOAD_BADMACHO);

/*
* 将加载命令映射到内核地址
* Map the load commands into kernel memory.
*/

//.......

/*
* 扫描每个命令,处理每个命令
* Scan through the commands, processing each one as necessary.
*/
for (pass = 1; pass <= 2; pass++) {
/*
* Loop through each of the load_commands indicated by the
* Mach-O header; if an absurd value is provided, we just
* run off the end of the reserved section by incrementing
* the offset too far, so we are implicitly fail-safe.
*/
offset = mach_header_sz;
//加载命令数目
ncmds = header->ncmds;
while (ncmds--) {
/*
* 获取指向命令的地址
* Get a pointer to the command.
*/
lcp = (struct load_command *)(addr + offset);
oldoffset = offset;
offset += lcp->cmdsize;
/*
* Perform prevalidation of the struct load_command
* before we attempt to use its contents. Invalid
* values are ones which result in an overflow, or
* which can not possibly be valid commands, or which
* straddle or exist past the reserved section at the
* start of the image.
*/
if (oldoffset > offset ||
lcp->cmdsize < sizeof(struct load_command) ||
offset > header->sizeofcmds + mach_header_sz) {
ret = LOAD_BADMACHO;
break;
}
/*
* Act on struct load_command's for which kernel
* intervention is required.
*/
switch(lcp->cmd) {
/*加载64位segment*/
case LC_SEGMENT_64:
if (pass != 1)
break;
ret = load_segment_64(
(struct segment_command_64 *)lcp,
pager,
file_offset,
macho_size,
ubc_getsize(vp),
map,
result);
break;
/*加载32位segment*/
case LC_SEGMENT:
if (pass != 1)
break;
ret = load_segment(
(struct segment_command *) lcp,
pager,
file_offset,
macho_size,
ubc_getsize(vp),
map,
result);
break;
/*加载线程数据*/
case LC_THREAD:
if (pass != 2)
break;
ret = load_thread((struct thread_command *)lcp,
thread,
result);
break;
/*加载unix线程数据*/
case LC_UNIXTHREAD:
if (pass != 2)
break;
ret = load_unixthread(
(struct thread_command *) lcp,
thread,
result);
break;
/*加载动态加载器 程序需要的dyld的路径*/
case LC_LOAD_DYLINKER:
if (pass != 2)
break;
if ((depth == 1) && (dlp == 0)) {
dlp = (struct dylinker_command *)lcp;
dlarchbits = (header->cputype & CPU_ARCH_MASK);
} else {
ret = LOAD_FAILURE;
}
break;
/*加载代码签名加载器*/
case LC_CODE_SIGNATURE:
/* CODE SIGNING */
if (pass != 2)
break;
/* pager -> uip ->
load signatures & store in uip
set VM object "signed_pages"
*/
ret = load_code_signature(
(struct linkedit_data_command *) lcp,
vp,
file_offset,
macho_size,
header->cputype,
(depth == 1) ? result : NULL);
if (ret != LOAD_SUCCESS) {
printf("proc %d: load code signature error %d "
"for file \"%s\"\n",
p->p_pid, ret, vp->v_name);
ret = LOAD_SUCCESS; /* ignore error */
} else {
got_code_signatures = TRUE;
}
break;
default:
/* Other commands are ignored by the kernel */
ret = LOAD_SUCCESS;
break;
}
if (ret != LOAD_SUCCESS)
break;
}
if (ret != LOAD_SUCCESS)
break;
}
//加载成功
if (ret == LOAD_SUCCESS) {
if (! got_code_signatures) {
struct cs_blob *blob;
/* no embedded signatures: look for detached ones */
blob = ubc_cs_blob_get(vp, -1, file_offset);
if (blob != NULL) {
/* get flags to be applied to the process */
result->csflags |= blob->csb_flags;
}
}
//加载动态链接器dlp为从上面获取到的动态dyliner的路径
if (dlp != 0)
//加载dylinker
ret = load_dylinker(dlp, dlarchbits, map, thread, depth, result, abi64);
//......
}
//.....
return(ret);
}

在parse_machfile中会将Mach O文件中loadCommand部分的命令加载到内存执行。这些command中最重要的命令是LC_SEGMENT/LC_SEGMENT_64 以及 LC_UNIXTHREAD/LC_MAINLC_SEGMENT/LC_SEGMENT_64用于告诉加载器某些可执行的部分代码需要映射到指定的内存区域,LC_UNIXTHREAD/LC_MAIN 告诉加载器在可执行代码加载后的入口点,有了它动态加载器知道在加载结束后跳到哪个位置,这些入口点要么是main方法,要么是在编译时期编译器添加的启动代码。如果没有动态库,这些命令已经足够了,但是如果有依赖动态库,在将所有的segments映射到内存中的时候,加载器还需要处理可执行代码的全部依赖,这些在后面的时候会详细介绍。由于parse_machfile会在后续递归调用,所以这里用了一个depth来控制递归深度。

当depth等于1的时候:

switch (header->filetype) {
case MH_OBJECT:
case MH_EXECUTE:
case MH_PRELOAD:
//depth=1 时候这里会走通
if (depth != 1) {
return (LOAD_FAILURE);
}
break;
case MH_FVMLIB:
case MH_DYLIB:
//depth=1 时候这里会失败
if (depth == 1) {
return (LOAD_FAILURE);
}
break;
case MH_DYLINKER:
//depth=1 时候这里会失败
if (depth != 2) {
return (LOAD_FAILURE);
}
break;
default:
return (LOAD_FAILURE);
}

这时候如果Mach O 文件类型为 MH_OBJECTMH_EXECUTEMH_PRELOAD 就会走到下面的流程,下面的流程会先执行LC_SEGMENT_64,LC_SEGMENT将某些可执行的代码映射到指定的内存区域。然后再执行LC_THREAD,LC_UNIXTHREAD,LC_LOAD_DYLINKER,LC_CODE_SIGNATURE。这里最为关键的命令是LC_LOAD_DYLINKER,它会对dyld进行赋值。在最后会调用:

ret = load_dylinker(dlp, dlarchbits, map, thread, depth, result, abi64);
static
load_return_t
load_dylinker(
struct dylinker_command *lcp,
integer_t archbits,
vm_map_t map,
thread_t thread,
int depth,
load_result_t *result,
boolean_t is_64bit
)
{
//..........
/*
* 首先直接映射dyld
* First try to map dyld in directly. This should work most of
* the time since there shouldn't normally be something already
* mapped to its address.
*/
// 解析dyld
ret = parse_machfile(vp, map, thread, &header, file_offset, macho_size, depth, &myresult);
/*
* If it turned out something was in the way, then we'll take
* take this longer path to map dyld into a temporary map and
* copy it into destination map at a different address.
*/
//如果加载成功设置会返回entry_point
if (ret == LOAD_SUCCESS) {
result->dynlinker = TRUE;
result->entry_point = myresult.entry_point;
(void)ubc_map(vp, PROT_READ | PROT_EXEC);
}
out:
vnode_put(vp);
return (ret);
}

load_dylinker函数主要负责加载dyld,以及调用parse_machfile函数对dyld解析。这时候header->filetype = MH_DYLINKER 并且 depth = 2.

switch (header->filetype) {
case MH_OBJECT:
case MH_EXECUTE:
case MH_PRELOAD:
//depth=2 时候这里走不通
if (depth != 1) {
return (LOAD_FAILURE);
}
break;
case MH_FVMLIB:
case MH_DYLIB:
//depth=2 时候这里会走通
if (depth == 1) {
return (LOAD_FAILURE);
}
break;
case MH_DYLINKER:
//depth=2 时候这里会走通
if (depth != 2) {
return (LOAD_FAILURE);
}
break;
default:
return (LOAD_FAILURE);
}

下面的流程会先执行LC_SEGMENT_64,LC_SEGMENT将某些可执行的代码映射到指定的内存区域。然后再执行LC_THREAD,LC_UNIXTHREAD,LC_CODE_SIGNATURE。这时候dyld就被加载进来了,在LC_UNIXTHREAD中会设置dyld的entry point。

我们来看下LC_UNIXTHREAD

case LC_UNIXTHREAD:
if (pass != 2)
break;
ret = load_unixthread(
(struct thread_command *) lcp,
thread,
result);
break;
static
load_return_t
load_thread(
struct thread_command *tcp,
thread_t thread,
load_result_t *result
)
{
//.....
task = get_threadtask(thread);
//.....

lret = load_threadstate(thread,
(unsigned long *)(((vm_offset_t)tcp) +
sizeof(struct thread_command)),
tcp->cmdsize - sizeof(struct thread_command));
if (lret != LOAD_SUCCESS)
return (lret);

if (result->thread_count == 0) {
lret = load_threadstack(thread,
(unsigned long *)(((vm_offset_t)tcp) +
sizeof(struct thread_command)),
tcp->cmdsize - sizeof(struct thread_command),
&result->user_stack,
&customstack);
if (customstack)
result->customstack = 1;
else
result->customstack = 0;

if (lret != LOAD_SUCCESS)
return(lret);

lret = load_threadentry(thread,
(unsigned long *)(((vm_offset_t)tcp) +
sizeof(struct thread_command)),
tcp->cmdsize - sizeof(struct thread_command),
&result->entry_point);
if (lret != LOAD_SUCCESS)
return(lret);
}
else
thread_resume(thread);

result->thread_count++;

return(LOAD_SUCCESS);
}

这里最关键的部分是load_threadentry

static
load_return_t
load_threadentry(
thread_t thread,
unsigned long *ts,
unsigned long total_size,
mach_vm_offset_t *entry_point
)
{
//......
/*
* Set the thread state.
*/
*entry_point = MACH_VM_MIN_ADDRESS;
while (total_size > 0) {
flavor = *ts++;
size = *ts++;
entry_size = (size+2)*sizeof(unsigned long);
if (entry_size > total_size)
return(LOAD_BADMACHO);
total_size -= entry_size;
/*
* Third argument is a kernel space pointer; it gets cast
* to the appropriate type in thread_entrypoint() based on
* the value of flavor.
*/
ret = thread_entrypoint(thread, flavor, (thread_state_t)ts, size, entry_point);
if (ret != KERN_SUCCESS) {
return(LOAD_FAILURE);
}
ts += size; /* ts is a (unsigned long *) */
}
return(LOAD_SUCCESS);
}
kern_return_t
thread_entrypoint(
__unused thread_t thread,
int flavor,
thread_state_t tstate,
__unused unsigned int count,
mach_vm_offset_t *entry_point
)
{
/*
* Set a default.
*/
if (*entry_point == 0)
*entry_point = VM_MIN_ADDRESS;

switch (flavor) {
case x86_THREAD_STATE32:
{
x86_thread_state32_t *state25;

state25 = (i386_thread_state_t *) tstate;
*entry_point = state25->eip ? state25->eip: VM_MIN_ADDRESS;
break;
}

case x86_THREAD_STATE64:
{
x86_thread_state64_t *state25;

state25 = (x86_thread_state64_t *) tstate;
*entry_point = state25->rip ? state25->rip: VM_MIN_ADDRESS64;
break;
}
}
return (KERN_SUCCESS);
}

thread_entrypoint 中会对entry_point进行设置。那么state25->rip又是什么呢?它就是dyld的入口地址****_dyld_start。也就是说在dyld加载到内存结束后,会将入口指向_dyld_start****。
如果想对dyld有比较直观的了解,其实在deviceSupport/xx.x.x/Symbols/usr/lib/dyld.到此为止就是从launchd开始到dyld加载结束到指定dyld的入口地址的全部过程。下篇博客将会对dyld进行详细的介绍。

我们编译出的.app 文件其实也是Mach O的一种,我们在对Runtime进行分析之前总是绕不开Mach O 和 dyld 这两大块内容,所以在这个系列开始之前对带大家了解下Mach O 和 dyld。

下图是Mach O文件的组成,主要分成三大部分,我们接下来以otool命令行工具来输出这些数据的内容:

Header: 头部区域;
LoadCommands: 加载命令,由多个Segments command组成;
Data:Segments的具体数据;

我们一一看下这些部分的组成数据结构以及功能:

struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
  • magic: 从这个字段上可以看出当前镜像的平台是64位还是32位的:
    由于这里只研究64位的,所以这个值为MH_MAGIC_64
  • cputype: cpu类型 (具体可查看mach/machine.h
  • cpusubtype: cpu子类型 (具体可查看mach/machine.h
  • filetype : 文件类型,是可执行文件还是动态链接文件
  • ncmds : load command 的数量
  • sizeofcmds : load command 占用的空间大小
  • flags : dyld 加载的标志

下面是otool工具打印出的某个应用的Header部分:

$ otool -hV IDLFundation
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL 0x00 EXECUTE 85 8400 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE

这里简单提下几种常见的Mach-O类型,也就是上面提到的filetype:

  • MH_EXECUTE
    可单独执行的可执行文件,这种文件一般有main方法作为入口。

  • MH_DYLINKER
    动态链接器,用于加载动态库 在iOS 中就是/usr/lib/dyld,/usr/lib/dyld仅会处理MH_BUNDLE, MH_DYLIB,MH_EXECUTE类型文件。

  • MH_OBJECT
    .m,.c等文件编译出来的目标文件,文件后缀是.o。Static Library类型产出是MH_OBJECT类型文件的archieve。

  • MH_FVMLIB MH_CORE MH_PRELOAD MH_DYLIB
    动态库文件,包括.dylib文件,动态framework;对应Dynamic Library类型产出。

  • MH_BUNDLE
    独立的二进制文件,不支持在项目中添加Link Binary使用。可以在Copy Bundle Resources中作为资源添加。 通过NSBundle load的方式加载;对应Bundle类型产出。典型的例子就是/System/Library/AccessibilityBundles目录的.axbundle后缀的文件。

  • MH_DSYM
    存储二进制文件符号信息的文件,用于Debug分析;

Load Commands

load commands 紧随着mach_header,它的总大小以及命令数都存储在mach_header中。

struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

load_command结构比较简单就两个字段cmd和cmdsize,cmd存放的是命令的类型,每个类型有一个专门的结构体。

这些加载命令在Mach-O文件加载解析时,用于指导如何加载对应的二进制数据段,下面列出了一些加载命令类型的常量,更多的加载命令类型可以查看loader.h

#define	LC_SEGMENT	     0x1	/* segment of this file to be mapped */
#define LC_SEGMENT_64 0x19 /* 64-bit segment of this file to be mapped */
#define LC_SYMTAB 0x2 /* link-edit stab symbol table info */
#define LC_SYMSEG 0x3 /* link-edit gdb symbol table info (obsolete) */
#define LC_THREAD 0x4 /* thread */
#define LC_UNIXTHREAD 0x5 /* unix thread (includes a stack) */
#define LC_LOADFVMLIB 0x6 /* load a specified fixed VM shared library */
#define LC_IDFVMLIB 0x7 /* fixed VM shared library identification */
#define LC_IDENT 0x8 /* object identification info (obsolete) */
#define LC_FVMFILE 0x9 /* fixed VM file inclusion (internal use) */
#define LC_PREPAGE 0xa /* prepage command (internal use) */
#define LC_DYSYMTAB 0xb /* dynamic link-edit symbol table info */
#define LC_LOAD_DYLIB 0xc /* load a dynamically linked shared library */
#define LC_ID_DYLIB 0xd /* dynamically linked shared lib ident */
#define LC_LOAD_DYLINKER 0xe /* load a dynamic linker */
#define LC_ID_DYLINKER 0xf /* dynamic linker identification */
#define LC_PREBOUND_DYLIB 0x10 /* modules prebound for a dynamically */
/* linked shared library */
#define LC_ROUTINES 0x11 /* image routines */
#define LC_SUB_FRAMEWORK 0x12 /* sub framework */
#define LC_SUB_UMBRELLA 0x13 /* sub umbrella */
#define LC_SUB_CLIENT 0x14 /* sub client */
#define LC_SUB_LIBRARY 0x15 /* sub library */
#define LC_TWOLEVEL_HINTS 0x16 /* two-level namespace lookup hints */
#define LC_PREBIND_CKSUM 0x17 /* prebind checksum */
#define LC_LOAD_WEAK_DYLIB (0x18 | LC_REQ_DYLD)

#define LC_ROUTINES_64 0x1a /* 64-bit image routines */
#define LC_UUID 0x1b /* the uuid */
#define LC_RPATH (0x1c | LC_REQ_DYLD) /* runpath additions */
#define LC_CODE_SIGNATURE 0x1d /* local of code signature */
#define LC_SEGMENT_SPLIT_INFO 0x1e /* local of info to split segments */
#define LC_REEXPORT_DYLIB (0x1f | LC_REQ_DYLD) /* load and re-export dylib */

下面是通过otool命令来输出的load commands:

otool -l IDLFundation
Load command 20
cmd LC_LOAD_DYLIB
cmdsize 80
name @rpath/FLAnimatedImage.framework/FLAnimatedImage (offset 24)
time stamp 2 Thu Jan 1 08:00:02 1970
current version 1.0.0
compatibility version 1.0.0

这里比较重要的是加载segments的命令:

它用于表示当前文件的一部分将会被映射到64位任务的地址空间

struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* 对于64位系统来说这个值恒为 LC_SEGMENT_64 */
uint32_t cmdsize; /* 用于表示这个命令的大小 */
char segname[16]; /* 该segment的名字 */
uint64_t vmaddr; /* 这个segment的虚拟内存地址空间 */
uint64_t vmsize; /* 这个segment的虚拟内存大小 */
uint64_t fileoff; /* 这个segment位于这个文件的偏移 */
uint64_t filesize; /* 文件的大小 */
vm_prot_t maxprot; /* 最大虚拟内存保护 */
vm_prot_t initprot; /* 初始的内存保护 */
uint32_t nsects; /* segments的session数 */
uint32_t flags; /* flags */
};

通过这个命令会将该段对应的文件从offset处加载 file size大小到虚拟内存 vmaddr处。

Segments & Session

Mach O文件中有一系列的Segment,每个segment又是由一系列的session构成。我们看下Mach O文件中都有哪些Segment:

  • __TEXT :代码段/只读数据段
  • __DATA :数据段
  • __PAGEZERO: 空指针陷阱段,用于捕获对NULL指针的引用
  • __LINKEDIT: 动态链接库使用的原始数据,比如符号,字符串,重定位表条目等等, 该段可读写。
  • __OBJC: 包括会被Runtime使用到的一些数据

可以用如下命令打印出各个段的数据

otool -v -s __DATA __objc_selrefs IDLFundation

下面是****__TEXT以及__DATA****segment下的重要section:

__TEXT 部分

__text	程序代码
__stubs 用于动态链接的存根
__stub_helper 用于动态链接的存根
__const 程序中const关键字修饰的常量变量
__objc_methname objc方法名
__cstring 程序中硬编码的ANSI的字符串
__objc_classname objc类名
__objc_methtype objc方法类型
__gcc_except_tab 异常处理相关
__ustring unicode字符串
__unwind_info 异常处理
__eh_frame 异常处理

__DATA 部分

__nl_symbol_ptr	动态符号链接相关,指针数组
__got 全局偏移表, Global Offset Table
__la_symbol_ptr 动态符号链接相关,也是指针数组,通过dyld_stub_binder辅助链接
__mod_init_func 初始化的全局函数地址,会在main之前被调用
__const const修饰的常量
__cstring 程序中硬编码的ANSI的字符串
__cfstring CF用到的字符串
__objc_classlist objc类列表
__objc_nlclslist objcload方法列表
__objc_catlist objc category列表
__objc_protolist objc protocol列表
__objc_imageinfo 镜像信息
__objc_const objc的常量
__objc_selrefs objc引用的SEL列表
__objc_protorefs objc引用的protocol列表
__objc_classrefs objc引用的class列表
__objc_superrefs objc父类的引用列表
__objc_ivar objcivar信息
__objc_data class信息
__bss 未初始化的静态变量区
__data 初始化的可变变量

关于Mach O结构部分先介绍到这,在博客的末尾简单列下:

-f print the fat headers
-a print the archive header
-h print the mach header
-l print the load commands
-L print shared libraries used
-D print shared library id name
-t print the text section (disassemble with -v)
-x print all text sections (disassemble with -v)
-p <routine name> start dissassemble from routine name
-s <segname> <sectname> print contents of section
-d print the data section
-o print the Objective-C segment
-r print the relocation entries
-S print the table of contents of a library (obsolete)
-T print the table of contents of a dynamic shared library (obsolete)
-M print the module table of a dynamic shared library (obsolete)
-R print the reference table of a dynamic shared library (obsolete)
-I print the indirect symbol table
-H print the two-level hints table (obsolete)
-G print the data in code table
-v print verbosely (symbolically) when possible
-V print disassembled operands symbolically
-c print argument strings of a core file
-X print no leading addresses or headers
-m don't use archive(member) syntax
-B force Thumb disassembly (ARM objects only)
-q use llvm's disassembler (the default)
-Q use otool(1)'s disassembler
-mcpu=arg use `arg' as the cpu for disassembly
-j print opcode bytes
-P print the info plist section as strings
-C print linker optimization hints
--version print the version of /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool

大家可以通过任何途径获取到你想研究应用到ipa包,然后把后缀名改为zip,解压缩得到Payload文件夹,里面就是你的APP,打开终端,直接cd到你的xxxx.app目录下。具体做法,输入cd,然后把xxxx.app直接拖到终端里打个回车。然后就可以通过otool命令来进行研究它了。

还有一个otool的扩展版本jtool大家有兴趣可以看下

在之前的博客《iOS 分类扩展与协议总结》中介绍了分类的用法以及性质,该博客将会从分类的源码对分类进一步分析。

首先我们先看下分类的数据结构:

struct category_t {
const char *name; //类的名字
classref_t cls; //类
struct method_list_t *instanceMethods; //catogies 中所有给类添加的实例方法列表
struct method_list_t *classMethods; //catogies 中所有给类添加的类方法列表
struct protocol_list_t *protocols; //catogies 中所有给类添加的协议列表
struct property_list_t *instanceProperties; //catogies 中所有给类添加的属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties; //catogies 中所有给类添加的类属性

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

从上述对象中可以看出通过category可以添加实例方法,类方法,协议,属性,但是不能为对象添加实例变量。因为category是在运行时添加到类中的,这时候类的内存布局已经确定,如果添加实例变量就会破坏类的内部布局。

接下来我们就从runtime最开始介绍分类是如何添加到类中的。

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();
lock_init();
exception_init();

//向dyld注册了回调函数,当image map到内存中,当初始化完成image时和卸载image的时候都会回调注册者
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

在_objc_init方法中会向dyld注册三个十分关键的回调函数:map_images,load_images,unmap_image

在镜像被加载后会回调map_images,分类的添加就是在这个阶段完成的,这以后会调用map_images_nolock方法来处理被dyld映射进来的镜像。map_images_nolock方法很长,但是和我们今天代码相关的只有

_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
//......
// Discover categories.
for (EACH_HEADER) {
category_t **catlist = _getObjc2CategoryList(hi, &count);
//查看是否包含类属性
bool hasClassProperties = hi->info()->hasCategoryClassProperties();

for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
//获取到所属的类
Class cls = remapClass(cat->cls);
// ....
// Process this category.
bool classExists = NO;
//如果有实例方法,协议或者实例属性
if (cat->instanceMethods || cat->protocols || cat->instanceProperties) {
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
//将分类添到属性,方法,协议添加到对应的类中
remethodizeClass(cls);
classExists = YES;
}
//......
}
//如果有类方法,协议或者类属性
if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) {
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
//......
}
}
}

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

//.......
// +load handled by prepare_load_methods()
if (DebugNonFragileIvars) {
realizeAllClasses();
}
//.......
}

_read_images 会从镜像中读取很多数据,这里只关注分类的部分。

首先通过****_getObjc2CategoryList****从_DATA_部分的__objc_catlist获取到当前镜像中全部的分类。__objc_catlist这个数据是哪里来的呢?其实是编译时候生成的。

那么编译阶段,编译器做了哪些事情呢?

  • 编译器生成了实例方法列表
  • 编译器生成了category本身
  • 编译器在DATA段下的objc_catlist section里保存了一个大小为1的category_t的数组

objc_catlist section就是在加载镜像的时候被加载进来的。

我们继续,然后针对每个分类调用addUnattachedCategoryForClass

static void addUnattachedCategoryForClass(category_t *cat, Class cls, header_info *catHeader) {
runtimeLock.assertLocked();

// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}

在分析addUnattachedCategoryForClass之前我们先看下unattachedCategories方法:

static NXMapTable *unattachedCategories(void) {
runtimeLock.assertLocked();
static NXMapTable *category_map = nil;
if (category_map) return category_map;
// fixme initial map size
category_map = NXCreateMapTable(NXPtrValueMapPrototype, 16);
return category_map;
}

注意category_map是静态的,首次的时候会通过NXCreateMapTable(NXPtrValueMapPrototype, 16)创建一个MapTable,后续会将同一个category_map返回。category_map存放的是什么呢?它存放的是 class => categories 映射关系,这些分类是未添加到对应类上的所以叫做unattachedCategories。我们再回到addUnattachedCategoryForClass:

在调用unattachedCategories获取到全部类以及它的分类映射map之后,通过NXMapGet(cats, cls) 获取到的是cats map中以cls类作为key的,为添加到cls的分类列表,然后开辟一个list->count + 1 再加sizeof(*list)大小的空间,用于存放cat以及catHeader。然后再将这个列表添加到category_map中。所以经过addUnattachedCategoryForClass处理后。我们获得到的数据将会以如下的形式组织起来:

接下来我们来看下分类的添加过程:

static void remethodizeClass(Class cls) {
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}

remethodizeClass方法中先通过unattachedCategoriesForClass获取到当前类的未添加的分类cats:

static category_list *
unattachedCategoriesForClass(Class cls, bool realizing)
{
runtimeLock.assertLocked();
return (category_list *)NXMapRemove(unattachedCategories(), cls);
}

注意这里返回分类列表后会调用NXMapRemove从category_map中移除。

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches) {
if (!cats) return;
bool isMeta = cls->isMetaClass();
// 方法列表
method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));
// 属性列表
property_list_t **proplists = (property_list_t **)malloc(cats->count * sizeof(*proplists));
// 协议列表
protocol_list_t **protolists = (protocol_list_t **)malloc(cats->count * sizeof(*protolists));

// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
//i 表示分类列表序号
while (i--) { //这里注意是逆序遍历,所以最后加载的最先添加到类中
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}

//将catories中的方法属性以及协议添加到rw中
auto rw = cls->data();

prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);

rw->properties.attachLists(proplists, propcount);
free(proplists);

rw->protocols.attachLists(protolists, protocount);
free(protolists);
}

分类的添加的逻辑位于attachCategories方法中。这里要特别注意理解数据结构的组织,为了帮助大家理解,我这里画了一张图:

mlists,proplists,protolists如上图所示,从横向看,mlists,proplists,protolists是当前类所有分类的方法列表,属性列表以及协议列表。从纵向看,分别是某个分类的方法列表,协议列表,以及属性列表。这里需要十分注意的是上面的遍历是逆序遍历,所以最后加载的分类最先添加到类中,也就是说后添加的分类的方法,协议,以及属性会覆盖先添加的同名的方法,协议,以及属性,这就导致查找某个方法的时候会逆着分类加载链寻找,一旦找到立刻停止。

紧接着将当前类的所有方法列表,属性列表和分类列表追加到类的可写区域中。这里我们再重点看下attachLists方法。

void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
// 原有数组的元素个数
uint32_t oldCount = array()->count;
// 待添加数组元素个数
uint32_t newCount = oldCount + addedCount;
// 为新数组开辟对应的空间
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//原有的数据向后腾出addedCount大小的空间,给addedLists。
memmove(array()->lists + addedCount/*指向用于存储复制内容的目标数组的起始地址*/, array()->lists/*指向要复制的数据源*/, oldCount * sizeof(array()->lists[0]/*要被复制的字节数*/));
//将addedLists添加到原有数据的前面。
memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}

attachLists其实是将该类所有的分类对应的方法列表,属性列表,协议列表,添加到类的可读写区域中,attachLists方法会重新开辟一个内存空间,将原有的数据向后腾出空间,给分类对应的元素。这意味着什么?意味着分类的方法,协议,以及属性,会被添加到类原有的方法,协议,以及属性之前。

通过上面的源码解析不难理解下面的一些细节点:

  • category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA。只不过category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会停止查找。

  • 在编译路径中位于后面的分类会先添加到列表,所以最后编译的分类最终生效。

iOS 这部分代码位于 objc-runtime/runtime/NSObject.mm:

1 对象的创建 alloc/init:

调用栈

--- alloc 
--- _objc_rootAlloc
--- callAlloc
--- allocWithZone
--- class_createInstance
--- _class_createInstanceFromZone
--- calloc
--- initIsa

--- init
--- _objc_rootInit

我们在创建对象的时候会调用 alloc 方法为要创建的对象分配内存空间:

+ (id)alloc {
return _objc_rootAlloc(self);
}

在alloc中会调用_objc_rootAlloc

id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

在_objc_rootAlloc中会调用callAlloc,它有两个参数第一个参数表示是否检查cls参数是否为空,第二个参数表示是否在zone中分配这个内存。

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil/*false*/, bool allocWithZone=false/*true*/)
{
if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
//hasCustomAWZ( )方法是用来判断当前class是否有自定义的allocWithZone。
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// 没有自定义的alloc/allocWithZone实现的时候
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif

// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}

首先看下hasCustomAWZ

bool hasCustomAWZ() {
return ! bits.hasDefaultAWZ();
}

bool hasDefaultAWZ( ) {
return data()->flags & RW_HAS_DEFAULT_AWZ;
}

#define RW_HAS_DEFAULT_AWZ (1<<16)

它实际上是查看metaclass 中的对应标识位flags的RW_HAS_DEFAULT_AWZ来查看当前的class或者是superclass是否有默认的alloc/allocWithZone:如果这个标志位被标记了那么hasCustomAWZ就为NO.

cls->canAllocFast() 这个一般情况下会返回false. 代码中有注解No ctors, raw isa, etc的情况下cls->canAllocFast() 返回YES.具体还不是很清楚这是什么意思。待弄明白后补上,这里记得返回false就好。

我们以最常见的:没有自定义alloc/allocWithZone:的情况作为简化后的分析对象:
这时候代码可以简化为:

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil/*false*/, bool allocWithZone=false/*true*/)
{
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}

我们继续往下看:

+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;

#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif

if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}

我们记住这里zone传进去的是nil,所以_objc_rootAllocWithZone代码简化后如下:

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;

(void)zone;
obj = class_createInstance(cls, 0);

if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}


id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
// Read class's info bits all at once for performance
// 当前class或者superclass 是否有构造方法的实现
bool hasCxxCtor = cls->hasCxxCtor();
// 判断当前class或者superclass 是否有析构方法的实现。
bool hasCxxDtor = cls->hasCxxDtor();
// 获取对象的大小
size_t size = cls->instanceSize(extraBytes);
//....
//分配size内存空间
obj = (id)calloc(1, size);
//初始化isa
obj->initIsa(cls);
//是否有构造函数
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}

_class_createInstanceFromZone 方法中首先判断当前class或者superclass 是否有构造方法和析构方法。然后从类对象中获取要创建当前对象所需要的空间大小,紧接着就调用calloc分配内存,然后初始化Isa结构体。如果有构造函数的情况下还需要调用_objc_constructOrFree。_objc_constructOrFree最终会找到该类的C++构造方法,向它发送消息。

- (id)init {
return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
2 对象的持有 retain

调用栈

对象的持有实际上是在对应的SideTable散列表中通过当前对象的地址作为散列值,找到存储当前对象引用计数的哈希表将引用计数增加1.我们下面看下整个调用过程:

--- retain
--- rootRetain
--- sidetable_retain
- (id)retain {
return ((id)self)->rootRetain();
}
ALWAYS_INLINE id 
objc_object::rootRetain()
{
return rootRetain(false, false);
}
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;

bool sideTableLocked = false;
bool transcribeToSideTable = false;

isa_t oldisa;
isa_t newisa;

do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++

if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}

if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}

这里最关键的是sidetable_retain,至于什么是SiteTable,以及对象引用计数的存储介绍可以查看 我准备的一个面试题:

谈谈iOS的内存管理方式的理解来理解如何查找散列表的过程。

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();

return (id)this;
}
3 对象的释放 release

调用栈

对象的释放实际上是在对应的SideTable散列表中通过当前对象的地址作为散列值,找到存储当前对象引用计数的哈希表将引用计数减1.如果减去1后为0 那么就会调用dealloc释放对象:

--- release
--- rootRelease
--- sidetable_release
- (oneway void)release {
((id)self)->rootRelease();
}
objc_object::rootRelease()
{
return rootRelease(true, false);
}

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;

bool sideTableLocked = false;

isa_t oldisa;
isa_t newisa;

retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));

if (slowpath(sideTableLocked)) sidetable_unlock();
return false;

underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate

// abandon newisa to undo the decrement
newisa = oldisa;

if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}

// Transfer retain count from side table to inline storage.

if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}

// Try to remove some retain counts from the side table.
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.

if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}

if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}

// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}

// Really deallocate.

if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

if (slowpath(sideTableLocked)) sidetable_unlock();

__sync_synchronize();
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return true;
}

uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

bool do_dealloc = false;

table.lock();
//在散列表中找到当前对象对应的引用计数
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
//没找到,标记为已经销毁
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
//引用计数减去1
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
//释放已经释放
if (do_dealloc && performDealloc) {
//调用dealloc
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
4 对象的销毁 dealloc

在介绍对象释放的最后有提到在某个对象的引用计数减为0的时候会调用对象的dealloc. 下面是相关的方法调用:

- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
assert(obj);

obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?

if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id 
object_dispose(id obj)
{
if (!obj) return nil;

objc_destructInstance(obj);
free(obj);

return nil;
}

void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}

return obj;
}

这里需要注意的是rootDealloc方法,它会先判断是否是taggpointer,如果是的话就直接返回,因为这种对象不需要释放内存,它的值就存放在指针中,这部分相关的内容可以查看
谈谈iOS的内存管理方式的理解,紧接会判断当前对象的具体情况,如果当前对象是Nonpointer_ISA类型,那么要看相应的标识位,判断是否是弱引用,是否有关联对象,是否有C++析构函数,是否用到了散列表?如果都没有的话就直接free对象。否则通过object_dispose进行后续处理:

相关的具体处理最终在objc_destructInstance中实现。大家可以看下该方法的注释:

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/

这里面重点针对是否有C++析构函数,是否有关联对象,是否有弱引用进行了对应的处理,我们接下来重点对这部分进行介绍:

调用析构函数是否对象

void object_cxxDestruct(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
object_cxxDestructFromClass(obj, obj->ISA());
}


static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);

// Call cls's dtor first, then superclasses's dtors.

for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
cls->nameForLogging());
}
(*dtor)(obj);
}
}
}

实际上就是在类的方法列表中查找对应的析构函数,然后对其进行调用。如果针对这部分想要细致了解大家可以查看ARC下dealloc过程及.cxx_destruct的探究这篇博客。

ARC下在编译器插入的.cxx_desctruct方法中会完成成员变量的自动释放。而对象的实例变量的销毁发生在根类[NSObject dealloc]方法中。

关联属性移除

关于关联对象大家可以查看对应的博客,它其实是由全局AssociationsManager进行管理的,在这里会找到对应的关联对象,然后添加到elements,然后对elements里面的对象调用ReleaseValue进行移除。这样和这个对象相关的关联对象就从AssociationsManager中移除了。

void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}

清除剩余标识位

inline void 
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
// 普通类型的指针指向的对象
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
// non-pointer isa 类型的对象。这种对象之所以要和普通对象区分开是因为它的弱引用标记以及是否使用散列表信息都存在“指针”中,所以要分开处理,但是处理的流程都是一样的,都是查看是否被弱引用指针指向,如果有则将这些设置为nil。紧接着从散列表中清除引用计数。最后将弱引用项从弱引用表中移除。
clearDeallocating_slow();
}

assert(!sidetable_present());
}

clearDeallocating 中实际上是用于处理剩余的标识位,这里分成是否是non-pointer isa:
non-pointer isa 对象之所以要和普通对象区分开是因为它的弱引用标记以及是否使用散列表信息都存在“指针”中,所以要分开处理,但是处理的流程都是一样的,都是查看是否被弱引用指针指向,如果有则将这些设置为nil。紧接着从散列表中清除引用计数。最后将弱引用项从弱引用表中移除。

void 
objc_object::sidetable_clearDeallocating()
{
//找到对应的散列表
SideTable& table = SideTables()[this];

// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
//当前对象是否是弱引用对象
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
//如果是的话则将对应对象对它的引用设置为nil
weak_clear_no_lock(&table.weak_table, (id)this);
}
//清除引用计数
table.refcnts.erase(it);
}
table.unlock();
}

sidetable_clearDeallocating 就做了三件事情:

  1. 找到对应的散列表
  2. 查看弱引用标记,是否是弱引用,如果是的话则将其他对象对它的引用置为nil,并将弱引用标志从SideTable的弱引用表中移除。
  3. 将该对象对应的引用计数表从SideTables中移除
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;

//找到对应的弱引用项入口
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
return;
}

// zero out references
weak_referrer_t *referrers;
size_t count;

if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}

for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
//将其他对象对他的引用设置为nil
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
//移除该项
weak_entry_remove(weak_table, entry);
}
5 Weak指针原理

__weak 修饰的变量有两大特点:

  • 不会增加指向对象的引用计数 (规避循环引用)
  • 指向对象释放后,变量会自动置 nil (规避野指针访问错误)

大体的实现思路如下:

使用__weak 修饰的变量时,runtime 会生成对应的 weak_entry 结构放入 weak_table 中,以赋值对象地址生成的 hash 值为 key,以包装 __weak 修饰的指针变量地址的 entry 为 value,当赋值对象释放时,runtime 会在目标对象的 dealloc 处理过程中,以对象地址(self)为 key 去 weak_table 查找 entry ,置空 entry 指向的的所有对象指针。weak_entry 使用数组保存指针变量地址,当地址数量小于4的时候,这个数组就是个普通的内置数组,在地址数量大于4的时候,这个数组就会扩充成一个 hash table。

我们接下来看下这部分源码:

当我们声明一个指针变量的时候就会调用objc_initWeak方法:

id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}

return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

weak 对象的存储是通过storeWeak完成的,在看storeWeak代码之前我们先来看下weak_table_t

struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};

weak_table_t 中存储的每个weak_entry_t是以被引用对象地址为hash Code 进行存储的,这个没啥好说的,我们来看下weak_entry_t。

struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};

bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}

weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}

weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};

weak_entry_t 里面包含了一个union,里面有两个结构体,其中 WEAK_INLINE_COUNT 宏为 4 ,也就是说在初始状态下,这个 union 的空间有 weak_referrer_t inline_referrers[4] 这么大,当 entry 保存指针变量地址的个数不大于 4 个时,我们就可以直接使用 inline_referrers 数组,这样写的话,访问更加快速便捷。但是如果超过4个的话则使用哈希表来存储,这样会加速查找的速度。

下面是weak相关数据结构的大体示意图:

referent: 是内存上的weak对象
referrers: 是指向weak对象的所有变量

有了上面的存储结构的介绍对storeWeak会有更清晰的理解

在设置新的关联前,如果 __weak 修饰的指针变量已经关联了其他对象,那么此函数会先解除旧关联,再设置新的。如果 newObjc 是 nil,那么只会进行解除关联以及指针置 nil 操作,objc_destroyWeak 就以这种方式调用 storeWeak 来执行销毁动作,这个后面会介绍。

template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);

Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;

//..........

// Clean up old value, if any.
if (haveOld) {
//__weak 修饰的指针变量已经指向过某对象
// 需要把这个对象和此指针变量的关联断开
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}

// Assign new value, if any.
if (haveNew) {
// 关联新对象和 __weak 修饰的指针变量
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected

// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
// 设置 isa 指针的 weakly_referenced 位 / sidetable 中的 SIDE_TABLE_WEAKLY_REFERENCED 位
// 标记此对象被 __weak 修饰的指针变量指向了,dealloc 时可以加速置 nil 处理
newObj->setWeaklyReferenced_nolock();
}
// 设置 __weak 修饰的指针变量的值为 newObj
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}

SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

return (id)newObj;
}
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

if (!referent || referent->isTaggedPointer()) return referent_id;

// ensure that the referenced object is viable
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}

if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}

// now remember it and where it is being stored
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}

// Do not set *referrer. objc_storeWeak() requires that the
// value not change.

return referent_id;
}

最关键的代码在weak_register_no_lock 中,它会判断某个referent的weak_entry是否存在,如果存在则直接在这个weak_entry中添加referrer,否则新建一个weak_entry,加到weak_table中。

weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}

销毁弱引用对象

void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}

销毁弱引用对象和创建弱引用对象入口实际上是一样的,都是storeWeak,只不过这里传入的值为nil。由于传入的是非 nil 新值,storeWeak会删除而不会新建关联信息。

void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
id *referrer_id)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

weak_entry_t *entry;

if (!referent) return;
// 根据对象地址获取 entry
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 移除 entry 中值为 referrer 的指针变量地址
remove_referrer(entry, referrer);
bool empty = true;
// entry 中是否有关联的指针变量地址
if (entry->out_of_line() && entry->num_refs != 0) {
empty = false;
}
else {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}

if (empty) {
// 如果 entry 是空的话,就从 weak_table 中移除掉
weak_entry_remove(weak_table, entry);
}
}
}