摘要:Aspects用来干什么?Aspect是一个简洁高效的用于使iOS支持AOP(面向切面编程)的框架。官方描述的很清楚,大致意思如下:你可以使用Aspect为每一个类或者类的实例的某个方法插入一段代码,切入点可以选择before(在原方法执行前执行)/instead(替换原方法)/after(原方法执行完之后执行)。ThinkofAspectsasmethodswizzlingonsteroids.Itallowsyoutoaddcodetoexistingmethodsper
Aspects用来干什么?
Aspect是一个简洁高效的用于使iOS支持AOP(面向切面编程)的框架。官方描述的很清楚,大致意思如下:你可以使用Aspect为每一个类或者类的实例的某个方法插入一段代码,切入点可以选择before(在原方法执行前执行)/instead(替换原方法)/after(原方法执行完之后执行)。
Think of Aspects as method swizzling on steroids. It allows you to
add code to existing methods per class or per instance, whilst thinking
of the insertion point e.g. before/instead/after. Aspects automatically
deals with calling super and is easier to use than regular method
swizzling.
本博文基于 v1.4.2 版本源码进行分析。
技术储备
Aspect是在Runtime的基础上构建的。在学习Aspect前,你需要搞清楚下面的概念:
1. NSMethodSignature 和 NSInvocation
使用NSMethodSignature 和 NSInvocation
不仅可以完成对method的调用,也可以完成block的调用。在Aspect中,正是运用NSMethodSignature,NSInvocation
实现了对block的统一处理。不清楚没关系,先搞清楚NSMethodSignature和NSInvocation的使用方法及如何使用他们执行method
或 block。
对象调用method代码示例
一个实例对象可以通过三种方式调用其方法。
- (void)test{
//type1
[self printStr1:@"hello world 1"];
//type2
[self performSelector:@selector(printStr1:) withObject:@"hello world 2"];
//type3
//获取方法签名
NSMethodSignature *sigOfPrintStr = [self methodSignatureForSelector:@selector(printStr1:)];
//获取方法签名对应的invocation
NSInvocation *invocationOfPrintStr = [NSInvocation invocationWithMethodSignature:sigOfPrintStr];
/**
设置消息接受者,与[invocationOfPrintStr setArgument:(__bridge void * _Nonnull)(self) atIndex:0]等价
*/
[invocationOfPrintStr setTarget:self];
/**设置要执行的selector。与[invocationOfPrintStr setArgument:@selector(printStr1:) atIndex:1] 等价*/
[invocationOfPrintStr setSelector:@selector(printStr1:)];
//设置参数
NSString *str = @"hello world 3";
[invocationOfPrintStr setArgument:&;str atIndex:2];
//开始执行
[invocationOfPrintStr invoke];
}
- (void)printStr1:(NSString*)str{
NSLog(@"printStr1 %@",str);
}
在调用test方法时,会分别输出:
2017-01-11 15:20:21.642 AspectTest[2997:146594] printStr1 hello world 1
2017-01-11 15:20:21.643 AspectTest[2997:146594] printStr1 hello world 2
2017-01-11 15:20:21.643 AspectTest[2997:146594] printStr1 hello world 3
type1和type2是我们常用的,这里不在赘述,我们来说说type3。
NSMethodSignature和NSInvocation是Foundation框架为我们提供的一种调用方法的方式,经常用于消息转发。
NSMethodSignature概述
NSMethodSignature用于描述method的类型信息:返回值类型,及每个参数的类型。 可以通过下面的方式进行创建:
@interface NSObject
//获取实例方法的签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
//获取类方法的签名
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector;
@end
-------------
//使用ObjCTypes创建方法签名
@interface NSMethodSignature
+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
@end
使用NSObject的实例方法和类方法创建NSMethodSignature很简单,不说了。咱撩一撩signatureWithObjCTypes。
在OC中,每一种数据类型可以通过一个字符编码来表示(Objective-C type encodings)。例如字符‘@’代表一个object, 'i'代表int。 那么,由这些字符组成的字符数组就可以表示方法类型了。举个例子:上面提到的printStr1:对应的ObjCTypes 为aliyunzixun@xxx.com:@。
’v‘ : void类型,第一个字符代表返回值类型
’@‘ : 一个id类型的对象,第一个参数类型
’:‘ : 对应SEL,第二个参数类型
’@‘ : 一个id类型的对象,第三个参数类型,也就是- (void)printStr1:(NSString*)str中的str。
printStr1:本来是一个参数,ObjCTypes怎么成了三个参数?要理解这个还必须理解OC中的消息机制。一个method对应的结构体如下,ObjCTypes中的参数其实与IMP
method_imp 函数指针指向的函数的参数相一致。相关内容有很多,不了解的可以参考这篇文章方法与消息。
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
NSInvocation概述
就像示例代码所示,我们可以通过+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;创建出NSInvocation对象。接下来你设置各个参数信息, 然后调用invoke进行调用。执行结束后,通过- (void)getReturnValue:(void *)retLoc;获取返回值。
这里需要注意,对NSInvocation对象设置的参数个数及类型和获取的返回值的类型要与创建对象时使用的NSMethodSignature对象代表的参数及返回值类型向一致,否则cresh。
使用NSInvocation调用block
下面展示block 的两种调用方式
- (void)test{
void (^block1)(int) = ^(int a){
NSLog(@"block1 %d",a);
};
//type1
block1(1);
//type2
//获取block类型对应的方法签名。
NSMethodSignature *signature = aspect_blockMethodSignature(block1);
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:block1];
int a=2;
[invocation setArgument:&;a atIndex:1];
[invocation invoke];
}
type1 就是常用的方法,不再赘述。看一下type2。 type2和上面调用method的type3用的一样的套路,只是参数不同:由block生成的NSInvocation对象的第一个参数是block本身,剩下的为 block自身的参数。
由于系统没有提供获取block的ObjCTypes的api,我们必须想办法找到这个ObjCTypes,只有这样才能生成NSMethodSignature对象!
block的数据结构 &; 从数据结构中获取 ObjCTypes
oc是一门动态语言,通过编译 oc可以转变为c语言。经过编译后block对应的数据结构是struct。(block中技术点还是挺过的,推荐一本书“Objective-C 高级编程”)
//代码来自 Aspect
// Block internals.
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;
在此结构体中 const char *signature 字段就是我们想要的。通过下面的方法获取signature并创建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];
}
2. method swizzling
在Objective-C中调用一个方法,其实是向一个对象发送消息。每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向Method具体的实现。
selector-imp.png
通过 method swizzling这种黑科技,你可以改变selector和方法实现的映射关系。
swizzled-imp
此时当执行[objc selectorC]时,实际调用的是 IMPn指针指向的函数。
具体实现代码如下:
代码来源: https://github.com/hejunm/iOS-Tools
@implementation HJMSwizzleTools:NSObject
+ (void)hjm_swizzleWithClass:(Class)processedClass originalSelector:(SEL)originSelector swizzleSelector:(SEL)swizzlSelector{
Method originMethod = class_getInstanceMethod(processedClass, originSelector);
Method swizzleMethod = class_getInstanceMethod(processedClass, swizzlSelector);
//当processedClass实现originSelector时,didAddMethod返回false,否则返回true. 如果当前类没有实现originSelector而父类实现了,这是直接使用method_exchangeImplementations会swizzle父类的originSelector。这样会出现很大的问题。
BOOL didAddMethod = class_addMethod(processedClass, originSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
class_replaceMethod(processedClass, swizzlSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else{
method_exchangeImplementations(originMethod, swizzleMethod);
}
}
@end
可以这样使用
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&;onceToken, ^{
[HJMSwizzleTools hjm_swizzleWithClass:self originalSelector:@selector(viewDidLoad) swizzleSelector:@selector(swizzleViewDidLoad)];
});
}
//被替换了。。
- (void)viewDidLoad {
[super viewDidLoad];
}
//现在系统会调用这个方法
- (void)swizzleViewDidLoad {
NSLog(@"do something");
}
3. 消息转发流程
在Objective-C中调用一个方法,其实是向一个对象发送消息。如果这个消息没有对应的实现时就会进行消息转发。转发流程图如下:
forwardMethod.png
下面用代码演示一遍
resolveInstanceMethod
当根据selector没有找到对应的method时,首先会调用这个方法,在该方法中你可以为一个类添加一个方法。并返回yes。下面的代码只是声明了runTo方法,没有实现。
//Car.h
@interface Car : NSObject
- (void)runTo:(NSString *)place;
@end
//Car.m
@implementation Car
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(runTo:)) {
class_addMethod(self, sel, (IMP)dynamicMethodIMPRunTo, "aliyunzixun@xxx.com:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
//动态添加的@selector(runTo:) 对应的实现
static void dynamicMethodIMPRunTo(id self, SEL _cmd,id place){
NSLog(@"dynamicMethodIMPRunTo %@",place);
}
@end
forwardingTargetForSelector
如果resolveInstanceMethod没有实现,返回No,或者没有动态添加方法的话,就会执行forwardingTargetForSelector。
在这里你可以返回一个能够执行这个selector的对象otherTarget,接下来消息会重新发送到这个otherTarget。
//Person.h
@interface Person : NSObject
- (void)runTo:(NSString *)place;
@end
//Person.m
@implementation Person
- (void)runTo:(NSString *)place;{
NSLog(@"person runTo %@",place);
}
@end
//Car.h
@interface Car : NSObject
- (void)runTo:(NSString *)place;
@end
//Car.m
@implementation Car
- (id)forwardingTargetForSelector:(SEL)aSelector{
//将消息转发给Person的实例
if (aSelector == @selector(runTo:)){
return [[Person alloc]init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
forwardInvocation
如果上面两种情况没有执行,就会执行通过forwardInvocation进行消息转发。
@implementation Car
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{
//判断selector是否为需要转发的,如果是则手动生成方法签名并返回。
if (aSelector == @selector(runTo:)){
return [NSMethodSignature signatureWithObjCTypes:"aliyunzixun@xxx.com:@"];
}
return [super forwardingTargetForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//判断待处理的anInvocation是否为我们要处理的
if (anInvocation.selector == @selector(runTo:)){
}else{
}
}
@end
在NSInvocation对象中保存着我们调用一个method的所有信息。可以看下其属性和方法:
methodSignature 含有返回值类型,参数个数及每个参数的类型 等信息。
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;获取调用method时传的参数
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx; 设置第index参数。
- (void)invoke; 开始执行
- (void)getReturnValue:(void *)retLoc; 获取返回值
下面的代码演示如何获取调用method时所传的各参数值
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if (anInvocation.selector == @selector(runTo:)){
void *argBuf = NULL;
NSUInteger numberOfArguments = anInvocation.methodSignature.numberOfArguments;
for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
const char *type = [anInvocation.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argSize;
NSGetSizeAndAlignment(type, &;argSize, NULL);
if (!(argBuf = reallocf(argBuf, argSize))) {
NSLog(@"Failed to allocate memory for block invocation.");
return ;
}
[anInvocation getArgument:argBuf atIndex:idx];
//现在argBuf 中保存着第index 参数的值。 你可以使用这些值进行其他处理,例如为block中各参数赋值,并调用。
}
}else{
}
}
通过手动触发消息转发(method已经实现)
前面所描述的消息转发都是在selector没有对应实现时自动进行的,我们称之为自动消息转发。现在有个需求:即使Car类实现了 runTo:,执行[objOfCar runTo:@"shangHai"]; 时也进行消息转发(手动触发),如何实现?
实现方法如下:利用 method swizzling 将selector的实现改变为_objc_msgForward或者_objc_msgForward_stret。在调selector时就会进行消息转发 看下面的代码:
//对 runTo: 进行消息转发
@implementation Car
//进行 method swizzling。此时调用runTo:就会进行消息转发
+ (void)load{
SEL selector = @selector(runTo:);
Method targetMethod = class_getInstanceMethod(self.class, @selector(selector));
const char *typeEncoding = method_getTypeEncoding(targetMethod);
IMP targetMethodIMP = _objc_msgForward;
class_replaceMethod(self.class, selector, targetMethodIMP, typeEncoding);
}
- (void)runTo:(NSString *)place{
NSLog(@"car runTo %@",place);
}
//消息转发,调用这个方法。anInvocation中保存着调用方法时传递的参数信息
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if (anInvocation.selector == @selector(runTo:)){
}else{
}
}
上面提到了_objc_msgForward或者_objc_msgForward_stret,
该如何选择?首先两者都是进行消息转发的,大概是这样:如果转发的消息的返回值是struct类型,就使用_objc_msgForward_stret,否则使用_objc_msgForward。参考资料。简单引用JSPatch作者的解释
大多数CPU在执行C函数时会把前几个参数放进寄存器里,对 obj_msgSend 来说前两个参数固定是 self /
_cmd,它们会放在寄存器上,在最后执行完后返回值也会保存在寄存器上,取这个寄存器的值就是返回值。普通的返回值(int/pointer)很小,放在寄存器上没问题,但有些
struct
是很大的,寄存器放不下,所以要用另一种方式,在一开始申请一段内存,把指针保存在寄存器上,返回值往这个指针指向的内存写数据,所以寄存器要腾出一个位置放这个指针,self
/ _cmd 在寄存器的位置就变了。objc_msgSend 不知道 self / _cmd 的位置变了,所以要用另一个方法
objc_msgSend_stret 代替。原理大概就是这样。在 NSMethodSignature 的 debugDescription
上打出了是否 special struct,只能通过这字符串判断。所以最终的处理是,在非 arm64 下,是 special struct 就走
_objc_msgForward_stret,否则走 _objc_msgForward。
根据selector返回值类型获取_objc_msgForward或者_objc_msgForward_stret 的代码如下:
//代码来自Aspect
static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
Method method = class_getInstanceMethod(self.class, selector);
const char *encoding = method_getTypeEncoding(method);
BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
if (methodReturnsStructValue) {
@try {
NSUInteger valueSize = 0;
NSGetSizeAndAlignment(encoding, &;valueSize, NULL);
if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
methodReturnsStructValue = NO;
}
} @catch (__unused NSException *e) {}
}
if (methodReturnsStructValue) {
msgForwardIMP = (IMP)_objc_msgForward_stret;
}
#endif
return msgForwardIMP;
}
Aspects 源码
一直在思考如何使用文字清晰的描述出Aspects的实现原理,最后决定使用在源码上添加注释的形式呈现。自己偷个懒。
Aspects 源码剖析
Aspects 使用场景
app埋点是 Aspects 框架的一个典型应用,你可以通过RunTime应用实例--关于埋点的思考了解 Aspects的使用场景。