Objective-C 是一门动态语言,它将很多静态语言在编译和链接时期做的事情,推迟到了运行时来处理。当我们调用一个方法时会通过运行时(Runtime)来绑定、分发。它的运行时(Runtime)实现了对类、方法、成员变量、属性等信息的动态管理,给我们在编码时极大的灵活性。结合出现的问题,分享下运行时中的方法的调用、拦截过程。
本篇中用的到Runtime函数发送消息 objc_msgSend(receiver, selector, arg1, arg2, ...)
获取类对象 object_getClass(id _Nullable obj)
获取父类 class_getSuperclass(Class _Nullable cls)
获取方法IMPclass_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name)
类的定义
struct objc_class : objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
class_rw_t *data() const {return bits.data();}
}
//class_rw_t
struct class_rw_t {
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->methods;
} else {
return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
}
}
//本篇只讨论method省略其他.....
}
消息发送(方法调用)
OC中方法的调用实际会转化为objc_msgsend消息进行发送如:
@interface TestClass: NSObject
//实例方法
- (void)printInstanceInfo;
//类方法
+ (void)printInfo;
@end
@implementation TestClass
- (void)printInstanceInfo {
NSLog(@"%s",__func__);
}
+ (void)printInfo {
NSLog(@"%s",__func__);
}
@end
//调用
TestClass *test = [[TestClass alloc] init];
[test printInstanceInfo];
[TestClass printInfo];
上面的方法调用实际会转换为:
```objectivec
TestClass *test = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("printInstanceInfo"));
当转换为此结构后才会进入消息的查找发送流程。
方法查询流程当调用到objc_msgSend(receiver, selector, arg1, arg2, ...)
函数时,系统会首先判断接受者receiver是否为nil,如果nil则直接结束消息发送的流程。实际中当接受者或者对象为nil时,调用方法时应用程序不会出现崩溃。
当receiver
不为nil时,则会根据实例对象的isa
指针找到类对象,在类对象的方法缓存cache
中查找。如果查询到则直接调用,如果未查找到则会在类方法列表methods
中查找。如果在该类对象的方法中查找到则会存储到cache中,然后再进行调用。
如果在当前类中未查询到,则会通过当前类的superclass
指针找到类对象的父类,在类对象父类中查找。当在父类中查找到后,后首先在父类的cache
中查找,如果找到则缓存在当前类的cache
中,然后再调用。
如果未查询到继续在父类的methods中查找,如果找到了,则会把该方法缓存到当前类的cache
中,然后再调用此方法。
如果在父类中没有找到则继续根据superclass
向上查找,直到nil
如果到了nil依旧没有找到方法,则会触发消息转发。
注意:实例方法与类方法类似,只不过实例方法在类对象中查找,类方法在元类对象中查找。实际的查找过程经过了苹果的优化,比如方法存放是散列表中,查询通过二分法进行,缓存则根据调用的频率进行排序等等。
专有词说明
通过上面的流程我们需要了解几个名词:实例对象(instance):开发者创建,其中的isa
指针指向类对象类对象(class):存储实例对象的方法、属性、协议等内容,superclass
指向父类并最终指向nil,isa
指向元类并最终指向NSObject
, NSObject
的isa
指针指向自身。元类对象(meta):存储类对象内容,其superclass指向父元类,最终指向nil。
通过张关系图可以更明确三者之间的关系:
消息转发(方法拦截)
如果在各类中未找到方法(SEL), 提供会给开发者提供三次挽救的机会,如果最终没有补救程序会崩溃。
第一次回调
+ (BOOL)resolveInstanceMethod:(SEL)sel;//(实例方法)
+ (BOOL)resolveClassMethod:(SEL)sel;//(类方法)
此时系统会根据方法类型回调到实例方法或类方法中。我们可以此时进行重新板顶 并返回YES。如果未绑定成功或返回NO则会进入到第二次补救。比如我们调用了TestClass
中不存在的方法printInstanceAbsent
, 此时我们通过此机会可以转发到已实现的printInstanceInfo
中
示例代码:
//TestClass2.m中
@interface TestClass2 : NSObject
@end
@implementation TestClass2
- (void)printInstanceAbsent {
NSLog(@"%s",__func__);
}
@end
@implementation TestClass
- (void)printInstanceInfo {
NSLog(@"%s",__func__);
}
//使用实例方法进行演示
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(printInstanceInfo)) {
class_addMethod([self class],sel,class_getMethodImplementation([TestClass2 class], @selector(printInstanceAbsent)),"v");
return NO;
}
return [super resolveInstanceMethod:sel];
}
//打印
//-[TestClass2 printInstanceAbsent]
@end
第二次回调
如果上述最终返回NO,则回调到此方法。此方法要求返回一个id,允许把当前类的方法转发到其他类中,由其他类中同名同参接收
- (id)forwardingTargetForSelector:(SEL)aSelector;
//TestClass2.m中
@interface TestClass2 : NSObject
@end
@implementation TestClass2
- (void)printInstanceAbsent {
NSLog(@"%s",__func__);
}
@end
@implementation TestClass
- (void)printInstanceInfo {
NSLog(@"%s",__func__);
}
// printInstanceAbsent,TestClass中未实现,而TestClass2已实现
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(printInstanceAbsent:)) {
return [TestClass2 new];
}
return [super forwardingTargetForSelector:aSelector];
}
//打印
//-[TestClass2 printInstanceAbsent]
@end
第三次回调
此步骤有两个回调方法,methodSignatureForSelector
返回方法签名,forwardInvocation
返回方法具体实现。只有当返回方法签名后才会执行方法实现。此步骤更加灵活,且可以返回到不同类中。如果此步骤未实现或未返回最终时间,则程序crash
//TestClass2.m中
@interface TestClass2 : NSObject
@end
@implementation TestClass2
- (void)printInstanceAbsent {
NSLog(@"%s",__func__);
}
@end
//TestClass3.m中
@interface TestClass3 : NSObject
@end
@implementation TestClass3
- (void)printInstanceAbsent {
NSLog(@"%s",__func__);
}
@end
@implementation TestClass
- (void)printInstanceInfo {
NSLog(@"%s",__func__);
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(printInstanceAbsent)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
TestClass2 *test2 = [TestClass2 new];
TestClass3 *test3 = [TestClass3 new];
if ([test2 respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:test2];
}
if ([test3 respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:test3];
}
}
//打印
// -[TestClass2 printInstanceAbsent]
// -[TestClass3 printInstanceAbsent]
@end
以上就是整个消息转发的过程,充分展现了Runtime动态语言的特性。可能此处有疑问的是方法签名[NSMethodSignature signatureWithObjCTypes:“v@:”];中为什么是v@:,在这里做个简要说明,以本篇示例中的printInstanceAbsent
v
:当前方法返回值为void
@
:一个id类型的对象
:
:对应的方法SEL根据调用的方法变化,具体描述的格式详见Apple编码规则
最终通过方法拦截解决了通过js调用原生方法时App崩溃问题。
最后插个安利:inBuilder 低代码平台开源社区版,免费下载,免费使用,欢迎体验:inBuilder社区