说到Objective-C,我们都知道它是一个扩充C的面向对象编程的动态语言,而其中的动态核心就是
Runtime
。
Runtime
Runtime
简单来说就是一套利用汇编语言
和C语言
编写成的代码库。
Objective-C runtime 有两个版本
modern
和legacy
。
modern
版本是在Objective-C 2.0中引入的,其中包括许多新功能。Objective-C Runtime Reference中描述了modern
版本的运行时的编程接口。
legacy
版本的编程接口在Objective-C 1 Runtime Reference
中进行了描述。
Runtime Api
Objective-C Runtime Api
Runtime 作用
- 消息发送
objc_msgSend(objc, @selector(methodName));
- 方法交换
Method oldMethod = class_getClassMethod(self, @selector(methodName:));
Method newMethod = class_getClassMethod(self, @selector(newMethodName:));
method_exchangeImplementations(oldMethod, newMethod);
- 动态添加属性
@implementation Person (Property)
- (void)setHobby:(NSString *)hobby {
// @param object: 保存于哪个对象中
// @param key:属性名称
// @param value:数据值
// @param policy:策略(strong,weak)
// void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
objc_setAssociatedObject(self, "hobby", hobby, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)hobby {
// @param object: 保存于哪个对象中
// @param key:属性名称
return objc_getAssociatedObject(self, "hobby");
}
- 动态添加方法
// Class cls:给哪个类添加方法
// SEL name:添加方法的方法编号
// IMP imp:添加方法的函数实现
// const char *types:函数的类型
// BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
class_addMethod(self, @selector(methodName), (IMP)methodName, "v@:");
- 数据模型转换 -
MJExtension
...
之所以能够实现这些功能,都是根据
Runtime
的机制和其提供的Api
。
方法的本质
前面说了这么多,其实就是为了了解方法的本质,在Objctive-C中,方法是怎么去实现的?
#import
@interface Person : NSObject
- (void)sayHello;
@end
@implementation Person
- (void)sayHello{
NSLog(@"%@", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
[person sayHello];
}
return 0;
}
clang
通过clang -rewrite-objc main.m -o main.cpp
,我们可查看编译后,运行前源码转换成了C语言
代码。
在main函数中,我们简化类型转换,可以发现都是通过
objc_msgSend
进行消息发送。
Person *person = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));
因此可以理解为Objctive-C方法的本质其实就是objc_msgSend
消息发送,并且默认带有id(消息接收者)
和sel(方法编号)
。
发送消息
-
objc_msgSend
将带有简单返回值的消息发送到类的实例。 -
objc_msgSendSuper
将具有简单返回值的消息发送到类实例的超类。 -
objc_msgSend_stret
将具有数据结构返回值的消息发送到类的实例。 -
objc_msgSendSuper_stret
将具有数据结构返回值的消息发送到类实例的超类。
/// Specifies the superclass of an instance.
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 */
};
#endif
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
[p sayHello];
// 方法调用底层编译
// 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)
objc_msgSend(p, sel_registerName("sayHello"));
// 类方法调用底层编译
objc_msgSend(objc_getClass("Person"), sel_registerName("sayHi"));
// 向父类发消息(对象方法)
struct objc_super pSuper;
pSuper.receiver = p;
pSuper.super_class = [Person class];
objc_msgSendSuper(&pSuper, @selector(sayHello));
//向父类发消息(类方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [p class];
myClassSuper.super_class = class_getSuperclass(object_getClass([p class]));
objc_msgSendSuper(&myClassSuper, @selector(sayHi));
}
return 0;
}
Tips:
使用objc_msgSend
函数要把Enable Strict Checking of objc_msgSend Calls
校验设置为NO
, 否则编译会报错了。
objc_msgSend汇编
objc_msgSend
的实现为何采用汇编代码?
- 性能:汇编语言更能容易被机器识别,无需在进行机器语言转换。
- 动态性:C语言无法通过写一个函数来保留未知的参数并跳转到一个任意函数指针。
在objc4源码
中,全局查询了objc_msgSend
,此处以arm64为主。
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
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]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
整体的流程:
1、对消息接收者(id self, sel _cmd)进行判断处理。
2、taggedPointer
判断处理。
3、 GetClassFromIsa_p16 p13
,获取相应的Class
。
4、CacheLookup NORMAL, _objc_msgSend
进行imp
查找
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
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
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
.endmacro
结合汇编源码的注释,CacheLookup
是一个方法查找的流程:
1、获取到类的cache_t
,并将拆分出对应的buckets
和mask
。
2、通过循环遍历buckets
,然后if (bucket->sel != _cmd)
判断在缓存中是否存储有对应的imp
。
3、存在相应的imp
,则来到CacheHit
,缓存命中,返回对应的imp
。
4、不存在相应的imp
,则继续遍历。
5、遍历结束,找不到相应的imp
,会再一次重试(考虑到并发
)。
6、最后还是找不到对应到方法时,来到JumpMiss
。
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.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
在CheckMiss
中,因为是NORMAL
的流程,所以会执行__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
// 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)]
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
// 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
简单的方法跳转到MethodTableLookup
中。
通过注释,前面进行参数的准备,然后调用了_lookUpImpOrForward
。
lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
在objc4源码的汇编中,我们已经找不到相应的实现,而在objc-runtime-new.mm
文件中发现了C函数的实现。
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* Without LOOKUP_CACHE: skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use LOOKUP_INITIALIZE and LOOKUP_CACHE
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use LOOKUP_NIL.
**********************************************************************/
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
//
// TODO: this check is quite costly during process startup.
checkIsKnownClass(cls);
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again
// 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
}
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookpu the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
if (slowpath((curClass = curClass->superclass) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
这段代码的流程:
-
if (fastpath(behavior & LOOKUP_CACHE))
还是回到汇编查询cache。 - 循环遍历,查找是否有对应的方法
- 实例方法:循环遍历当前类,父类,根类,从这些类中的
Method List
中查询是否存在对应imp
。 - 类方法:循环遍历当前元类,父元类,根元类,最后到
NSObject
,从中的Method List
中查询是否存在对应imp
。
- 实例方法:循环遍历当前类,父类,根类,从这些类中的
- 查询到相应的
imp
,对方法进行缓存log_and_fill_cache
,然后返回相应的imp
。 - 未查询到,则
imp = _objc_msgForward_impcache
。 - 进行一次方法解析尝试
resolveMethod_locked
,再调用lookUpImpOrForward
重试。 - 最后如果还是没找到,就返回
imp = _objc_msgForward_impcache
。
此处
log_and_fill_cache
只是简单的判断是否支持消息日志记录和调用cache_fill
,
而cache_fill
的分析,在类的内容中有做简单解释。
/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled.
* implementer is the class that owns the implementation in question.
**********************************************************************/
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill(cls, sel, imp, receiver);
}
方法Crash
通常情况下,我们如果调用一个未实现的方法,系统会有一的
Carsh
表现。
-[Person saySomething]: unrecognized selector sent to instance 0x100683700
在查找方法的最后,有个赋值imp = _objc_msgForward_impcache
。
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel) {
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
在方法的实现中,我们找到了Carsh
输出。
最后
objc方法的调用,其实就是通过objc_msgSend
消息的发送。
- 先通过
objc_msgSend
汇编代码,在类的cache_t
中快速查找。 - 在
cache_t
找到,直接返回对应的imp
。 - 如果在类的
cache_t
中查询不到,则跳转至C函数lookUpImpOrForward
进行慢速查找。- 实例方法:通过层层递归,
类
-父类
-NSObject
-nil
- 类方法:通过层层递归,
元类
-父元类
-根元类
-NSObject
-nil
- 实例方法:通过层层递归,
- 找到,直接返回对应的
imp
。 - 未找到,进行一次动态解析
resolveMethod_locked
,在重复一次慢速查找。 - 最后如果如果没有处理动态解析,则
Crash
。