消息转发机制与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源码解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Design and Analysis of Distributed Algorithms (Wiley Series on P
Nicola Santoro / Wiley-Interscience / 2006-10-27 / USD 140.95
This text is based on a simple and fully reactive computational model that allows for intuitive comprehension and logical designs. The principles and techniques presented can be applied to any distrib......一起来看看 《Design and Analysis of Distributed Algorithms (Wiley Series on P》 这本书的介绍吧!
CSS 压缩/解压工具
在线压缩/解压 CSS 代码
HEX CMYK 转换工具
HEX CMYK 互转工具