这几年函数式编程变得愈来愈火,Github上有很多Objective-C函数式编程的经典实现,譬如ReactiveCocoa、Mansory,将Block的特性发挥的淋漓尽致。之前一直只是仅仅在用,很少会自己实现函数式编程,借着最近有个需求,正好实践一番。
作为一个完整的App,当然少不了日志、统计等,这些代码散布的到处都是,有时候新建一个类,就忘记了加统计信息,到了上线才发现,这是件很崩溃的事情。后来看到Method Swizzling 和 AOP 实践,深受启发,于是将App内的友盟统计抽离出一个切面,将统计信息集中在一起,便于管理。由于采用hook的形式,所以也不用担心忘记给某个Controller添加统计信息。
改完之后一直工作的很好,最近重构代码的时候才发现,由于页面很多,统计很琐碎,一个类经常需要hook一堆东西,导致Hook的字典无比巨大,经常括号、花括号不知道那个对应哪个,过于庞大也导致了易读性的损失,维护起来也不方便,于是决定将这大块代码重构一次。关于Aspects和AOP,这里就不做介绍,有兴趣请转AOP(面向切面编程) & Aspects 源码解析
新的整个形式采用了类似于Mansory的形式,有两个好处,一是用惯了Mansory的人可以轻松上手,二是类似于mas_makeContain的大Block正好将对该类的所有Hook放在一起,方便管理。

将每一次Hook抽象为一个事件,我们YLEventMaker来制造事件。
我们先来看Event的定义
想要进行链式编程,就必须在方法的末尾将自己作为返回值return出去,方便下一次操作

@interface YLHookEvent : NSObject

- (YLHookEvent *(^)(SEL selector))selector;
- (YLHookEvent *(^)(id blk))block;
- (YLHookEvent *)after;
- (YLHookEvent *)instead;
- (YLHookEvent *)before;
- (YLHookEvent *)automaticRemoval;
- (YLHookEvent *)execute;
@end

这里将切入参数AspectOptions分别定义成了afterinsteadbeforeautomaticRemoval四个操作,这里定义的execute类似于Masonry里的with,加不加均可。这样在语义上形成after selector execute block,非常直观形象。

- (YLHookEvent *(^)(SEL selector))selector {
    return ^id(SEL selector) {
        self.hookSelector = selector;
        return self;
    };
}

刚开始写的时候,这种写法极其不习惯,而且不便于理解,其实可以写成另外一种比较好理解的写法如下:

typedef YLHookEvent *(^YLHookEventSelectorBlock)(SEL selector);

- (YLHookEventSelectorBlock)selector {
    return ^id(SEL selector) {
        self.hookSelector = selector;
        return self;
    };
}

这样看起来就清晰多了。selector方法返回了一个block,而这个block以YLHookEvent为返回类型、以NSString为参数类型。
使用时的效果如下:

event.selector(@selector(viewDidLoad));
//等同于
YLHookEventSelectorBlock *blk = event.selector;
blk(@"viewDidLoad");

这样以来,含义就明确多了。但是这个参数还是有点不那么优雅,之后会提到一种更加简洁的方式,先卖个关子。
下面这个方法就又嵌套一层,block方法返回了一个block,而这个block以YLHookEvent为返回类型、以id为参数类型。其实这里还看不出来哪里多了一层,在实际应用中,这个id实际又会传入一个block类型,由于Aspects对这个block进行了优化使其支持任意个参数,所以我这里也使用了id类型来维护这个特性。

- (YLHookEvent *(^)(id blk))block {
    return ^id(id blk) {
        self.handlerBlock = blk;
        return self;
    };
}

使用的时候通常是这个样子

event.block(^(id<AspectInfo> aspectInfo){
    NSLog(@"这又是个block");
});

也就是说,通过block方法,传入了一个block0,然后这个方法返回了一个block1,而block1是以block0为传入参数类型的。好吧,有点绕口,我们将上面的方法展开:

typedef void (^AspectBlock)(id<AspectInfo> aspectInfo);
typedef YLHookEvent *(^YLHookEventBlock)(id blk);
YLHookEventBlock block1 = make.after.block;
AspectBlock block0 = ^(id<AspectInfo> aspectInfo){
    NSLog(@"这是一个Block");
};
block1(block0);

这样以来就形成了我可以让一个方法里面传入一个方法的的假象,实际上还是通过传递消息进行的,因为在Obejctive-C中,Block实际有自己的isa指针,它是被当做一个对象处理的,调用的时候也是给这个block发送了一个消息。
同理,我们封装一个YLHookEventMaker来形成new新的event,然后封装一个YLHook来决定被hook的类,以及产生一个YLHookEventMaker。
剩下一个小小的瑕疵,就是这个

event.selector(@selector(viewDidLoad));

两个selector着实难看,由于经常不import类,直接使用类名来hook,就导致这里也经常会报警告。一种处理方式是将参数换成NSString类型,我最初是这么想的,后来有了更好的方式——宏。

#define YL_HELPER_0(x) #x
#define YL_HELPER_1(x) YL_HELPER_0(clang diagnostic ignored #x)
#define YL_CLANG_WARNING(x) _Pragma(YL_HELPER_1(x))

#define sel(SELECTOR)  selector(\
_Pragma("clang diagnostic push")    \
YL_CLANG_WARNING(-Wundeclared-selector) \
@selector(SELECTOR) \
_Pragma("clang diagnostic pop")\
)

上面的宏完成从sel(viewDidLoad)selector(@selector(viewDidLoad))的转换,还去掉这里的未声明的警告。
于是乎

event.selector(@selector(viewDidLoad));
//  简化成下面这样
event.sel(viewDidLoad);

如果提前import过对应的类的话,这里还能够享受提词的福利。

最终效果变成了这个样子

[[YLHook hookClassByName:@"UIViewController"] makeEvents:^(YLHookEventMaker *make) {
        make.after.sel(viewDidLoad).execute.block(^(id<AspectInfo> aspectInfo){
            NSLog(@"[%@]after viewDidLoad",[[aspectInfo instance] class]);
        });
        make.before.sel(viewWillAppear:).execute.block(^(id<AspectInfo> aspectInfo){
            NSLog(@"[%@]before viewWillAppear",[[aspectInfo instance] class]);
        });
        make.before.sel(viewDidAppear:).execute.block(^(id<AspectInfo> aspectInfo){
            NSLog(@"[%@]before viewDidAppear",[[aspectInfo instance] class]);
        });
}];

以上就是函数式编程的实践。


好了,渔(guang gao)来了。
关于Aspects完整的封装,YLHook


这样将Aspects封装完毕之后,再去AOP就显得简单多了,每个makeEvents:对应的大括号内包含所有对该类的所有hook。
和原来一样(参见Method Swizzling 和 AOP 实践中的Demo),我们新建一个Category,然后在AppDelegate里面引入这个文件然后进行setupStatistics即可。

#import "AppDelegate.h"
@interface AppDelegate (Statistics)

- (void)setupStatistics;
@end
#import "AppDelegate+Statistics.h"
#import "UMMobClick/MobClick.h"
#import "YLHook.h"
#define UMengAppKey @"UMengAppKey"
@implementation AppDelegate (Statistics)

- (void)setupStatistics {
    [self setupUMeng];
    [self setupBasicStatistics];
    [self setupMoreStatistics];
}
// 友盟等统计SDK基本设置

- (void)setupUMeng {
    UMConfigInstance.appKey = UMengAppKey;
    UMConfigInstance.channelId = @"XJTULink Beta";

    NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
    [MobClick setAppVersion:version];

    [MobClick setEncryptEnabled:YES];
#if DEBUG
    [MobClick setLogEnabled:YES];
#endif
    [MobClick startWithConfigure:UMConfigInstance];
}


// 基础页面统计,即所有ViewController均要执行,需要注意的是Aspects不支持同时对父类和子类的同一个方法进行hook

- (void)setupBasicStatistics {
    [UIViewController yl_makeEvents:^(YLHookEventMaker *make) {
        make.after.sel(viewDidAppear:).block(^(id<AspectInfo> aspectInfo) {
            NSString *className = NSStringFromClass([[aspectInfo instance] class]);
            [MobClick beginLogPageView:className];
            NSLog(@"[%@][viewWillAppear]",className);
        });
        make.after.sel(viewDidDisappear:).block(^(id<AspectInfo> aspectInfo) {
            NSString *className = NSStringFromClass([[aspectInfo instance] class]);
            [MobClick beginLogPageView:className];
            NSLog(@"[%@][viewWillDisappear]",className);
        });
    }];
}

// 设置每个具体的类要进行的统计信息

- (void)setupMoreStatistics {
    [[YLHook hookClassByName:@"MainViewController"] makeEvents:^(YLHookEventMaker *make) {
        make.after.sel(buttonOneClicked:).block(^(id<AspectInfo> aspectInfo) {
            NSLog(@"第一个Button被点击了");
        });

        make.after.sel(buttonTwoClicked:).block(^(id<AspectInfo> aspectInfo) {
            NSLog(@"第二个Button被点击了");
        });
    }];
}

这样是不是明显整洁多了呢!

实践就到此为止,有什么错误欢迎指正,有什么更好的建议也欢迎讨论。

Reference

[1] Method Swizzling 和 AOP 实践
[2] AOP(面向切面编程) & Aspects 源码解析
[3] block在美团iOS的实践