Runtime是C,C++汇编一起写成的API,有两个版本Modern和Legacy,OC2.0之后用的是Modern Version版本,可以运行在iOS2.0和macOS 10.5之后的系统中。
都说Objective-C是一门动态运行时语言,那什么是运行时?什么是编译时?
运行时:代码跑起来之后会装载到内存中,提供运行时功能
编译时:正在编译的时间,就是把源代码(高级语言)翻译成机器能识别的语言->机器语言->二进制
Runtime可以做的事情有很多
- 黑魔法:动态交换方法、KVO实现等
- 关联对象:给分类添加属性等
- 消息转发:项目中一些防崩溃处理等
它可以做的事情有很多,在这里就探索Runtime的底层是如何实现的,也便深入理解Objective-C这门语言。
Runtime的消息传递过程
在OC调用方法时,我们clang一下,看看对应的C++实现是什么
clang -rewrite-objc main.m -o main.cpp
Person *p = [Person new];
[p sayHello];
//C++实现
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));
可以看出,在调用方法时,编译器将它转成了objc_msgSend
消息发送了,在Runtime的执行过程如下
- 1、Runtime先通过对象
p
找到isa
指针,判断isa指针是否为nil
,为nil
直接return。 - 2、若不为空则通过
isa
指针找到当前实例的类对象,在类对象下查找缓存是否有sayHello
方法。 - 3、若找到
sayHello
方法,则直接调用IMP
方法。 - 4、若没找到,则查找当前类对象的方法列表
methodlist
,若找到方法则将其添加到缓存中 - 5、若没找到,则继续到当前类的父类中以相同的方式查找,直到追溯到最上层
NSObject
。 - 6、若还是没有找到,则启用动态方法解析、备用接收者、消息转发三部曲,给程序最后一个机会
- 7、若还是没找到,则Runtime会抛出异常
doesNotRecognizeSelector
。
以上就是Runtime的消息传递过程,我们从上往下慢慢分析。
isa指针
首先是isa指针,什么是isa指针?
打开Runtime源码,全局搜索isa_t
,锁定objc-private.h
类,里面有这么一段代码,isa指针是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
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
};
#endif
};
OC对象的本质,每个OC对象都含有一个isa指针,__arm64__
之前,isa
仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__
架构之后,apple对isa
进行了优化,变成了一个联合体union
结构,同时使用位域来存储更多的信息。
它是通过isa
的bits
进行位运算,取出响应位置的值,runtime中的isa
是被联合体位域优化过的,它不单单是指向类对象了,而是把64位中的每一位都运用了起来,其中的shiftcls
为33位,代表了类对象的地址,其他的位都有各自的用处。
-
nonpointer
:表示是否对isa指针开启指针优化
0:不开启,表示纯isa指针。 1开启,不单单是类对象的地址,isa中包含了类信息和对象的引用计数等。 -
has_assoc
:关联对象标识位,0没有,1有,没有关联对象会释放的更快 -
has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象 -
shiftcls
:存储类指针class
的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤用来存储类指针。 -
magic
:固定值为0xd2,用于在调试时分辨对象是否完成初始化 -
weakly_referenced
:表示对象是否被指向或者曾经指向一个 ARC 的弱引用变量,
没有弱引⽤用的对象可以更更快释放。 deallocating:标志对象是否正在释放内存 -
has_sidetable_rc
:当对象的引用计数大于10,以至于无法存储在isa指针中时,用散列表去计数 -
extra_rc
:表示该对象的引用计数,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数⼤于 10, 则需要使⽤到has_sidetable_rc
objc_msgSend的方法调用本质
好了现在清楚了isa指针是怎么一回事儿了,接下来探索方法调用的本质是什么。
打开Runtime源码,全局搜索objc_msgSend(
,找到objc-msg-arm64.s
这个类
objc_msgSend的方法调用分为两部分,汇编和C语言。
一、汇编部分
1、ENTRY _objc_msgSend
2、进入LNilOrTagged
,当数据类型是NSTaggedPointer
或消息名为空时,直接END_ENTRY _objc_msgSend
,不为空则继续执行下一步,NSTaggedPointer
类型的对象采用和isa
一样的联合体位域的方式,可直接从地址中读取出想要的值,一般当数据类型的"value"足够小时,系统会自动转换为NSTaggedPointer
类型,比如NSString
转换为NSTaggedPointerString
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
3、LGetIsaDone
处理完isa
,这一步就是isa指针已经处理完,要么从缓存中找到IMP
,要么执行objc_msgSend_uncached
从方法列表中获取IMP
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
4、CacheLookup NORMAL
去缓存中找IMP
,会有以下2种情况
1)CacheHit
在缓存中找到了则直接calls imp
2)CheckMiss
没找到则执行objc_msgSend_uncached
CacheLookup执行如下,主要看关键字就行。
//代码剔除了混淆理解的部分
.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
.endmacro
CacheHit执行如下
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
AuthAndResignAsIMP x0, x12 // authenticate imp and re-sign as IMP
ret // return IMP
.elseif $0 == LOOKUP
AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
ret // return imp via x17
.endmacro
CheckMiss执行如下
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.endmacro
5、objc_msgSend_uncached
,缓存中没找到则去MethodTableLookup
到方法列表中寻找
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
6、MethodTableLookup
中会到objc-runtime-new.mm
中调用_class_lookupMethodAndLoadCache3
方法,从这里开始便从汇编到C语言执行了
.macro MethodTableLookup
bl __class_lookupMethodAndLoadCache3
.endmacro
二、C语言部分
1、执行_class_lookupMethodAndLoadCache3方法,该方法调用了lookUpImpOrForward函数
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
2、lookUpImpOrForward 查找imp,主要分为以下几步
- 1)
getMethodNoSuper_nolock(cls, sel);
到method_list
中找,如果找到了method
,则把它存入缓存;如果没找到,则向上遍历父类,步骤也是执行1)、2)步,直到查到根部(root)NSObject。
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) {
//循环遍历method列表,查询方法
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;
}
- 2)log_and_fill_cache存入缓存
static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
cache_fill (cls, sel, imp, receiver);
}
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
}
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
//获取类的缓存
cache_t *cache = getCache(cls);
//这里可以知道cache是以方法名作为key
cache_key_t key = getKey(sel);
// Use the cache as-is if it is less than 3/4 full
//在当前缓存大小大于缓存吃容量的3/4时,扩大缓存,在低于3/4时可以正常存储
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {//满了就扩大缓存容量
cache->expand();
}
把方法的IMP存储到cache中
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
如果上面的流程走完没有找到IMP,那么还有接下来三种补救方法,动态方法解析、备用接收者、消息转发
三、消息转发
Part1、动态方法解析
调用_class_resolveMethod
方法,判断当前类是否为元类,
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
//不是元类的话会调用`_class_resolveInstanceMethod`,如果这个方法能找到`imp`的话,会再次`objc_msgSend`发送消息
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);
}
}
}
- 1)不是元类的话会调用
_class_resolveInstanceMethod
,如果这个方法能找到imp
的话,会再次objc_msgSend
发送消息。 - 2)是元类的话分两步,先调用
_class_resolveClassMethod
、再调用_class_resolveInstanceMethod
。
注意:为什么元类要调用先调用_class_resolveClassMethod
,再调用_class_resolveInstanceMethod
呢,因为类方法在元类中是以实例的形式存在的,所以查找步骤如下Person(类方法)->元类(实例方法)->根元类(实例方法)->NSObject(实例方法)
实现resolveInstanceMethod
和resolveClassMethod
如下
//调用对象的实例方法时,先执行resolveInstanceMethod方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(run)) {
if (sel == @selector(run)) {
SEL readBookSel = @selector(readBook);
Method readM = class_getInstanceMethod(self, readBookSel);
IMP readImp = class_getMethodImplementation(self, readBookSel);
const char *type = method_getTypeEncoding(readM);
return class_addMethod(self, sel, readImp, type);
}
}
return [super resolveInstanceMethod:sel];
}
//调用类方法时,先执行resolveClassMethod方法
+ (BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(walk)) {
SEL readBookSel = @selector(readBook);
Method readM = class_getInstanceMethod(self, readBookSel);
IMP readImp = class_getMethodImplementation(self, readBookSel);
const char *type = method_getTypeEncoding(readM);
return class_addMethod(object_getClass(self), sel, readImp, type);
}
return [super resolveClassMethod:sel];
}
Part2、备用接收者
如果动态方法解析依然没有结果,就会进行寻找备用接收者,(id)forwardingTargetForSelector:(SEL)aSelector;
这个forwarding方法在runtime底层是通过汇编调用的,苹果并没有给出源码实现,但是我们可以通过打印runtime的方法调用信息(log),来查看runtime接下来调用了哪些方法.方式如下:
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
instrumentObjcMessageSends(YES);
[LGPerson walk];
instrumentObjcMessageSends(NO);
}
return 0;
}
这样在finder中找到/private/tmp/文件夹,找到最新的msgSends-xxxx文件,这个文件就是LGPerson调用walk方法的时候所执行的方法信息,如图所示
显而易见的,LGPerson在动态方法解析后,继续调用了NSObject的动态方法解析,在都没有imp的情况下调用了forwardingTargetForSeletor(备用接收者)方法,当备用接受者也没有实现时,会调用最后一步methodSignatureForSelector(消息转发)
举例如下:
//寻找备用接收者,让别的类来提供同名的方法
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(walk)) {
return [LGStudent new];
}
return [super forwardingTargetForSelector:aSelector];
}
在forwardingTargetForSelector方法中我们可以坐哪些操作?
1.找备用接收者处理当前类没有实现的方法(注意:备用接收者的方法只能是实例方法)
2.crash收集
3.防止崩溃
Part3、消息转发
如果备用接收者没有找到IMP方法,则执行消息转发,主要涉及以下两个方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
在动态方法解析和备用接收者都无法找到IMP时,系统会调用methodSignatureForSelector
方法,如果方法返回nil,则系统直接调用doesNotRecognizeSelector
方法使程序崩溃并打印崩溃信息;如果返回一个NSMethodSignature
类型的函数签名,则系统会创建一个NSInvocation
对象并调用forwardInvocation
方法,我们可以在方法中指定别的方法或其他对象去执行这个的方法。
例子如下
//若返回函数签名,则继续调用forwardInvocation方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(run)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(run)) {
//指定当前类的一个方法作为IMP
// anInvocation.selector = @selector(readBook);
// [anInvocation invoke];
//指定其他类来执行这个IMP
LGStudent *student = [LGStudent new];
[anInvocation invokeWithTarget:student];
}
}
以上就是Runtime消息传递的全部过程。
Runtime博大精深,未完待续~