[重要]iOS 开发 -- 使用拦截器来取代基类

http://www.cocoachina.com/ios/20161116/18099.html

自从在百度实习开始后,习惯了把 ViewController 里面的一些通用逻辑写在一个基类,然后其它 ViewController 再继承这个基类,以前一直都认为这是一个不错的做法,但今天看了篇关于 View 层的架构文章,完全颠覆了我以前的想法,派生基类并不是最好的选择。

简单的分析下原因

  • 派生的基类会增加业务使用的成本

    1. 增加集成成本,在百度实习的时候,开发的 App 依赖于百度地图和百度导航,而且都是直接源码依赖进来的,每次编译一次都好几分钟,在添加新的页面和调试页面时,需要经常运行查看,单是编译的时间都让人无法接受了。想新建一个基于我们开发的 App 环境的 Demo,但我们所有的 ViewController 都继承于一个基类,而基类又依赖于各种样的基础库,折腾半天也搞不出这么一个 Demo.

    2. 增加学习成本,使用派生的基类时还需要我们去学习派生基类的使用

既然这种方式不是最好的选择,那当然有更好的方式去取代这种方式来实现相同的效果,下面说下通过拦截器来实现和派生基类一样的功能。

这里我使用已经造好的轮子 Aspects 来进行方法的拦截,我们来创建一个继承 NSObject 的 ViewController 的拦截器:

1834458-fa4863be18be5af0.png.jpeg

.m 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@implementation ViewControllerInterceptor
 
// 会在应用启动的时候自动被runtime调用,通过这个方法可以实现代码的注入
+ (void)load {
     [ super  load];
     [ViewControllerInterceptor sharedInstance];
}
 
// 单例
+ (instancetype)sharedInstance {
     static dispatch_once_t onceToken;
     static ViewControllerInterceptor *sharedInstance;
     dispatch_once(&onceToken, ^{
         sharedInstance = [[ViewControllerInterceptor alloc] init];
     });
     return  sharedInstance;
}
 
- (instancetype)init {
     if  ([ super  init]) {
 
     }
     return  self;
}
 
@end

实现一个单例来确保只初始化一次。因为继承 NSObject,load() 方法就会在启动时被runtime调用,通过这个方法可以实现代码的注入。所以我们把 Aspects 的拦截方法实现在 init() 方法里面:

1
2
3
4
5
6
7
8
9
10
11
- (instancetype)init {
     if  ([ super  init]) {
          // 使用 Aspects 进行方法的拦截
          // AspectOptions 三种方式选择:在原本方法前执行、在原本方法后执行、替换原本方法
         [UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<aspectinfo> aspectInfo, BOOL animated){
             UIViewController * vc = [aspectInfo instance];
             [self viewWillAppear:animated viewController:vc];
         } error:NULL];
     }
     return  self;
}</aspectinfo>

这里会监听 UIViewController 的 viewWillAppear: 方法,当 UIViewController 执行 viewWillAppear: 方法后,就会拦截到,然后执行拦截器的模拟 viewWillAppear: 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 通过这种方式可以代替原来框架中的基类,不必每个 ViewController 再去继续原框架的基类
#pragma mark - fake methods
- (void)viewWillAppear:(BOOL)animated viewController:(UIViewController *)viewController
{
     // 去做基础业务相关的内容
     if  (!viewController.isInitTheme) {
         [self ThemeDidNeedUpdateStyle];
         viewController.isInitTheme = YES;
     }
     // 其他操作......
}
 
- (void)ThemeDidNeedUpdateStyle {
     NSLog(@ "Theme did need update style" );
}

在这里,我想当的 ViewController 执行 viewWillAppear: 方法后判断是否需要初始化主题,如果已经初始化成功后就会再次执行,所有我们需要在 ViewController 添加一个标志属性,但 ViewController 是不确定的,我们并不知道当前 ViewController 是哪一个类,如果我每个 ViewController 都添加一个 isInitTheme 的标志,那就又回到派生基类上去了,这时候,就由神奇的 Category 来处理了。

我们对 UIViewControler Category 添加一个 isInitTheme 的属性:

1
2
3
4
5
@interface UIViewController (Addition)
 
@property(nonatomic, assign) BOOL isInitTheme;
 
@end

然后再通过 runtime 来动态添加一个 isInitTheme 的实例变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define KeyIsInitTheme @"KeyIsInitTheme"
 
@implementation UIViewController (Addition)
 
#pragma mark - inline property
- (BOOL)isInitTheme {
     return  objc_getAssociatedObject(self, KeyIsInitTheme);
}
 
- (void)setIsInitTheme:(BOOL)isInitTheme {
     objc_setAssociatedObject(self, KeyIsInitTheme, @(isInitTheme), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}                 
 
@end

这里我们就成功在 UIViewController 的 Category 中添加一个实例变量,然后我们就可以使用这个属性来进行判断了。

扩展一个问题,当前的代码是会拦截所有的 ViewController,如果我们想针对某些 ViewController 不拦截又需要怎么办呢?

其实很简单,同上面的 isInitTheme 属性一样,再添加一个判断是否需要进行监听的属性:

1
2
// 拦截器是否有效
@property(nonatomic, assign) BOOL disabledInterceptor;

然后一样需要通过 runtime 来实现实例变量。然后在 Aspects 拦截成功后进行判断是否需要下一步的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (instancetype)init {
     if  ([ super  init]) {
          // 使用 Aspects 进行方法的拦截
          // AspectOptions 三种方式选择:在原本方法前执行、在原本方法后执行、替换原本方法
         [UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<aspectinfo> aspectInfo, BOOL animated){
             UIViewController * vc = [aspectInfo instance];
             if  (!vc.disabledInterceptor) {
                 [self viewWillAppear:animated viewController:vc];
             }
         } error:NULL];
     }
     return  self;
}</aspectinfo>

在这里,通过拦截来取代派生的基类,这样的做法的好处是 业务代码不需要对框架的主动迎合,使得业务能够被框架感知 ,这里只拿 UIViewControler 来做例子,但不限 UIViewControler, 其它的类也是适用的。

这里介绍了通过拦截器来取代派生基类,但是在需要用继承的地方法还是需要使用继承,适当选择最优的方案才是最明智的, Demo  放在 github ,需要的可以自行下载。

github: https://github.com/dengyhgit/InterceptorDemo
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值