背景
一个库:Aspects
两篇文章:面向切面编程之 Aspects 源码解析及应用
消息转发机制与Aspects源码解析。
Aspects库的作用就是可以通过一行代码在某个类的某个方法里插入代码。
核心接口:
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
但是它有几个比较明显的问题:
- 为什么用 forwardInvocation?这会导致没有返回值
- 为什么继承链里只能被修改一次?
- 为什么没有类方法修改?
尝试解决
看它的代码的时候,发现并没有想象的简单,在我的想法里,插入一段代码,就是:把原本的method和另一个method切换,然后在那个method里调用原来的method和插入的代码。就跟你想在一个方法里添加一段代码那样去写,我觉得这是最直观的了。可是它最后绕到了forwardInvocation里去了。
简单说,就是把原来的method的实现搞没了去,然后利用OC的消息转发特性最后转发到了forwardInvocation方法。用这个方法有两个坏处:
- 没有返回值,forwardInvocation的返回值是void,所以如果你修改的方法原本是有返回值的,会被搞没有。
2. 会和其他的swizzle库冲突,因为forwardInvocation方法只有一个,你搞一个自己的实现,它搞一个自己的实现。后一个就挤掉前面的想了下是有解决办法的,但是要所有的库都同时遵守,即调用完自己的实现都要调用原来的实现,如果同时有多个库,那么这个原来的实现可能就是别的库的实现,这样就可以实现一个链式调用,大家都会调用。
反正我就尝试按直觉的那样去写, demo在此。
+(void)injectAspectsToSelector:(SEL)selector block:(id)block error:(NSError **)error{
if (![self isInjectAvailableForSelector:selector error:error]) {
return;
}
Method originMethod = class_getInstanceMethod(self, selector);
IMP originalIMP = method_getImplementation(originMethod);
const char *originalTypes = method_getTypeEncoding(originMethod);
//位置1
class_replaceMethod(self, selector, (IMP)injectedCommonFunc, "@@:");
SEL injectedselector = NSSelectorFromString([injectedSelectorPrefix stringByAppendingFormat:@"_%@",NSStringFromSelector(selector)]);
//位置2
BOOL addSucceed = class_addMethod(self, injectedselector, originalIMP, originalTypes);
if (!addSucceed) {
NSLog(@"%@ add method %@ failed",TFClassDesc(self), NSStringFromSelector(injectedselector));
}
//位置3
objc_setAssociatedObject(self, injectedselector, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
核心就是这个方法了,selector
是想要修改的方法,block
是想插入的代码。
把原来的方法的
IMP
切换成我定义的一个通用函数injectedCommonFunc
(位置1)。
这个函数定义得跟objc_msgSend
一样:id injectedCommonFunc(id self, SEL selector, ...)
。我的想法是使用变参函数来应对不确定的情况。定义两个这样的函数,一个有返回值一个没返回值就可以了,可以根据Method的typeEncoding
获取返回值情况,然后决定使用哪个。添加一个新方法指向原来的
IMP
,新方法名使用一个前缀加原来的方法名(位置2)。把要插入的block和被修改的类使用
objc_setAssociatedObject
绑定,并且key使用新方法。
转发到injectedCommonFunc
经过上面的处理,调用原方法后,实际执行的是injectedCommonFunc
。
- 获取要插入的block
Class realClass = object_getClass(self);
SEL injectedselector = NSSelectorFromString([injectedSelectorPrefix stringByAppendingFormat:@"_%@",NSStringFromSelector(selector)]);
//find the first injected block along the class inheritance chain
id injectBlock;
Class injectedClass = realClass;
do {
injectBlock = objc_getAssociatedObject(injectedClass, injectedselector);
} while (!injectBlock && (injectedClass = class_getSuperclass(injectedClass)));
这个do-while循环的目的是:沿着继承链向上找到和类绑定的block。因为我想设计的效果是,代码插入效果是可以被子类继承的,所以插入的block可能会在某个父类里,而不是和当前调用者的class绑定。所以要追溯向上找到。
那么接下来的问题就是怎么调用这个block?
这里的关键问题是参数是未知的,而block只是id类型,不是变参函数。所以我借鉴了Aspects,使用NSInvocation。
- 构建blockInvocation
Method injectedMethod = class_getInstanceMethod(realClass, injectedselector);
const char *originalTypes = method_getTypeEncoding(injectedMethod);
NSMethodSignature *originSignature = [NSMethodSignature signatureWithObjCTypes:originalTypes];
char *blockTypes = malloc(sizeof(char)*(strlen(originalTypes)+1));
strcat(blockTypes, [originSignature methodReturnType]);
strcat(blockTypes, "@?");
for (int i = 2; i<[originSignature numberOfArguments]; i++) {
strcat(blockTypes, [originSignature getArgumentTypeAtIndex:i]);
}
NSMethodSignature *blockSignature = [NSMethodSignature signatureWithObjCTypes:blockTypes];
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:blockSignature];
NSInvocation *originalInvocation = [NSInvocation invocationWithMethodSignature:originSignature];
originalInvocation.selector = injectedselector;
originalInvocation.target = self;
这里默认的认知是,block的参数类型和被插入代码的方法类型是一样的,某则没法搞。
获取原方法的签名
originSignature
,因为OC方法自带self和selector两个参数,所以实际参数从第三个开始。先把返回值类型赋值给blockTypes,然后从第三个参数开始,依次把参数类型拷贝过去。
然后由类型字符串blockTypes构建签名blockSignature;由签名构建blockInvocation
给blockInvocation设置参数
va_list params;
va_start(params, selector);
.......
.......
void *argument = NULL;
id object = nil;
int num_int;
for (int i = 1; i< blockSignature.numberOfArguments; i++) {
const char argType = [blockSignature getArgumentTypeAtIndex:i][0];
//TODO: other arg types
if (argType == _C_ID) {
object = va_arg(params, id);
argument = &object;
}else if (argType == _C_INT){
num_int = va_arg(params, int);
argument = &num_int;
}
[blockInvocation setArgument:argument atIndex:i];
[originalInvocation setArgument:argument atIndex:i+1];
}
va_end(params);
使用变参函数的性质,把参数一个个取出来,但是要直到类型才能取。但是因为有*block参数类型和原方法一致的设定,那么参数类型是直到的。所以对不同的argType
,调用不同的类型取值。比如:@
表示对象,即id
,那就调用va_arg(params, id)
取值。这些对应关系在Type Encodings里。
原方法的调用也使用NSInvocation来调用,因为发现也没有办法传递参数。但它和blockInvocation类型,也不必多做多少处理。
- 调用NSInvocation,拿到返回值
[blockInvocation invokeWithTarget:injectBlock];
[originalInvocation invoke];
void *returnValue = nil;
[originalInvocation getReturnValue:&returnValue];
return (__bridge id)(returnValue);
这里有个小坑:getReturnValue
的结果是直接把内存赋值给returnValue,没有做任何内存管理相关的操作的,相当于没有retain,如果你用一个__strong
类型的变量去接,后面用完了会release,这样就会堆出来一个release, 然后crash。所以先用一个__weak
指针或void*指针去接,然后转到正确类型。
转折
一开始跑得都挺好的,直到我突然发现不行了,怎么会?我明明没有修改什么东西?然后我猛地意识到似乎之前都是在模拟器上跑!-_-
关键点在变参函数取不到值了,而在模拟器上是可以的。
我仔细看了下变参函数获取参数的那几个宏:va_list
,va_start
,va_arg
和va_end
。
网上可以查到他们的定义,原理是依靠参数入栈的规律:参数由后往前逐个入栈,且地址从高到底一次排列。这样只要知道了其中某个参数的位置,其他参数都可以通过类型一次找出来。
但可惜的是,经过观察,iOS和mac上都不是这样的!我看到的结论是:
固定参数的位置和变参的位置是在不同的区域,并且不是紧贴这的。
固定参数的位置是一次排列的,但是是前往后,地址逐渐降低,而不是升高
-
使用
va_start(ap, param)
用来定位第一个变参函数的位置,这个在模拟和真机上有区别,正是这个导致了整个方案的失败。- 在模拟器上,va_start得到的位置是根据函数自身来确定的,比如你有一个固定参数,那么定位的是第二个参数,如果你有固定参数,那么定位的就是第三个参数。
- 在真机上,va_start定位似乎是根据内存分布来的,调用函数的时候,哪些是固定,哪些是变参就已经确定好了,跟函数定义没关系。
- 举例:
IMP unknownIMP = class_getMethodImplementation([TFPerson class], @selector(unknownParamsFunc:otherSome:)); ((NSString *(*)(id self, SEL selector, ...))unknownIMP)(person,@selector(unknownParamsFunc:otherSome:),@"known_xx0",@"known_xx1",@"known_xx2",@"known_xx3");
unknownParamsFunc:otherSome:
这个方法实际是有两个参数的,在真机上,va_start永远定位第一个参数known_xx0
,因为调用的时候,转成(NSString *(*)(id self, SEL selector, ...)
类型来调用的,所有4个参数都是变参。如果改成(id self, SEL selector,id name, ...)
就会是第二个参数known_xx1
。
而在模拟就永远定位在第三个参数,因为函数有两个定参。 所以在模拟器上,我把一个有n个固定参数的方法的IMP指向一个变参函数
injectedCommonFunc
,我还是可以去得到所有的参数值的。而在真机上,原本调用的时候就没有变参,va_start
定位就是空,取不到固定参数。
最后
最后,我想到了objc_msgsend
,我们调用函数都是通过它转发,它的参数类型也是(id self, SEL selector, ...)
,那么它又是怎么做到把固定参数和变参都取到的?
然后就找到mikeash的一篇文章,翻译, 原文。关于参数的部分看了下,用的汇编。
“整型数和指针参数会被传入寄存器 %rsi, %rdi, %rdx, %rcx, %r8 和 %r9。其他类型的参数会被传进栈(stack)中” 之类的处理,但明确的事,没有开放的函数/接口可以用来处理这些事,即使猜到了内部的处理,也是不稳定的,因为没有开放接口,那么内部的改变就不需要对外界负责。
到此也明白了为什么要用forwardInvocation
来做处理,而不是自定义的函数,因为forwardInvocation
自带一个NSInvocation参数,包含了原方法所有的参数信息。至于类方法的修改,使用object_getClass(self)
来做调用者,因为类方法放在metaClass里,object_getClass(self)
当self本身就是Class是得到的就是它的metaClass。最后继承链里只能一个类被修改,这个我没想通为什么这么做,因为我的方案在模拟器上实验,多个修改是没有问题的。
所以就到此结束了,当一次学习吧。