有没有内推,最近想换个坑位。
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
的处理。