消息转发机制与Aspects源码解析
栏目: Objective-C · 发布时间: 7年前
内容简介:消息转发机制与Aspects源码解析
版权声明:本文为博主原创文章,如需转载请注明出处
前言
最近在搞重构相关的事情,遇到了不少这样的场景:
进入一个界面,在viewWillAppear:的时候做相应判断,如果满足条件则执行对应代码。
这类业务有一个特点,业务内容是对应整个App的,与对应的ViewController毛关系都没有,但是却不得不耦合到(即使是调用代码可以精简到一行)ViewController中。
我们都知道,这种类似的业务用AOP(面向切片编程)来做十分适合,所谓面向切片编程就是在不修改原方法的前提下,动态的插入自己的想要的执行代码,由于Objective C是动态语言,可以很容易的利用method swizzling来实现AOP。
在正文之前特别感谢 微信 阅读团队的这篇博客:
这篇博客原理上讲解的比较清楚,但是细节上并没有讲的很详细,所以也就有了本文。
Objective C方法调用过程
这个其实我之前在这篇博客里讲过:
这里,把核心的内容再一次列出来。
如下Objective C代码
- (NSInteger )myTestFunction:(NSInteger)input{ return input + 1; } - (void)mySpecialFunction{ NSInteger result = [self myTestFunction:10]; }
用clang来重写为C++,
clang -rewrite-objc MyClass.m
然后,我们通过搜索mySpecialFunction方法名字,来找到转换后的代码,经过简单整理如下
static NSInteger _I_MyClass_myTestFunction_(MyClass * self, SEL _cmd, NSInteger input) { return input + 1; } static void _I_MyClass_mySpecialFunction(MyClass * self, SEL _cmd) { NSInteger result = objc_msgSend(self, sel_registerName("myTestFunction:"),10); }
我们看到,方法体进行了如下转换
//OC - (NSInteger )myTestFunction:(NSInteger)input{ return input + 1; } //C++ static NSInteger _I_MyClass_myTestFunction_(MyClass * self, SEL _cmd, NSInteger input) { return input + 1; }
方法调用进行了如下转换
//OC NSInteger result = [self myTestFunction:10]; //C++ NSInteger result = objc_msgSend(self, sel_registerName("myTestFunction:"),10);
不难看出,方法的调用并不是直接转换成了对应的C/C++方法调用,而是调用了objc_msgSend通过SEL(就是一个字符串)在运行时动态找到这个的执行体_I_MyClass_myTestFunction_。
那么,在运行时如何找到这个方法的执行体呢? 这里省略一些细节,对细节感兴趣的同学可以看我上文写的那篇文章。一个实例方法的流程如下:
-
对象实例收到消息(SEL+参数)
-
根据存储在对象实例中的ISA到类对象,类对象依次查找Class Cache(方法表缓存)和dispatch table找到对应的Method,如果找到Method,执行对应Method的IMP(方法体),并且返回结果
-
如果找不到Method,则根据类对象中的super_class指针找到父类的Class对象。一直找到NSObject的类对象
-
如果NSObject也无法找到这个SEL,则进入消息转发机制
-
如果消息转发机制无法处理,则抛出异常: doesNotRecognizeSelector
Method Swizzling
通过上文我们知道,一个方法的调用实际上就是SEL(方法名)通过Runtime找到IMP(方法执行体)
既然是通过Runtime动态找到的,那么我们就可以利用Runtime的API,讲SEL_1来指向IMP_2,接着我们再在在IMP_2的方法体中执行IMP_1,就实现了动态插入代码。
消息转发机制
在Objective C的方法调用过程中,我们提到了当无法响应一个selector时,在抛出异常之前会先进入消息转发机制。这里来详细讲解消息转发的过程:
关于消息转发,官方文档在这里: Message Forwarding
在触发消息转发机制即forwardInvocation:之前,Runtime提供了两步来进行轻量级的动态处理这个selector.
resolveInstanceMethod:
Dynamically provides an implementation for a given selector for an instance method.
这个方法提供了一个机会: 为当前类无法识别的SEL动态增加IMP 。
比如:最常见的可以通过class_addMethod
void dynamicMethodIMP(id self, SEL _cmd){/*...implementation...*/} + (BOOL) resolveInstanceMethod:(SEL)aSEL { if (aSEL == @selector(resolveThisMethodDynamically)) { class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:aSel]; }
Tips,这里的"v@:"表示方法参数编码,v表示Void,@表示OC对象,:表示SEL类型。关于方法参数编码,更详细的内容参见 文档 。
如果resolveInstanceMethod返回NO,则表示无法在这一步动态的添加方法,则进入下一步:
-
forwardingTargetForSelector:
Returns the object to which unrecognized messages should first be directed.
这个方法提供了一个机会:简单的把这个SEL交给另外一个对象来执行。
比如:
-(id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(dynamicSelector) && [self.myObj respondsToSelector:@selector(dynamicSelector)]) { return self.myObj; }else{ return [super forwardingTargetForSelector:aSelector]; } }
如果上述两步都无法完成这个SEL的处理,则进入消息转发机制,消息转发机制有两个比较重要的方法:
-
forwardInvocation: 具体的NSInvocaion
-
methodSignatureForSelector: 返回SEL的方法签名
这里不得不提一下两个类:
-
NSMethodSignature 用来表示方法的参数签名信息:返回值,参数数量和类型
-
NSInvocaion SEL + 执行SEL的Target + 参数值
通常,拿到NSInvocaion对象后,我们可选择的进行如下操作
-
修改执行的SEL
-
修改执行的Target
-
修改传入的参数
然后调用:[invocation invoke],来执行这个消息。
_objc_msgForward
我们知道,正常情况下SEL背后会对一个IMP,在OC中有一个特殊的IMP就是:_objc_msgForward。当执行_objc_msgForward时,会直接触发消息转发机制,即forwardInvocation:。
Aspect的基本原理
使用Aspect,可以在一个OC方法执行前/后插入代码,也可以替换这个OC方法的实现。
这里,我们以在ViewControler的viewWillAppear:方法之后插入一段代码为例,来讲解hook前后的变化,
在没有hook之前,ViewController的SEL与IMP关系如下
调用以下aspect来hook viewWillAppear:后:
[ViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^{ NSLog(@"Insert some code after ViewWillAppear"); } error:&error];
-
最初的viewWillAppear: 指向了_objc_msgForward
-
增加了aspects_viewWillAppear:,指向最初的viewWillAppear:的IMP
-
最初的forwardInvocation:指向了Aspect提供的一个C方法__ASPECTS_ARE_BEING_CALLED__
-
动态增加了__aspects_forwardInvocation:,指向最初的forwardInvocation:的IMP
然后,我们再来看看hook后,一个viewWillAppear:的实际调用顺序:
-
object收到selector(viewWillAppear:)的消息
-
找到对应的IMP:_objc_msgForward,执行后触发消息转发机制。
-
object收到forwardInvocation:消息
-
找到对应的IMP:__ASPECTS_ARE_BEING_CALLED__,执行IMP
向object对象发送aspects_viewWillAppear:,执行最初的viewWillAppear方法的IMP
执行插入的block代码
如果ViewController无法响应aspects_viewWillAppear,则向object对象发送__aspects_forwardInvocation:来执行最初的forwardInvocation IMP
所以,Aspects是采用了集中式的hook方式,所有的调用最后走的都是一个C函数__ASPECTS_ARE_BEING_CALLED__。
核心类/数据结构
-
AspectIdentifier - 代表一个Aspect的具体信息:包括被Hook的对象,SEL,插入的block等具体信息。
@interface AspectIdentifier : NSObject @property (nonatomic, assign) SEL selector; @property (nonatomic, strong) id block; @property (nonatomic, strong) NSMethodSignature *blockSignature; @property (nonatomic, weak) id object; @property (nonatomic, assign) AspectOptions options; @end
-
AspectTracker - 跟踪一个类的继承链中的hook状态:包括被hook的类,哪些SEL被hook了。
@interface AspectTracker : NSObject @property (nonatomic, strong) Class trackedClass; @property (nonatomic, readonly) NSString *trackedClassName; @property (nonatomic, strong) NSMutableSet *selectorNames; @property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers; @end
-
AspectContainer - AspectIdentifier的容器:以SEL合成key,然后作为关联对象存储到对应的类/对象里。包括beforeAspects,insteadAspects,afterAspects
@interface AspectsContainer : NSObject @property (atomic, copy) NSArray *beforeAspects; @property (atomic, copy) NSArray *insteadAspects; @property (atomic, copy) NSArray *afterAspects; @end
-
AspectInfo - NSInvocation的容器,表示一个执行的Command。
@interface AspectInfo : NSObject @property (nonatomic, unsafe_unretained, readonly) id instance; @property (nonatomic, strong, readonly) NSArray *arguments; @property (nonatomic, strong, readonly) NSInvocation *originalInvocation; @end
hook过程
同样,我们以一个实例方法为例,讲解在这个方法调用后发生了什么
[ViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^{ NSLog(@"Insert some code after ViewWillAppear"); } error:&error];
1.对Class和MetaClass进行进行合法性检查,判断能否hook,规则如下
retain,release,autorelease,forwoardInvocation:不能被hook
dealloc只能在方法前hook
类的继承关系中,同一个方法只能被hook一次
2.创建AspectsContainer对象,以aspects_ + SEL为key,作为关联对象依附到被hook 的对象上
objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
3.创建AspectIdentifier对象,并且添加到AspectsContainer对象里存储起来。这个过程分为两步
生成block的方法签名NSMethodSignature
对比block的方法签名和待hook的方法签名是否兼容(参数个数,按照顺序的类型)
4.根据hook实例对象/类对象/类元对象的方法做不同处理。其中,对于上文以类方法来hook的时候,分为两步
hook类对象的forwoardInvocation:方法,指向一个静态的C方法,并且创建一个aspects_ forwoardInvocation:动态添加到之前的类中
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); if (originalImplementation) { class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); }
hook类对象的viewWillAppear:方法让其指向_objc_msgForward,动态添加aspects_viewWillAppear:指向最初的viewWillAppear:实现
Hook实例的方法
Aspects支持只hook一个对象的实例方法
只不过在第4步略有出入,当hook一个对象的实例方法的时候:
-
新建一个子类,_Aspects_ViewController,并且按照上述的方式hook forwoardInvocation:
-
hook _Aspects_ViewController的class方法,让其返回ViewController
-
hook 子类的类元对象,让其返回ViewController
-
调用objc_setClass来修改ViewController的类为_Aspects_ViewController
这样做,就可以通过object_getClass(self)获得类名,然后看看是否有前缀类名来判断是否被hook过了
其他
object_getClass/与self.class的区别
-
object_getClass获得的是isa的指向
-
self.class则不一样,当self是实例对象的时候,返回的是类对象,否则则返回自身。
比如:
TestClass * testObj = [[TestClass alloc] init]; //Same logAddress([testObj class]); logAddress([TestClass class]); //Not same logAddress(object_getClass(testObj)); logAddress(object_getClass([TestClass class]));
Log
2017-05-22 22:41:48.216 OCTest[899:25934] 0x107d10930 2017-05-22 22:41:48.216 OCTest[899:25934] 0x107d10930 2017-05-22 22:41:48.216 OCTest[899:25934] 0x107d10930 2017-05-22 22:41:49.061 OCTest[899:25934] 0x107d10908
Block签名
block因为背后其实是一个C结构体,结构体中存储着着一个函数指针来指向实际的方法体
Block的内存布局如下
typedef NS_OPTIONS(int, AspectBlockFlags) { AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), AspectBlockFlagsHasSignature = (1 << 30) }; typedef struct _AspectBlock { __unused Class isa; AspectBlockFlags flags; __unused int reserved; void (__unused *invoke)(struct _AspectBlock *block, ...); struct { unsigned long int reserved; unsigned long int size; // requires AspectBlockFlagsHasCopyDisposeHelpers void (*copy)(void *dst, const void *src); void (*dispose)(const void *); // requires AspectBlockFlagsHasSignature const char *signature; const char *layout; } *descriptor; // imported variables } *AspectBlockRef;
对应生成NSMethodSignature的方法:
static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) { AspectBlockRef layout = (__bridge void *)block; if (!(layout->flags & AspectBlockFlagsHasSignature)) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block]; AspectError(AspectErrorMissingBlockSignature, description); return nil; } void *desc = layout->descriptor; desc += 2 * sizeof(unsigned long int); if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) { desc += 2 * sizeof(void *); } if (!desc) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block]; AspectError(AspectErrorMissingBlockSignature, description); return nil; } const char *signature = (*(const char **)desc); return [NSMethodSignature signatureWithObjCTypes:signature]; }
关于Block的更多讲解,参见我的前一篇博客
效率
消息转发机制相对于正常的方法调用来说是比较昂贵的,所以一定不要用消息转发机制来处理那些一秒钟成百上千次的调用。
总结
Objective C的消息转发机制是一个非常灵活的机制,用好它会让你实现很多黑科技,也能够让你的 架构 更加灵活。
以上所述就是小编给大家介绍的《消息转发机制与Aspects源码解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
你不知道的JavaScript(中卷)
[美] Kyle Simpson / 单业、姜南 / 人民邮电出版社 / 2016-8 / 79.00元
JavaScript这门语言简单易用,很容易上手,但其语言机制复杂微妙,即使是经验丰富的JavaScript开发人员,如果没有认真学习的话也无法真正理解。本套书直面当前JavaScript开发人员不求甚解的大趋势,深入理解语言内部的机制,全面介绍了JavaScript中常被人误解和忽视的重要知识点。本书是其中卷,主要介绍了类型、语法、异步和性能。一起来看看 《你不知道的JavaScript(中卷)》 这本书的介绍吧!