记一次针对UIViewController的AOP尝试

记一次针对UIViewController的AOP尝试


前言

    最近在看casa大牛博客的架构系列其中的一章 iOS应用架构谈 view层的组织和调用方案。在“是否有必要让业务方统一派生ViewController”这一观点上,casa举了在阿里工做时的例子,他发现当在作Demo时搭建环境是一件很痛苦很麻烦的事情,另外当要把作好的Demo合到项目中去时须要修改各类继承关系,而且要提早去考虑接入后父类代码可能会形成的影响。casa认为统一派生没有必要,建议使用AOP的方式。
开始我表示认同,由于确实是遇到过想要搭建简单的环境可是出现各类代码耦合的问题,我不想在原来的庞大的项目上另拉分支去搞,由于每次编译都要很久;另外复制粘贴代码搭建环境的话各类黏连让人烦到死,因此我便根据casa大牛的思路进行了尝试,这篇文章就是记录了我此次尝试的过程,而且在尝试的过程也发现了一些问题并引起了一些思考。html


目录

  • AOP思想介绍以及实现方案
  • 针对于UIViewController的AOP实现
  • 发现的一些问题
  • 总结与思考

AOP思想介绍以及实现方案

1.AOP思想

先贴上百度百科上AOP的解释说明ios

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,经过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
MMP,什么鬼?? 彻底不理解。多是理解能力比较差的缘故,起初不是很理解,对于AOP我总以为是什么望尘莫及的高深思想,直到看了casa大牛写的解释才明白这种思想其实很简单。 2015-04-28 09:28补:关于AOP

我本身理解,AOP其实就是拦截一个流程中的某几个状态,并执行本身自定义操做的一种思想
在一个在程序执行1,2,3,4步的间隙跑到别处去执行你自定义的代码,这种作法就是面向切片编程。git

使用AOP的好处:假如你要在某几个步骤中间作一些操做,好比作一些埋点或者监听、统计之类的事情,不用AOP也同样能够达到目的,直接往步骤之间塞代码。可是事实状况每每很复杂,直接塞进去的代码颇有多是跟原业务无关的代码,在同一份代码文件里面掺杂多种业务,这会带来业务间耦合,有时你可能以为只是多了几句代码,可是随着后续需求的迭代,而且在团队开发中常常会存在多人修改同一份类文件,这种耦合会变得愈来愈粘合。那么为了下降这种耦合度,因此使用AOP的思想来剥离这部分代码。github

其实我以为代理模式中向外抛出的各类状态方法,这种作法很像AOP。好比UIScrollView的代理方法,使得在UIScrollView处在各个阶段时,将状态抛出给代理类去作本身想作的事,而代理类中的处理代码与UIScrollView自身的处理逻辑无关,不存在耦合,因此我我的以为这些代理方法很像AOP中的切片。objective-c

2.实现方案

使用Method Swizzing

利用Objective-C的runtime特性,能够在运行时交换方法实现,使得你能够将本身自定义的代码注入指定的方法内。编程

// 工具方法
+ (void)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector {
    Class class = self;
    
    Method originalMethod = class_getInstanceMethod(class, origSelector);
    Method swizzledMethod = class_getInstanceMethod(class, newSelector);
    
    BOOL didAddMethod = class_addMethod(class,
                                        origSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        class_replaceMethod(class,
                            newSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

// UIViewController+AOP类中:
+ (void)load {
    [UIViewController swizzleMethod:@selector(viewDidLoad) withMethod:@selector(aop_viewDidLoad)];
}

- (void)aop_viewDidLoad {
    [self aop_viewDidLoad];
    // 添加自定义的代码
    ...
}

使用Aspects

另外可使用Aspects,一个现成关于的Method Swizzing的框架。
上面的代码只须要改写成以下代码:缓存

// UIViewController+AOP类中:
+ (void)load {
    NSError *error;
    [UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        UIViewController *vc = aspectInfo.instance;
        [vc aop_viewDidLoad];
    } error:&error];
    if (error) NSLog(@"%@", error);
}

- (void)aop_viewDidLoad {
    [self aop_viewDidLoad];
    // 添加自定义的代码
    ...
}

关于Ascepts的使用比较简单,只有两个方法架构

clipboard.png

其中参数:app

selector:将要hook的方法
options:block参数的调用时机,能够设置block在原方法执行前/后执行,或者彻底取代原方法
block:注入的block代码
error:错误回调对象

关于block中对原方法入参的获取:(以UIViewController的presentViewController:animated:completion:方法为例)框架

[UIViewController aspect_hookSelector:@selector(presentViewController:animated:completion:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo) {
        UIViewController *vc    = aspectInfo.instance;
        NSArray *arguments      = aspectInfo.arguments;
        [vc aop_presentViewController:arguments[0] animated:[arguments[1] boolValue] completion:arguments[2]];
    } error:nil];

具体使用还能够参考该框架在github上的介绍:Aspects


针对UIViewController的AOP实现

1.思路

这篇文章的想法是原由于casa大牛的建议,将UIViewController派生改成AOP方式实现,因此我打算将以前写的ZCPViewController改写为UIViewController+AOP。

1.分析原来继承方式实现的ZCPViewController。通常派生会重写生命周期方法,而且会加一些自定义的方法和属性。
2.对于生命周期方法固然是直接hook,原本也就是想要作这个的;
3.对于新增的属性和方法,若是不使用继承的话,只能采用分类的形式附加到UIViewController上;
4.对于分类的使用必定要考虑好做用域,由于分类会在任何直接或间接引入头文件的地方生效。(请看做用域的相关补充)
5.作Demo测试效果

2.分析

咱们先看下派生类的功能:设置控制器的view背景颜色为白色,监听了键盘事件用于当点击view的时候收起键盘。

ZCPViewController.h
clipboard.png

ZCPViewController.m
clipboard.png
clipboard.png
clipboard.png

3.将生命周期方法拆分出来

咱们须要建立一个分类单独处理hook的生命周期方法。

UIViewController+AOP.m
clipboard.png

其中:
在load方法中使用Aspects去hook viewDidLoad和viewDidDisappear:方法;
在block中转而调用相应的aop_viewDidLoad和aop_DidDisappear:方法去处理相关逻辑。至于为何不直接在block中写处理代码,是由于大量代码堆积在load方法内,代码的可读性太差。

4.将属性和方法拆分出来

将生命周期方法拆分后,会发现出现了好多编译错误,这是由于找不到方法和属性的缘由,不要急,如今咱们把属性和方法也拆到分类中。

UIViewController+Property.h
clipboard.png

UIViewController+Property.m
clipboard.png

此处使用了runtime动态添加属性的写法,更为详细的使用方法请搜索objc_setAssociatedObject函数的使用。

UIViewController+Method.h
clipboard.png

UIViewController+Method.m
clipboard.png

最后,咱们在UIViewController+AOP头文件中引入方法分类和属性分类便可消除编译错误。

UIViewController+AOP.h
clipboard.png

5.分类做用域的考虑

因为使用ZCPViewController时,只须要导入ZCPViewController.h就可使用其暴露出来的全部公有方法和属性,所以也应当在导入UIViewController+AOP的地方可使用全部的分类方法和属性。因此UIViewController+AOP选择了在.h文件中引入UIViewController+Property和UIViewController+Method。

补充:
其实当你将分类引入项目的时候,会自动编译.m文件(也就是会自动将.m文件加入到TARGETS->Build Phases->Compile Sources中),此时在项目的任何地方,即便不导入该分类的头文件,也能够经过performSelector:等方式去调用该分类方法,且不会出错!
因此没必要太纠结分类是在.h中导入仍是.m中导入,由于其做用域在你引入项目时就是全局生效的!
另外若是你在分类中重写了该类的方法,则整个项目都会被影响。
这也是为何apple官方不建议你在分类中重写方法的缘由,由于它形成的破坏是全局的,而不是仅仅局限于你导入分类头文件的地方。

6.作Demo测试效果

经过上面的步骤,咱们成功的将ZCPViewController拆分开来,以后咱们建立控制器时能够直接继承UIViewController。

让咱们作一个小Demo来试一试成果吧?。

建立一个项目,而后在自动生成的ViewController类中加一个UITextField,预计的效果是:

1.控制台打印aop_viewDidLoad;
2.点击输入框弹出键盘,而后点击视图空白位置键盘会收起。
ViewController.m代码以下:
clipboard.png

运行起来后控制台打印:
clipboard.png

看上去很完美?,咱们再试试点击输入框:

clipboard.png

点击后:
clipboard.png

纳尼,当点击输入框的时候整个屏幕居然变白了~变白了~白了~ ?

最后在打断点找了好久以后,发现了问题出在这个地方:
clipboard.png

缘由是当键盘弹出来的时候,管理键盘的控制器也是一个UIViewController的子类,因此当在aop_viewDidLoad方法中设置view的背景颜色时,一样也会改变键盘控制器的view背景颜色。

问题已经搞明白了,解决的话须要把aop_viewDidLoad方法中设置背景颜色的代码放到ViewController的viewDidLoad中去实现,

UIViewController+AOP.m
clipboard.png

ViewController.m
clipboard.png

验证一下,发现没有问题了。
clipboard.png


发现的一些问题

问题

1.hook UIViewController的viewDidLoad方法会影响到其全部的派生类。

继承UIViewController的不只仅是本身自定义的类,UIKit框架中也有不少类继承UIViewController,此外可能还会有一些第三方框架(不多),那么在hook UIViewController的viewDidLoad方法后会通通影响到这些派生类。

2.Category影响范围较大

Xcode在将Category预编译后会影响整个项目,而不是只是在引入头文件的地方。

这个问题是在作Demo的时候无心中发现的。

针对问题2的Demo验证

Demo1

有三个控制器,继承关系为:UIViewController -> ZCPViewController -> ViewController。最后装载到window上的视图为ViewController。

下面为各个类的代码:

// ZCPViewController.h
@interface ZCPViewController : UIViewController

@end

// ZCPViewController.m
@implementation ZCPViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"ZCPViewController viewDidLoad");
}

@end
// ViewController.h
@interface ViewController : ZCPViewController

@end

// ViewController.m
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"ViewController viewDidLoad");
}

@end

另外有两个分类,分别hook了ViewController和ZCPViewController的viewDidLoad方法

// ZCPViewController+AOP.h
@implementation ZCPViewController (AOP)

+ (void)load {
    NSError *error;
    [ZCPViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        ZCPViewController *vc = aspectInfo.instance;
        [vc aop_viewDidLoad];
    } error:&error];
    if (error) NSLog(@"%@", error);
}

- (void)aop_viewDidLoad {
    NSLog(@"ZCPViewController+AOP aop_viewDidLoad");
}

@end
// ViewController+AOP.m

@implementation ViewController (AOP)

+ (void)load {
    NSError *error;
    [ViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        ViewController *vc = aspectInfo.instance;
        [vc aop_viewDidLoad];
    } error:&error];
    if (error) NSLog(@"%@", error);
}

- (void)aop_viewDidLoad {
    NSLog(@"ViewController+AOP aop_viewDidLoad");
}

@end

(注意,我在ViewController.h和.m文件中没有导入任何Category的头文件。!!)
在运行起来以后会发现控制台打印:
clipboard.png

就是说咱们不能在一个继承树上hook两个相同的方法。

而后咱们把ViewController+AOP中的hook删掉,只保留ZCPViewController+AOP的hook

// ViewController+AOP.m
@implementation ViewController (AOP)

- (void)aop_viewDidLoad {
    NSLog(@"ViewController+AOP aop_viewDidLoad");
}

@end

而后咱们跑起来,看控制台打印的信息:
clipboard.png

有没有以为很诡异,咱们明明hook了ZCPViewController的viewDidLoad方法,也没有去导入ViewController+AOP.h这个头文件,怎么会打印“ViewController+AOP aop_viewDidLoad”,按理说不是应该打印“ZCPViewController+AOP aop_viewDidLoad”吗?闹鬼了???

如今咱们来捋一下代码执行的顺序:

-> run
-> ViewController: viewDidLoad
-> ViewController: [super viewDidLoad]
-> ZCPViewController: viewDidLoad
-> ZCPViewController: NSLog(@"ZCPViewController viewDidLoad")
-> ZCPViewController+AOP: hookBlock
-> ZCPViewController+AOP: [vc aop_viewDidLoad]
-> ??
-> ViewController: NSLog(@"ViewController viewDidLoad")

如今有没有发现问题,在??上面[vc aop_viewDidLoad]这句。vc对象是ViewController实例,当在执行aop_viewDidLoad方法的时候,根据Objective-C语言继承类的方法执行顺序和其动态特性,这句代码执行后会先判断ViewController类是否能响应aop_viewDidLoad方法,若是能够响应则执行,若是不行则判断父类可否响应该方法。
这就说明了一个结果:ViewController类能够响应ViewController+AOP中写的aop_viewDidLoad方法!!

根据控制台打印的结果,猜测应该是正确的,Category中写的方法都会被响应到。

Demo2

你可能会以为不相信,那让咱们再来作一个尝试,删掉前面写的关于ZCPViewController+AOP的相关代码和NSLog代码,只保留下面的代码

// ViewController.m
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    if ([self respondsToSelector:@selector(aop_viewDidLoad)]) {
        [self performSelector:@selector(aop_viewDidLoad)];
    }
}

@end
// ViewController+AOP
@implementation ViewController (AOP)

- (void)aop_viewDidLoad {
    NSLog(@"ViewController+AOP aop_viewDidLoad");
}

@end

话很少说,看控制台打印结果吧:
clipboard.png

动态语言就是这么牛。猜测再次获得了验证。


总结与思考

1.仍是有必要统一派生
2.hook框架类的方法不肯定性太大
3.要理解Category的设计初衷,规范使用
4.AOP的应用场景

1.仍是有必要统一派生

先说一下对于本篇文章主要作的事情的思考吧,经过了上面的一番尝试对于casa大牛的用AOP方式彻底取代统一派生的想法不敢苟同。缘由有以下几点。

1.统一派生的作法是封装UIViewController,在建立新控制器的时候去继承它,将公共的任务和配置交给这个统一的父类去处理。
以ZCPViewController为例,统一派生的方式你在ZCPViewController中写的公共代码的做用范围是其全部的自定义子类。而采用AOP方式hook UIViewController的话,这些公共代码的做用范围是全部UIViewController的子类。
其实你写这些公共代码的初衷只是想要针对本身自定义的这些子类。
针对做用范围比较之下我以为使用继承更为合理一些。

ps: 后面我又想到一种解决办法,建立一个空的ZCPViewController,而后把hook UIViewController的代码都改成hook ZCPViewController。这样改写后,虽然使用了AOP方式,可是做用范围被局限在ZCPViewController和其子类中。这种想法与使用继承方式的初衷一致。

2.其实即便是使用了AOP,搭建环境仍然很麻烦,由于若是你想作一个Demo须要一部分项目环境,你仍旧须要将这些分类拷出来导入Demo中,而以前继承方式写代码的这部分耦合如今转移到分类中的代码里了。剥离起来仍是很烦。
举个例子:假设继承方式ZCPViewController的viewDidLoad方法中写了关于处理本地缓存的代码,用到了ZCPCache类,而ZCPCache又用到了其余的xxx类。如今使用AOP后,这部分代码跑到了aop_viewDidLoad中。当咱们要搭建环境时,都须要将ZCPCache相关的一系列类拷出来,仍是拔出萝卜带出泥。
其次在将Demo合并到本来的项目中去时仍旧须要考虑hook的处理代码和Demo代码之间的相互影响。

3.使用Category处理本来派生类添加的属性须要使用runtime,写起来麻烦并且比较怪异。

综上所述,我觉的仍是有必要统一派生。可是针对于这些痛点,我以为更应该从代码的结构上下手,将代码分块剥离,尽可能下降块与块之间的耦合。

2.hook框架类的方法不肯定性太大

咱们已经在上面的Demo中发现了hook框架类的问题,你在aop_viewDidLoad中设置背景颜色时不管如何也想不到会影响键盘控制器。这种地毯式轰炸的方式仍是颇有可能会误伤友军的,因为UIKit并未开源,因此没法肯定对如今框架形成的影响,另外或许之后apple会更新UIKit,对之后框架的影响也很难预测。且用且当心。

3.要理解Category的设计初衷,规范使用

Category的设计初衷是对原有类扩充一组方法,好比MGBox框架里面的UIView+MGEasyFrame类中有一组处理frame的方法,top、left、bottom、right等等。
此外尽可能不要去添加属性和重写方法。缘由在上面已经说了不少次了。

4.AOP的应用场景

那么说了这么多,AOP思想有什么应用场景呢?
其实咱们想一下,作Demo时遇问题是因为hook方法注入的代码与UIViewController自身有密切的关系(这也是为了尝试不使用派生,结果却不太好)。AOP的应用场景主要用在注入与源类无关的代码。好比像统计、埋点,或者本地存储之类的,只要是与源类无耦合或者不影响到源类便可。咱们注入的代码既然肯定与UIViewController无关不会影响到它,那么为何不开心的抽取出来呢。

总结

因此我以为在使用时,与UIViewController有关的代码写到统一派生类中,无关的代码可使用AOP抽取出来。这也是本篇文章尝试下来最后的结论。


参考文章:

iOS应用架构谈 view层的组织和调用方案
漫谈iOS AOP编程之路