Jul 9

小记:iOS不依赖目标代码引入的情况下做方法交换

Lrdcq , 2020/07/09 01:44 , 程序 , 閱讀(1278) , Via 本站原創
背景:我们做一些辅助工具库时,为了对目标库进行最小的入侵,往往会通过方法交换来实现特定的节点完成特定的操作。这样的代码假设我们有一个Utils要入侵A,只需要U依赖A即可。但是偶尔会有这种情况,我们的Utils适配了ABCDE五个库,比如我是一个流量统计库,对这五个库的网络操作进行了抓取。但是我Utils并不适合直接依赖这五个库,而是期望用户实际引用到啥,我就统计啥。

因此,需要实现的是Utils不依赖目标库,也不依赖目标类,来实现方法交换。

讨论

我们一般实现方法交换的方法,是为目标类添加一个分类,然后对这个类的目标方法和分类新增的方法进行交换。我们知道分类其实是运行时动态加入的,因此其实它对原始类的实现可以没有依赖。因此尝试的是:假如我要为Class XXX新增分类,但是不依赖XXX怎么做呢:
@interface XXX : NSObject
@end

@implementation XXX (AddMethod)

- (void)lrd_toswitch {
    //....
}

@end

强行声明出XXX,但是不做实现。然后为XXX添加用户交换的方法——这个做法是可以编译过,并且为真正的XXX类型上新增上分类方法的。但是有一个问题——如果实际引入的库没有XXX时,编译倒是没事,link会报错——按理说既然是运行时添加的,理应没有link依赖才对,怀疑是因为是语言特性,因此llvm有相对严谨的link校验。

因此确认,正常的添加分类并且执行交换的方法走不通,得自己想办法。

方法一:手动添加分类方法,并方法交换

既然语言特性的分类加方法编译不过,我们手动在runtime时,模拟分类的操作添加方法不就好了。于是有如下代码:
const SEL __lrd_toswitch_selector_name = (SEL)"lrd_toswitch:";
const char *__lrd_toswitch_selector_type = "v24@0:8d16";

void __lrd_toswitch_selector_IMP(id self, SEL _cmd, CFTimeInterval diff) {
    //do what i add
    //call source
    [self performSelector:__lrd_toswitch_selector_name withObject:@(diff)];
}

//-----------

Class clz = NSClassFromString(@"XXX");
//判断目标交换类是否存在
if (clz) {
    //类似category操作,动态添加方法
    class_addMethod(clz, __lrd_toswitch_selector_name, (IMP)__lrd_toswitch_selector_IMP, __lrd_toswitch_selector_type);
    
    //然后正常执行方法交换
    [SWLSwizzle swizzleInstanceMethod:NSSelectorFromString(@"toswitch:") ofClass:clz withMethod:__lrd_toswitch_selector_name ofClass:clz];
}

这段代码事实上做的是:

- 散装声明了一个具体的方法,类型和实现。
- 如果XXX存在,则通过class_addMethod把这个散装的方法加入类中,这个过程其实是在模拟分类
- 然后正常的执行方法交换即可。

方法二:方法实现交换

注意到方法一我们已经开始手动编写IMP了,那意识到既然这么玩,新增一个方法在做交换已经没必要了。

所以进阶的可选方法二是直接进行实现的交换。此处利用的runtime中后面加的辅助方法imp_implementationWithBlock。
Class clz = NSClassFromString(@"XXX");
if (!clz) {
    return;
}
SEL selector = NSSelectorFromString(@"toswitch:");
Method method = class_getInstanceMethod(clz, selector);
IMP imp = method_getImplementation(method);
class_replaceMethod(clz, selector, imp_implementationWithBlock(^(id self_inner, CFTimeInterval diff) {
    //do what i add
    //call source
    ((void (*)(id, SEL, CFTimeInterval))imp)(self_inner, selector, diff);
}), method_getTypeEncoding(method));

这个方法相对于方法一运行成本上更低性能有优势,当然也更容易写坏。

补充几点:

- NSSelectorFromString其实还不如直接写char *,但是NSClassFromString会判断类型是否存在,非常有必要
- 方法typeencoding涉及到基本类型的时候,只要声明是对的,方法到实现传递的时候会自动转型,比较放心。但是直接调用实现,当然不行。
关键词:runtime
logo