背景
一個庫:Aspects
兩篇文章:面向切面編程之 Aspects 源碼解析及應(yīng)用
消息轉(zhuǎn)發(fā)機制與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?這會導(dǎo)致沒有返回值
- 為什么繼承鏈里只能被修改一次?
- 為什么沒有類方法修改?
嘗試解決
看它的代碼的時候,發(fā)現(xiàn)并沒有想象的簡單,在我的想法里,插入一段代碼,就是:把原本的method和另一個method切換,然后在那個method里調(diào)用原來的method和插入的代碼。就跟你想在一個方法里添加一段代碼那樣去寫,我覺得這是最直觀的了??墒撬詈罄@到了forwardInvocation里去了。
簡單說,就是把原來的method的實現(xiàn)搞沒了去,然后利用OC的消息轉(zhuǎn)發(fā)特性最后轉(zhuǎn)發(fā)到了forwardInvocation方法。用這個方法有兩個壞處:
- 沒有返回值,forwardInvocation的返回值是void,所以如果你修改的方法原本是有返回值的,會被搞沒有。
2. 會和其他的swizzle庫沖突,因為forwardInvocation方法只有一個,你搞一個自己的實現(xiàn),它搞一個自己的實現(xiàn)。后一個就擠掉前面的想了下是有解決辦法的,但是要所有的庫都同時遵守,即調(diào)用完自己的實現(xiàn)都要調(diào)用原來的實現(xiàn),如果同時有多個庫,那么這個原來的實現(xiàn)可能就是別的庫的實現(xiàn),這樣就可以實現(xiàn)一個鏈?zhǔn)秸{(diào)用,大家都會調(diào)用。
反正我就嘗試按直覺的那樣去寫, 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切換成我定義的一個通用函數(shù)injectedCommonFunc(位置1)。
這個函數(shù)定義得跟objc_msgSend一樣:id injectedCommonFunc(id self, SEL selector, ...)。我的想法是使用變參函數(shù)來應(yīng)對不確定的情況。定義兩個這樣的函數(shù),一個有返回值一個沒返回值就可以了,可以根據(jù)Method的typeEncoding獲取返回值情況,然后決定使用哪個。添加一個新方法指向原來的
IMP,新方法名使用一個前綴加原來的方法名(位置2)。把要插入的block和被修改的類使用
objc_setAssociatedObject綁定,并且key使用新方法。
轉(zhuǎn)發(fā)到injectedCommonFunc
經(jīng)過上面的處理,調(diào)用原方法后,實際執(zhí)行的是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循環(huán)的目的是:沿著繼承鏈向上找到和類綁定的block。因為我想設(shè)計的效果是,代碼插入效果是可以被子類繼承的,所以插入的block可能會在某個父類里,而不是和當(dāng)前調(diào)用者的class綁定。所以要追溯向上找到。
那么接下來的問題就是怎么調(diào)用這個block?
這里的關(guān)鍵問題是參數(shù)是未知的,而block只是id類型,不是變參函數(shù)。所以我借鑒了Aspects,使用NSInvocation。
- 構(gòu)建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;
這里默認(rèn)的認(rèn)知是,block的參數(shù)類型和被插入代碼的方法類型是一樣的,某則沒法搞。
獲取原方法的簽名
originSignature,因為OC方法自帶self和selector兩個參數(shù),所以實際參數(shù)從第三個開始。先把返回值類型賦值給blockTypes,然后從第三個參數(shù)開始,依次把參數(shù)類型拷貝過去。
然后由類型字符串blockTypes構(gòu)建簽名blockSignature;由簽名構(gòu)建blockInvocation
給blockInvocation設(shè)置參數(shù)
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);
使用變參函數(shù)的性質(zhì),把參數(shù)一個個取出來,但是要直到類型才能取。但是因為有*block參數(shù)類型和原方法一致的設(shè)定,那么參數(shù)類型是直到的。所以對不同的argType,調(diào)用不同的類型取值。比如:@表示對象,即id,那就調(diào)用va_arg(params, id)取值。這些對應(yīng)關(guān)系在Type Encodings里。
原方法的調(diào)用也使用NSInvocation來調(diào)用,因為發(fā)現(xiàn)也沒有辦法傳遞參數(shù)。但它和blockInvocation類型,也不必多做多少處理。
- 調(diào)用NSInvocation,拿到返回值
[blockInvocation invokeWithTarget:injectBlock];
[originalInvocation invoke];
void *returnValue = nil;
[originalInvocation getReturnValue:&returnValue];
return (__bridge id)(returnValue);
這里有個小坑:getReturnValue的結(jié)果是直接把內(nèi)存賦值給returnValue,沒有做任何內(nèi)存管理相關(guān)的操作的,相當(dāng)于沒有retain,如果你用一個__strong類型的變量去接,后面用完了會release,這樣就會堆出來一個release, 然后crash。所以先用一個__weak指針或void*指針去接,然后轉(zhuǎn)到正確類型。
轉(zhuǎn)折
一開始跑得都挺好的,直到我突然發(fā)現(xiàn)不行了,怎么會?我明明沒有修改什么東西?然后我猛地意識到似乎之前都是在模擬器上跑!-_-
關(guān)鍵點在變參函數(shù)取不到值了,而在模擬器上是可以的。
我仔細(xì)看了下變參函數(shù)獲取參數(shù)的那幾個宏:va_list,va_start,va_arg和va_end。
網(wǎng)上可以查到他們的定義,原理是依靠參數(shù)入棧的規(guī)律:參數(shù)由后往前逐個入棧,且地址從高到底一次排列。這樣只要知道了其中某個參數(shù)的位置,其他參數(shù)都可以通過類型一次找出來。
但可惜的是,經(jīng)過觀察,iOS和mac上都不是這樣的!我看到的結(jié)論是:
固定參數(shù)的位置和變參的位置是在不同的區(qū)域,并且不是緊貼這的。
固定參數(shù)的位置是一次排列的,但是是前往后,地址逐漸降低,而不是升高
-
使用
va_start(ap, param)用來定位第一個變參函數(shù)的位置,這個在模擬和真機上有區(qū)別,正是這個導(dǎo)致了整個方案的失敗。- 在模擬器上,va_start得到的位置是根據(jù)函數(shù)自身來確定的,比如你有一個固定參數(shù),那么定位的是第二個參數(shù),如果你有固定參數(shù),那么定位的就是第三個參數(shù)。
- 在真機上,va_start定位似乎是根據(jù)內(nèi)存分布來的,調(diào)用函數(shù)的時候,哪些是固定,哪些是變參就已經(jīng)確定好了,跟函數(shù)定義沒關(guān)系。
- 舉例:
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:這個方法實際是有兩個參數(shù)的,在真機上,va_start永遠(yuǎn)定位第一個參數(shù)known_xx0,因為調(diào)用的時候,轉(zhuǎn)成(NSString *(*)(id self, SEL selector, ...)類型來調(diào)用的,所有4個參數(shù)都是變參。如果改成(id self, SEL selector,id name, ...)就會是第二個參數(shù)known_xx1。
而在模擬就永遠(yuǎn)定位在第三個參數(shù),因為函數(shù)有兩個定參。 所以在模擬器上,我把一個有n個固定參數(shù)的方法的IMP指向一個變參函數(shù)
injectedCommonFunc,我還是可以去得到所有的參數(shù)值的。而在真機上,原本調(diào)用的時候就沒有變參,va_start定位就是空,取不到固定參數(shù)。
最后
最后,我想到了objc_msgsend,我們調(diào)用函數(shù)都是通過它轉(zhuǎn)發(fā),它的參數(shù)類型也是(id self, SEL selector, ...),那么它又是怎么做到把固定參數(shù)和變參都取到的?
然后就找到mikeash的一篇文章,翻譯, 原文。關(guān)于參數(shù)的部分看了下,用的匯編。
“整型數(shù)和指針參數(shù)會被傳入寄存器 %rsi, %rdi, %rdx, %rcx, %r8 和 %r9。其他類型的參數(shù)會被傳進棧(stack)中” 之類的處理,但明確的事,沒有開放的函數(shù)/接口可以用來處理這些事,即使猜到了內(nèi)部的處理,也是不穩(wěn)定的,因為沒有開放接口,那么內(nèi)部的改變就不需要對外界負(fù)責(zé)。
到此也明白了為什么要用forwardInvocation來做處理,而不是自定義的函數(shù),因為forwardInvocation自帶一個NSInvocation參數(shù),包含了原方法所有的參數(shù)信息。至于類方法的修改,使用object_getClass(self)來做調(diào)用者,因為類方法放在metaClass里,object_getClass(self)當(dāng)self本身就是Class是得到的就是它的metaClass。最后繼承鏈里只能一個類被修改,這個我沒想通為什么這么做,因為我的方案在模擬器上實驗,多個修改是沒有問題的。
所以就到此結(jié)束了,當(dāng)一次學(xué)習(xí)吧。