黑科技 - Method Swizzling 与 Aspects

反复阅读了宇大一篇关于应用架构的文章. 不得不说宇大的这个系列文章干货十足,每次阅读都会有不同的收获, 而且也潜移默化的改变了我很多的编码风格.而且其中一些前卫的思想即时现在也不太理解. 当时对

业务方可以不用通过继承的方法,然后框架能够做到对ViewController的统一配置。
业务方即使脱离框架环境,不需要修改任何代码也能够跑完代码。业务方的ViewController一旦丢入框架环境,不需要修改任何代码,框架就能够起到它应该起的作用。

取消继承的想法很感兴趣,而Method Swizzling可以帮助我们在不改变一个类或类实例的代码的前提下,有效更改类的方法实现。最近利用空闲时间对自己的项目使用 Category + Method Swizzling(Aspects)取代了继承, 在此把之间的一些所学记录一下。

Method Swizzling原理


首先需要了解一下runtime中的相关知识,Objective-C中主要使用的是消息机制,当执行一个方法的时候内部是通过objc_msgSend()实现

1
2
3
[self loadView:baseView];
objc_msgSend(self, @selector(loadView:), baseView);

这两行代码本质上是一样的。

详细说明一下消息的传递过程,在Objective-C中,object,class,method都是一个C的结构体,在objc/objc.h头文件中可以看到定义

1
2
3
4
5
6
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

object结构体的第一个成员是isa指向自己的class。而class保存了方法列表,还有指向父类的指针。
class中的每一个方法的数据结构如下

1
2
3
4
5
6
7
struct objc_method
{
SEL method_name;
char * method_types;
IMP method_imp;
};
typedef objc_method Method;

每个方法有3个属性

  • 方法名:方法名为此方法的签名,有着相同函数名和参数名的方法有着相同的方法名。
  • 方法类型:方法类型描述了参数的类型。
  • IMP: IMP即函数指针,为方法具体实现代码块的地址,可像普通C函数调用一样使用IMP。

所以在方法列表中通过找到对应的方法。最后一步,去实现这个方法的IMP。
以上是整个方法执行过程的底层实现,最后画了一张草图。


亦菲表演机器猫

说了这么多然后回到我们的主题,Method Swizzling就是改变了交换了两个方法的实现,也就是交换了两个方法的IMP。看图:


亦菲表演机器猫

Method Swizzling基本用法

然后我们就能利用这个技巧做一些想做的事,比如要在每个页面加载的时候记录一下,就可以定义一个ViewController的 类别,然后添加将要Swizzled的方法比如viewWillAppear:利用method_exchangeImplementations来交换成我们自己想要的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+ (void)load
{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(myViewWillAppear:);
Method methodA = class_getInstanceMethod(class, originalSelector);
Method methodB = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(methodA, methodB);
}
- (void)myViewWillAppear:(BOOL)animated
{
[self myViewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}

最下面的myViewWillAppear:递归调用其实调用的是系统的viewWillAppear:,而我们自己定义的方法在调用系统的方法时已经调用了。

Aspects

Aspects是一个封装了Method Swizzling的runtime实现过程,只提供了两个对外接口。用于支持AOP(面向切面编程)模式,来解决部分OOP(面向对象)模式无法解决的特定问题。

关于aop

AOP一般都是需要有一个拦截器,然后在每一个切片运行之前和运行之后(或者任何你希望的地方),通过调用拦截器的方法来把这个jointpoint扔到外面,在外面获得这个jointpoint的时候,执行相应的代码。
在iOS开发领域,objective-C的runtime有提供了一系列的方法,能够让我们拦截到某个方法的调用,来实现拦截器的功能,这种手段我们称为Method Swizzling。Aspects通过这个手段实现了针对某个类和某个实例中方法的拦截

这是官方给出的两个接口,分别对类和对象的某个方法执行前/替换/后添加一段代码。

1
2
3
4
5
6
7
8
9
10
11
12
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
id<AspectToken> aspect = ...;
[aspect remove];

关于使用,写在一个类别里或者新写一个类,写在其中的+(void)load方法中。具体的实现很简单,但是我在实现过程中遇到了坑,比如我想在我的几个Controller中拦截viewWillAppear:.

1
2
3
4
5
6
[UIViewController aspect_hookSelector:@selector(viewWillAppear:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated){
NSString *className = NSStringFromClass([[aspectInfo instance] class]);
DLog(@"%@", className);
} error:NULL];

代码没有问题 然后打印出来呢,出现一些UIInputWindowController,UICompatibilityInputViewController等根本没有见过的Controller。而我并不想拦截他们,而且拦截之后也容易出现问题。咨询了之后得到如下思路,使用facade模式,拦截到viewcontroller之后,看这个viewcontroller的facade是不是我们需要的viewcontroller,然后再继续做拦截的事情。我自己觉得每个需要拦截的页面都要实现Protocol还是有些麻烦,虽然我得VC不是很多,所以偷了个懒,把我需要拦截的VC名称写到数组中,判断拦截到的VC的name是否在这个数组中。暂时满足了需求,不知道后续是否还会有坑。