有没有内推,最近想换个坑位。
0x00 - 运行时简介
Runtime
运行时,代码跑起来,已经加载到内存中, 只有加入到内存中, 代码才被激活,才跑起来
Buildtime
代码在未加载到内存中之前是死家伙,就存在磁盘上
Objective-C Runtime Programming Guide 官方文档
一套API, c/c++/汇编,为oc提供运行时的功能;
-
早期版本对应的编程接口:1.0
-
现行版本对应的编程接口:2.0
早期版本用于Objective-C 1.0 , 32位的Mac OS X的平台上
现行 版本: iPhone 程序和Mac OS X v10.5及以后的系统中64位程序
@selector() == sel_registerName == NSSelectorFromString() 不同层面调用方式不一样,结果一样
使用runtime的三种方式
-
通过
Objective-C代码调用[obj someThing] -
通过
NSObject接口调用NSSelectorFromString() isKindOfClass()等 -
通过
Runtime APIs函数调用sel_reisterName, class_getInstanceSize等函数
0x01 - 方法的本质探索
发送消息
objc_msgSend(id , sel)
id 消息的接受者 sel 方法的编号
objc_superMsgSend 向父类发送消息
在手撕iOS底层08 -- isa走位详解中,通过clang编译,clang -rewrite-objc main.m -o main.cpp
通过打开main.cpp分析,理解了OC对象的本质,同样的在这个文件中,
// main函数中oc方法的调用
Person *p = [Person alloc];
[p say1];
[p say2];
// clang 编译后底层实现
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("say1"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("say2"));
通过这段代码对比,得到方法的本质就是objc_msgSend消息发送, 可以在main函数中直接使用objc_msgSend来调用say1这个函数。
Person *p = [Person alloc];
objc_msgSend(p, @selector(say1));
[p say1];
// -[Person say1:]
// -[Person say1:]
⚠️ 这里需要设置 Build Settings -> Enable Strict Checking of objc_msgSend Calls -> NO 关闭这个静态检测函数参数问题
最终输出的结果一样的。
objc_msgSendSuper 让子类对象调用父类的方法
@interface Person : NSObject
-(void)sayBye;
@end
@implementation Person
- (void)sayBye {
NSLog(@"bye bye");
}
@end
@interface Teacher : Person
@end
@implementation Teacher
@end
// 在 main 中执行
Person *p = [Person alloc];
Teacher *t = [Teacher alloc];
[t say2];
struct objc_super mySuper;
mySuper.receiver = t;
mySuper.super_class = [Person class];
objc_msgSendSuper(&mySuper, @selector(say2));
给父类发送消息需要传递一个struct 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 */
};
结果都是输出
LGPerson say : -[Person say2]
LGPerson say : -[Person say2]
通过输出结果,发现[t say2] 和objc_msgSendSuper都是执行父类的say2方法实现。所以这里暂时得到一个结论方法调用,先在自己所属的类中查找,如果自己的类没有,会走继承链查找
0x02 - 简要分析objc_msgSend流程
通过在objc-781源码中搜索 objc_msgSend,由于我们的目标设备是arm64架构,所以我们打开objc-msg-arm64.s汇编文件来查看。
这个汇编的整体流程是:
- 从消息接受这查找方法, 先是通过对象isa找到类
- 通过类去
cache_t的缓存查找方法 - 缓存没有,去
bits的methodList查找方法
objc_msgSend本身使用汇编语言写的,为什么使用汇编?原因主要有俩个:
-
OC语言中调用方法,都是通过objc_msgSend,可以说这个方法是OC方法必经之路,所以在这个方法上面进行性能优化能够提升整个App生命周期的性能, 而汇编语言在性能上优化是属于原子级优化,能够做到极致。 -
其它语言难以实现
未知参数跳转任意函数指针的功能 -
快 效率高 + 动态性(不确定性) 参数不确定
id objc_msgSend(id self, SEL _cmd, ...);
现获取对象对应类的信息,再获取方法的缓存,根据
selector查找函数和指针,经过异常处理后,最后跳到对应的函数实现
0x03 - arm64 objc_msgSend汇编分析
ENTRY _objc_msgSend //入口
UNWIND _objc_msgSend, NoFrame // 没有窗口界面
cmp p0, #0 //nil check and tagged pointer check 检查第一个参数,即self是否为0
#if SUPPORT_TAGGED_POINTERS // 如果支持SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative) 如果小于等于0 就跳转到LNilOrTagged处理
#else // 不支持 SUPPORT_TAGGED_POINTERS
b.eq LReturnZero // 是否等于0 等于0就跳转到 LReturnZero 进行nil处理
#endif
ldr p13, [x0] // p13 = isa //
GetClassFromIsa_p16 p13 // p16 = class
ldr是Load Register 的缩写,[]是间接寻址, 表示从x0所表示的地址中取出8字节数据,放到x13中,x0是self的地址,所以这里拿到的是isa的地址, 这里解释一下
因为self是指针, 实际上是指向struct objc_object, 定义是
struct objc_object {
private:
isa_t isa;
....
}
objc_object中只有一个成员isa,因此取出指针指向的内容,也就是取到了isa的值。
调用GetClassFromIsa_p16 进一步获取class的地址,这里是重点, 因为后续都要使用这个class,
获取class
GetClassFromIsa_p16 的实现
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA // 这部分主要是watchOS上支持
// Indexed isa
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__ //arm64
// 64-bit packed isa
and p16, $0, #ISA_MASK //
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
and p16, $0, #ISA_MASK
and表示与运算, p16=$0 & ISA_MASK, $0是第一个传进来的参数, 也就是isa的值,ISA_MASK 0x0000000ffffffff8ULL
这个和objc-781源码
inline Class
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK); // 这里也是用与的算法来获取class
#endif
}
到这里我们就获取到了class的地址,就可以跳到LGetIsaDone:,进行缓存的查找CacheLookup
由于对象的实例方法存储在所属的类中,那么它的方法缓存也在类里面,简单回顾下上一篇文章的内容
struct objc_class : objc_object {
// Class ISA; // 8
Class superclass; // 8
cache_t cache; // 16 // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 属性 方法 协议 //8
}
可以看我上一篇文章了解cache_t里的缓存知识,现在的寄存器状态是 x1 = SEL, x16 = isa
查找过程
CacheLookup NORMAL|GETIMP|LOOKUP <function>
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
CACHE定义如下,2个指针大小,也就是16字节
#define CACHE (2 * __SIZEOF_POINTER__)
这段是说,从x16寄存器中偏移CACHE个位置取出8个字节大小的数据,放入到p11中,
为什么偏移16字节, 看objc_class的定义
struct objc_class : objc_object {
// Class ISA; //8字节
Class superclass; // 8字节
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
}
cache所在的位置在ISA和superclass后边,所以要偏移16字节,再取出8字节的内容,就得到了_maskAndBuckets值,
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
之前的文章提到过_maskAndBuckets buckets存在低48位,
p10=p11&0x0000ffffffffffff 得到p10=buckets
LSR表示逻辑右移, p11右移48位, 得到maks放到p11, p1里放的是sel,通过sel&mask得出sel在哈希表中的index,
// 得到mask值
p11 = p11 >> 48
// 求出index
p12 = p1 & p11
接着获取索引项的对应地址
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
LSL逻辑左移, PTRSHIFT=3, 就是p12左移4位, 相当于乘以16, 因为每一个bucket大小是16字节, 所以index也就是索引是多少, 偏移多少大小,p10保存的是buckets缓存表的地址,加上偏移量,就得到索引项的地址,存入p12中。
ldp p17, p9, [x12] // {imp, sel} = *bucket
通过ldr指令 从x12中的地址中取出2个8字节的数据,放到p17和p9中, p17是imp数据, p9是sel,从bucket_t的数据结构中得知,缓存表中的每一项都是{imp, sel}
这里开始俩次扫描缓存表,如示意图
1: cmp p9, p1 // if (bucket->sel != _cmd) 这里是比较参数的sel(_cmd)和缓存的sel
b.ne 2f // scan more 不想等, 跳转到2执行
CacheHit $0 // call or return imp 命中缓存 , 调用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
通过CheckMiss $0检查当前index的bucket是否为空,
.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
.else
.abort oops
.endif
.endmacro
在ChickMiss中通过p9判断是否为0, p9表示当前sel,如果为空,就执行__objc_msgSend_uncached,会进行c方法的慢速查找流程。
不为空就会走后边的流程
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
p10是当前缓存表的地址,p12是当前sel的bucket,意思判断当前的p12是否是当前的表头,如不是就不断的循环比较
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
ldp后边的指令跟了一个!,表示将p12减去一个BUCKET_SIZE大小,写回p12中,再取出IMP和SEL分别给p17和p9中。
如果是表头,会走3处理
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
就是将p12指向表尾,然后再从后向前遍历方法缓存,p11是当前缓存的地址,因为mask再其中占高16位,所以右移48位,得到mask,因为每一项bucket大小是16字节,所以要mask << 4得到总共占用的大小,也就是总的偏移量,再用p12首地址去加上总的偏移量,所以p12就指向表尾了
// 分步表示
mask = p11 >> 48
offset = mask << 4
然后开始[第二次]遍历方法缓存,从新走 第一步和第二步的流程,第三步会跳出来
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
整体代码参考
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
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
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
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
未找到方法最后都会跳到__objc_msgSend_uncached
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
.macro MethodTableLookup
// 保存寄存器 start
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// 保存寄存器 end
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward //调用lookUpImpOrForward查找
// IMP in x0
mov x17, x0 //由于返回结果是放在 x0 中,之前缓存查找结果的 imp 是放在 x17 中,这里保持一致
// 恢复寄存器
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
.macro TailCallFunctionPointer
// $0 = function pointer value
br $0 //只是调用br指令 跳转传入的IMP
.endmacro
到这里就结束了, 后续再补充Tagged pointer和nil的处理。