前面两篇放了一张runtime的思维导图,提供了一些runtime
的常用API
,这一章主要从汇编 、C、 C++源码上分析runtime
的底层,后面还会写Runtime开发中的实际应用(构思ing中...)。本篇博客涉及到的demo
和runtime
源码会在博客结尾给出下载链接
一点半了,我靠,今天不写了,明天还要上班,睡觉去了,狗命要紧......这个文章这周一定找时间更新上去
runtime是什么❓
c,c++,汇编一起写的API,装载到内存给程序提供运行时的功能;编译期runtime下层会被编译成runtime的API。
runtime的三种调用方式
- runtimeAPI
- NSObject的API(isKindOf等)
- OC的上层(@selector)
- 注:代码中...代表不去讲解的代码,非重点
我们从一个简单实例入手,查看runtime
的原理;首先创建一个LGPerson
类,包含一个run
的实例方法;VC中的代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *p = [[LGPerson alloc] init];
[p run];
}
通过在终端输入clang -rewrite-objc ViewController.m -o new.cpp
把.m
文件转 c++
后看到如下东西:
...
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; //
};
...
LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
- 通过
typedef struct objc_object LGPerson;
可以看到,原来我们的OC对象的本质是结构体; - ·run·方法转成了
objc_msgSend
的消息可以看出:方法的本质是消息发送
- 什么是
objc_msgSend?
objc_msgSend
是汇编写的一个C函数,用于消息的发送。- 为什么用汇编去写?
C写一个函数,没法保留未知的参数,跳转到任意的指针。汇编有寄存器可以做到;汇编快速,相比于C,快50%-80%;
[p run];
被转换成((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
其中(id)p
是消息接受者;sel_registerName("run")
代表方法编号SEL _cmd
,底层是一个字符串name
;我们可以笼统的看成所有的方法都会转成类似于这样的一个实现(这里是没有带参数的):
void runIMP(id self,SEL _cmd){
...
}
通过sel
,我们可以找到imp
(函数实现的指针).怎么找到的???这个是重点(敲黑板ing),老司机源码解析告诉你。
首先消息的发送((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
前面代码就是做了类型的转换,我们可以写成如下:
objc_msgSend(s,sel_registerName("run”));
如果大家对sel_registerName
感到陌生,可以执行如下代码打印看看就知道了:
NSLog(@"%p---%p",sel_registerName("run"),@selector(run));
//*************************************************************************
2019-08-10 12:25:23.712418+0800 01---runtime[3542:307149] 0x10b64d4d3---0x10b64d4d3
方法的本质是消息发送,在发送时,还隐式的传了两个参数消息接收者和方法编号;
对象方法和类方法发送消息,向父类发送消息对比:
1. 对象方法
objc_msgSend(s,sel_registerName("run”));
向父类发送消息:objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
objc_super
结构体代码如下:
struct objc_super {
/// Specifies an instance of a class.类的实例
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
对应一个实例,就是这样的:
struct objc_super mySuper;
mySuper.receiver = p;
mySuper.super_class = class_getSuperclass([p class]);
objc_msgSendSuper(&mySuper,@selector(run))
2. 类方法
objc_msgSend(objc_getClass("LGPerson"),sel_registerName("run”));
向父类发送消息:
struct objc_super myClassSuper;
myClassSuper.receiver = p;
myClassSuper.super_class = class_getSuperclass(object_getClass([p class]));
objc_msgSendSuper(&myClassSuper,sel_registerName(“run”))
object_getClass([p class])
是元类。元类就是类所属的类。
面试题:
对象方法存在哪? 对应的类里面.
类方法存在哪? 类的元类里面,本质是一个实例方法.
- 一个类方法,在元类里面,就是一个实例 ,所以类方法存在对应的元类里面,本质是一个实例方法。
- OC的类其实也是一个对象,一个对象就要有一个它属于的类,意味着类也要有一个 isa 指针,指向他所属的类,也就是元类。
当你给对象发送消息时,消息是在寻找这个对象的类的方法列表。
当你给类发消息时,消息是在寻找这个类的元类的方法列表。
Runtime源码分析
源码下载地址:https://opensource.apple.com/ 最新的版本是objc4-750
,大家也可以去GitHub上找有注释的源码,我在最后的demo下载中也会提供一份runtime的源码。
objc_msgSend的消息的发送有两种方式:
- 快速的查找,从缓存找cache_t cache查找;
缓存来源于我们的类.这个可以通过源码验证一下,在objc4的源码中找到class:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
每一个类里面都会有一个cache,存储方法的sel,
imp,objc_msgSend首先回来这里通过sel查找imp,找到了,
直接返回,没有则下面介绍的第二种方式继续查找,找到了会存入cache。
cache_t cache;
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
下面的代码不是重点介绍的,省略了
...
}
- 慢速的查找,lookup(C,C++,汇编查找);
下面从objc_msgSend
的源码入手,查看消息的发送流程:
在runtime
源码中搜索_objc_msgSend
(方法前面加_可以查看方法的汇编代码),可以看到有各种环境下的,这里以arm64
为例子讲解:
objc_msgSend
方法执行,首先进入到ENTRY
LNilOrTagged:
/*为空则直接返回 END_ENTRY _objc_msgSend*/
b.eq LReturnZero // nil check
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
//上面这些代码 是对isa进行了处理,具体不管它,LGetIsaDone这个才是重要方法
b LGetIsaDone
LNilOrTagged这个方法走完后,我们接着往下走,来到CacheLookup,就是从缓存中查找:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
从注释可以看到,会出现两种情况: calls imp 也就是我们要的东西;或者执行objc_msgSend_uncached;下面是CacheLookup的实现
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
*
* Locate the implementation for a selector in a class method cache.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
//.macro是一个宏的意思
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
注:一般我们看源码看到类似这样注释的基本都是核心代码,一个小技巧
CacheLookup
,先进入缓存进行查找CacheHit,这是一种快速查找的方法,通过哈希表找到对应的imp
;如果没有找到--->CheckMiss,则进入慢速的查找lookup
,慢速查找找到了也会进行缓存起来;add操作是其他地方找到了会调用,添加到缓存中。下面是没找到时执行的代码:
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz x9, LGetImpMiss
.elseif $0 == NORMAL
//因为我们传入的参数 是NORMAL,所以走这里
cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
因为我们传入的参数 是NORMAL,没找到则执行__objc_msgSend_uncached
(.macro是宏定义)
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search
//里面就这一个方法,就他了,意思就是‘方法列表中查找’
MethodTableLookup
br x17
END_ENTRY __objc_msgSend_uncached
//*************************************************************
.macro MethodTableLookup
。。。
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// imp in x0
mov x17, x0
。。。
.endmacro
通过MethodTableLookup
方法列表查找imp
; 会执行 __class_lookupMethodAndLoadCache3
这个方法我们搜索时会发现怎么没有了,找不到了,很多小伙伴在这里直接懵逼了,其实这里需要从汇编回到C,干掉一个 _
搜索_class_lookupMethodAndLoadCache3
,找到如下代码:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
YES/initialize/, 因为前面编译找到了isa,所以为YES
NO/cache/, 因为没有缓存才来到这的,所以NO
YES/resolver/ 最后都没有找到imp,则是否进行动态解析,所以YES
代码的流程是这样的lookUpImpOrForward
------------> realizeClass(cls);
---------> imp = cache_getImp(cls, sel);
重映射,再一次取,这是一个漫长的方法查找过程。代码如下,老长老长了,不想看可以跳过,看的话看我写的代码注释就可以了:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {//这里肯定为NO
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
if (!cls->isRealized()) {
// Drop the read-lock and acquire the write-lock.
// realizeClass() checks isRealized() again to prevent
// a race while the lock is down.
runtimeLock.unlockRead();
runtimeLock.write();
// 没有实现则走这个方法
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
//初始化
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// 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.assertReading();
// Try this class's cache.
这里再去一次缓存的原因是1.因为多线程异步执行等原因,
缓存中有了对应的imp;2.remap重映射的原因有了imp
imp = cache_getImp(cls, sel);
if (imp) goto done;//找到了就结束
// Try this class's method lists.
{//没找到继续getMethodNoSuper_nolock,传入class ,sel,具体方法实现我下面也给出来了
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到了,缓存起来
imp = meth->imp;
goto done;//找到了,结果,回去。
}
}
//上面的流程没找到,开始找父类了 superclass
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;//nil的加入是到了NSObject可以结束了,上面没有了
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;
}
}
//再次调用getMethodNoSuper_nolock(curClass, sel);进行循环了进行循环了进行循环了,这是一个漫长的过程
// 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.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// 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;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
这里把上面过程中cache_getImp(cls, sel)没有找到后--->调用的getMethodNoSuper_nolock(cls, sel)展示给大家看一下:
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
//for 循环去方法列表中查找,从开始beginLists到结束endLists,把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;
}
而如果还是没有找到,则执行动态方法解析,就是最后的那一小段代码,我单独再抠出来:
动态方法解析
//一直找不到,从这里开始开始动态方法解析了 ,尝试方法解析一次,是的,once
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
//这里只会进来一次,triedResolver = YES;
runtimeLock.unlockRead();
//走这个方法
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// 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;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
/***********************************************************************
✨✨✨Call +resolveClassMethod or +resolveInstanceMethod.✨✨✨
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) { //不是元类
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {//是元类
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
我们这里直接查看对象方法的实现_class_resolveInstanceMethod(cls, sel, inst);
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
//消息转发来了~~~ msg(cls, SEL_resolveInstanceMethod, sel);
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
可以看到里面通过msg(cls, SEL_resolveInstanceMethod, sel);
进行了消息的转发操作;通常消息转发和动态方法解析是互不相干的,在进入消息转发机制之前,respondsToSelector:
和instancesRespondToSelector:
会被首先调用。您可以在这两个 方法中为传进来的选标提供一个IMP
。如果您实现了resolveInstanceMethod:
方法但是仍然希望正 常的消息转发机制进行,您只需要返回NO
就可以了,否则消息转发流程不执行。
OC
中有两个方法可以调用:resolveClassMethod
和resolveInstanceMethod
,既然有OC的方法,果断上demo尝试一下啦!(demo和源码都放在最后面了)其实就是借着一开始的那个例子,LGPerson
内写一个类方法 +(void)walk;
然后.m
中walk
的实现不写,我们用resolveClassMethod
代替,如下:
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"来了 老弟");
return [super resolveClassMethod:sel];
}
//*******************************************
2019-08-10 16:03:33.168525+0800 002---runtime动态解析[5727:409195] 来了 老弟
2019-08-10 16:03:36.161490+0800 002---runtime动态解析[5727:409195] 来了 老弟
会补救2次,还是没有,消息转发流程,最后抛出异常,崩溃...
这里打个断点我们通过lldb打印一下看看:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001056f660b 002---runtime动态解析`+[LGPerson resolveInstanceMethod:](self=LGPerson, _cmd="resolveInstanceMethod:", sel="run") at LGPerson.m:23:5
frame #1: 0x0000000105fdae76 libobjc.A.dylib`resolveInstanceMethod(objc_class*, objc_selector*, objc_object*) + 91
frame #2: 0x0000000105fd54e5 libobjc.A.dylib`lookUpImpOrForward + 464
frame #3: 0x0000000105fe30d4 libobjc.A.dylib`_objc_msgSend_uncached + 68
第二次执行
2019-08-10 16:10:07.579418+0800 002---runtime动态解析[5786:412162] 来了 老弟
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001056f660b 002---runtime动态解析`+[LGPerson resolveInstanceMethod:](self=LGPerson, _cmd="resolveInstanceMethod:", sel="run") at LGPerson.m:23:5
frame #1: 0x0000000105fdae76 libobjc.A.dylib`resolveInstanceMethod(objc_class*, objc_selector*, objc_object*) + 91
frame #2: 0x0000000105fd54e5 libobjc.A.dylib`lookUpImpOrForward + 464
frame #3: 0x0000000105fd529a libobjc.A.dylib`class_getInstanceMethod + 50
frame #4: 0x0000000106a48dfb CoreFoundation`__methodDescriptionForSelector + 299
frame #5: 0x0000000106a48ea6 CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
frame #6: 0x0000000106a2f1fa CoreFoundation`___forwarding___ + 378
frame #7: 0x0000000106a31418 CoreFoundation`__forwarding_prep_0___ + 120
看着打印是不是还是有点懵逼,没啥,的流程打印和图进行对照分析,就很清晰了,上图:(好尴尬,图不在这个电脑。。。后面补)
消息转发
动态方法解析失败后----->进入消息的转发:
运行时系统会在抛出错误前,给该对象发送一条forwardInvocation:消息,该消息的唯一参数是个 NSInvocation 类型的对象——该对象封装了 原始的消息和消息的参数。您可以实现 forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以以其它的某种 方式来避免错误被抛出。如 forwardInvocation:的名字所示,它通常用来将消息转发给其它的对象。
关于消息转发的作用,您可以考虑如下情景:假设,您需要设计一个能够响应 negotiate 消息的对象, 并且能够包括其它类型的对象对消息的响应。 通过在 negotiate 方法的实现中将 negotiate 消息 转发给其它的对象来很容易的达到这一目的。
更进一步,假设您希望您的对象和另外一个类的对象对negotiate的消息的响应完全一致。一种可能的 方式就是让您的类继承其它类的方法实现。 然后,有时候这种方式不可行,因为您的类和其它类可能需要
在不同的继承体系中响应 negotiate 消息。 虽然您的类无法继承其它类的negotiate方法,您仍然可以提供一个方法实现,这个方法实现只是简单
的将 negotiate 消息转发给其他类的对象,就好像从其它类那儿“借”来的现一样。(消息的转发,后面直接来个demo实例讲解下就能很好理解了,上面的官方语言只是让我们知道消息转发能干嘛)。
要转发消息给其它对象,forwardInvocation:方法所必须做的有:
- 决定将消息转发给谁,并且
- 将消息和原来的参数一块转发出去
消息可以通过 invokeWithTarget:方法来转发.具体的转发流程操作,这里不再讲解,后面通过demo说明。
以上就是通过方法的实质探查runtime的全部解析过程,这里大家再来看看思维导图对比一下,可能会清晰很多,两张图是两次总结Runtime画的,凑活看:
分析Runtime时,有一个很重要的东西,叫isa,关于isa,我会在后面文章中介绍。
最后,奉上demo下载地址(还没整理,后面补);
最后的最后,送大家一句名言共勉:
将来的你一定会感谢现在奋斗的自己。 ----李清照