- iOS类与方法底层探析(一)-源码探析
-
iOS类与方法底层探析(二)运行时类的内存结构
承接以上部分接着来,
消息机制
RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。
对于C语言,函数的调用在编译的时候会决定调用哪个函数( C语言的函数调用请看这里 )。编译完成之后直接顺序执行,无任何二义性。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
[PlatoJobs PJ_ClassMethod];
首先,编译器将代码[PlatoJobs PJ_ClassMethod]
;转化为objc_msgSend(PlatoJobs, @selector (PJ_ClassMethod))
;,在 objc_msgSend
函数中。首先通过PlatoJobs
的isa
指针找到PlatoJobs
对应的class
。在Class
中先去cache
中 通过SEL
查找对应函数method
(猜测cache
中method列表
是以SEL
为key
通过hash表
来存储的,这样能提高函数查找速度),若 cache
中未找到。再去methodList
中查找,若methodlist
中未找到,则取superClass
中查找。若能找到,则将method
加 入到cache
中,以方便下次查找,并通过method
中的函数指针跳转到对应的函数中去执行。
本文将详细解释:
#import <UIKit/UIKit.h>
@interface PlatoJobs : NSObject
+ (void) classMethod;
@end
@implementation PlatoJobs
+ (void) classMethod{
NSLog(@"class method");
}
@end
int main(int argc, char * argv[]){
@autoreleasepool {
[PlatoJobs classMethod];
return 0;
}
}
我们将上述代码转换成C++代码:
xcrun -sdk iphoneos clang -rewrite-objc -F UIKit -fobjc-arc -arch arm64 /Users/platojobs/Desktop/PL/PL/main.m
在生成的cpp
文件中,可以看到main
函数对应的c++
代码
通过如下代码可以知道我们在文初所开始的方法被转换成了objc_msgSend
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("PlatoJobs"), sel_registerName("classMethod"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_wm_jgydmfmx3vnbhg_9dglv581c0000gn_T_main_5349ff_mi_1);
}
return 0;
}
那么问题来了,objc_msgSend
它是怎么确定应该调用哪个方法的呢?它有一个动态查找过程,具体如下:
汇编代码中长成这牙子:
/*****************************************************************
*
* id objc_msgSend(id self, SEL _cmd,...);
*
*****************************************************************/
ENTRY objc_msgSend
MESSENGER_START
cbz r0, LNilReceiver_f // 判断消息接收者是否为nil
ldr r9, [r0] // r9 = self->isa
CacheLookup NORMAL // 到缓存中查找方法
LCacheMiss: // 方法未缓存
MESSENGER_END_SLOW
ldr r9, [r0, #ISA]
b __objc_msgSend_uncached
LNilReceiver: // 消息接收者为nil处理
mov r1, #0
mov r2, #0
mov r3, #0
FP_RETURN_ZERO
MESSENGER_END_NIL
bx lr
LMsgSendExit:
END_ENTRY objc_msgSend
其实主要就是:
判断接收者是否为nil,如果为nil,清空寄存器,消息发送返回nil
到类缓存中查找方法,如果存在直接返回方法
没有找到缓存,到类的方法列表中依次寻找
查找方法实现是通过_class_lookupMethodAndLoadCache3
这个奇怪的函数完成的:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP methodPC = nil;
Method meth;
bool triedResolver = NO;
methodListLock.assertUnlocked();
// 如果传入的cache为YES,到类缓存中查找方法缓存
if (cache) {
methodPC = _cache_getImp(cls, sel);
if (methodPC) return methodPC;
}
// 判断类是否已经被释放
if (cls == _class_getFreedObjectClass())
return (IMP) _freedHandler;
// 如果类未初始化,对其进行初始化。如果这个消息是initialize,那么直接进行类的初始化
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}
retry:
methodListLock.lock();
// 忽略在GC环境下的部分消息,比如retain、release等
if (ignoreSelector(sel)) {
methodPC = _cache_addIgnoredEntry(cls, sel);
goto done;
}
// 遍历缓存方法,如果找到,直接返回
methodPC = _cache_getImp(cls, sel);
if (methodPC) goto done;
// 遍历类自身的方法列表查找方法实现
meth = _class_getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, cls, meth, sel);
methodPC = method_getImplementation(meth);
goto done;
}
// 尝试向上遍历父类的方法列表查找实现
curClass = cls;
while ((curClass = curClass->superclass)) {
// Superclass cache.
meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
if (meth) {
if (meth != (Method)1) {
log_and_fill_cache(cls, curClass, meth, sel);
methodPC = method_getImplementation(meth);
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;
}
}
// 查找父类的方法列表
meth = _class_getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, curClass, meth, sel);
methodPC = method_getImplementation(meth);
goto done;
}
}
// 没有找到任何的方法实现,进入消息转发第一阶段“动态方法解析”
// 调用+ (BOOL)resolveInstanceMethod: (SEL)selector
// 征询接收者所属的类是否能够动态的添加这个未实现的方法来解决问题
if (resolver && !triedResolver) {
methodListLock.unlock();
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
// 仍然没有找到方法实现进入消息转发第二阶段“备援接收者”
// 先后会调用 -(id)forwardingTargetForSelector: (SEL)selector
// 以及 - (void)forwardInvocation: (NSInvocation*)invocation 进行最后的补救
// 如果补救未成功抛出消息发送错误异常
_cache_addForwardEntry(cls, sel);
methodPC = _objc_msgForward_impcache;
done:
methodListLock.unlock();
assert(!(ignoreSelector(sel) && methodPC != (IMP)&_objc_ignored_method));
return methodPC;
}
上面就是一个方法调用的全部过程。主要分为三个部分:
查找是否存在对应的方法缓存,如果存在直接返回调用
为了优化性能,方法的缓存使用了散列表的方式,在下一部分会进行比较详细的讲述
未找到缓存,到类本身或顺着类结构向上查找方法实现,返回的
method_t *
类型也被命名为Method
//非加锁状态下查找方法实现
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
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);
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;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
如果在这个步骤中找到了方法的实现,那么将它加入到方法缓存中以便下次调用能快速找到:
// 记录并且缓存方法
static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill (cls, sel, imp, receiver);
}
//在无加锁状态下缓存方法
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
if (!cls->isInitialized()) return;
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// 如果缓存占用不到3/4,进行缓存。
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied expand();
}
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
如果在类自身中没有找到方法实现,那么循环获取父类,重复上面的查找动作,找到后再将方法缓存到本类而非父类的缓存中
未找到任何方法实现,触发消息转发机制进行最后补救
其中消息转发分为两个阶段,第一个阶段我们可以通过动态添加方法之后让编译器再次执行查找方法实现的过程;第二个阶段称作备援的接收者,就是找到一个接盘侠来处理这个事件,
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
// 非beta类的情况下直接调用 resolveInstanceMethod 方法
if (! cls->isMetaClass()) {
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// 先调用 resolveClassMethod 请求动态添加方法
// 然后进行一次查找判断是否处理完成
// 如果没有添加,再调用 resolveInstanceMethod 方法
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
在最新的runtime源码的objc-runtime-new.h中,objc_class的结构如下(笔者已经略去了大部分的函数):
struct objc_class : objc_object {
Class superclass; // Class ISA;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
// .........
}
很明显cache存储着我们在方法调用中需要查找的方法缓存。作为缓存方法的cache采用了散列表,以此来大幅度提高检索的速度:
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
// functions
}
// cache method
buckets = (cache_entry **)cache->buckets;
for (index = CACHE_HASH(sel, cache->mask);
buckets[index] != NULL;
index = (index+1) & cache->mask)
{ }
buckets[index] = entry;
在每次调用完未被缓存的方法时,下面的那段缓存方法的代码就会调用。苹果利用了sel的指针地址和mask做了一个简单的位运算,然后找到一个空槽存储起来。 以此我们可以推出从缓存中查找sel实现的代码CacheLookup,但是为了高度优化性能,苹果同样丧心病狂的使用汇编完成了查找的步骤,官方给出的注释足够我们大致看明白这段代码:
.macro CacheLookup
ldrh r12, [r9, #CACHE_MASK] // r12 = mask
ldr r9, [r9, #CACHE] // r9 = buckets
.if $0 == STRET || $0 == SUPER_STRET
and r12, r12, r2 // r12 = index = SEL & mask
.else
and r12, r12, r1 // r12 = index = SEL & mask
.endif
add r9, r9, r12, LSL #3 // r9 = bucket = buckets+index*8
ldr r12, [r9] // r12 = bucket->sel
2:
.if $0 == STRET || $0 == SUPER_STRET
teq r12, r2
.else
teq r12, r1
.endif
bne 1f
CacheHit $0
1:
cmp r12, #1
blo LCacheMiss_f // if (bucket->sel == 0) cache miss
it eq // if (bucket->sel == 1) cache wrap
ldreq r9, [r9, #4] // bucket->imp is before first bucket
ldr r12, [r9, #8]! // r12 = (++bucket)->sel
b 2b
.endmacro
在OC中方法被抽象成的数据类型是Method,如果了解并且使用过runtime的读者们可能了解这个类型,其结构如下:
struct old_method {
SEL method_name;
char *method_types;
IMP method_imp;
};
typedef struct method_t *Method;
method_imp
方法的实现代码,你可以把它看做一个block
。事实上,后者确实可以转换成一个IMP类型来实现某些黑魔法。method_types
方法的参数编码,什么意思?在属性与变量中我说过每一种数据类型有着自己对应的字符编码,这个表示方法返回值、参数的字符编码,比如-(void)playWith:(id)
的字符编码为v@:@
method_name
顾名思义,方法的名字。通常我们使用@selector()
的方式获取一个方法的sel地址,这个被用来进行散列计算存储方法的imp
实现。由于SEL
类型采用了散列的算法,因此如果同一个类中存在同样名字的方法,那么就会导致方法的imp
地址无法唯一化。这也是苹果不允许同名不同参数类型的方法存在的原因.
以上的过程就是:
具体调用过程如下:
- 在相应对象的缓存方法列表中(
objc-class
的cache
)查找调用的方法。 - 如果没找到,则在相应对象的方法列表中找调用方法。
- 如果还没找到,就在父类指针指向的对象中执行1和2两步。
- 如果直到根类都没有找到,就进行消息转发,给自己保留处理找不到方法这一状况的机会。
- 调用
resolveInstanceMethod
和resolveClassMethod
,有机会让类添加这个函数的实现。 - 调用
forwardingTargetForSelector
,让其他对象执行这个函数。 - 调用
forwardInvocation
更加灵活的处理函数调用。 - 如果以上都没有找到,也没有进行特殊处理,就抛出
doesNotRecognizeSelector
异常.