写点什么

inBuilder 今日分享丨 Object-C 消息转发与发送机制

  • 2023-06-15
    山东
  • 本文字数:3367 字

    阅读完需:约 11 分钟

inBuilder今日分享丨Object-C消息转发与发送机制

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];
复制代码


上面的方法调用实际会转换为:

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社区

用户头像

还未添加个人签名 2023-03-07 加入

塑造企业一体化研发新范式

评论

发布
暂无评论
inBuilder今日分享丨Object-C消息转发与发送机制_inBuilder低代码平台_InfoQ写作社区