iOS 面向切面(AOP)编程 —— Aspects & BlockHook

前言:

Aspect Oriented Programming (AOP,面向切面编程) 在 Objective-C 社区内没有那么有名,但是 AOP 在运行时可以有巨大威力。 但是因为没有事实上的标准,Apple 也没有开箱即用的提供,也显得不重要,开发者都不怎么考虑它。

—— 引用自禅与 Objective-C 编程艺术

但在实际项目中有时需要集成统计SDK,比如 Google Analytics, Flurry, MatomoTracker, 等等。一般情况下是直接将统计代码写到对应的地方,比如需要统计某个界面的展示次数会将代码写在viewDidAppear:方法内,这就造成了很大的入侵性,并且view controller里的代码将变糟糕起来。这时候就需要通过使用AOP将统计代码单独分离出来,这样view controller不会被其它代码污染,并且单独分离出来以后扩展或者更换其它统计SDK会方便很多。

在对类的特点方法进行切面可以使用Aspects,但是在一些特殊情况下统计代码需要写在block的回调内,这时就需要用上BlockHook,比如需要在某个网络请求成功的block回调内,这时候就需要Aspects & BlockHook配合使用。本文针对Aspects & BlockHook将分成两个部分来讲,主要讲如何使用和使用中遇到的坑。

Aspects:

Aspects一个基于runtime的轻量级AOP开源框架,作者Peter Steinberger
,主要是对方法进行Hook,该框架简单易用,源码不到千行却非常健全,考虑到了很多关于Hook方面的安全问题。

基本用法:

Aspects暴露了两个方法(方法名一样),分别对应类方法和实例方法,下面为使用示例:

SEL selektor = NSSelectorFromString(@"loginWithAccount:password:block:");
Class clazz = objc_getMetaClass(@"HYLoginNetwork".UTF8String);//类方法
//Class clazz = NSClassFromString(@"HYLoginNetwork");//实例方法
[clazz aspect_hookSelector:selektor
               withOptions:AspectPositionAfter//在Hook方法 执行完成之后 执行usingBlock里的代码
                usingBlock:^(id<AspectInfo> aspectInfo, NSString *account, NSString *password, id block) {
                //需要执行的代码...
                }
                     error:nil];

通过字符串的方式创建selektor方法名和clazz对象,这样可以减少过多的引入头文件,输入错误的方法名或对象名时会输出错误日志。方法返回的AspectToken对象可以通过remove方法取消Hook。AspectOptions代表何时执行usingBlock的代码。usingBlock的参数是动态参数,除了第一个参数aspectInfo是固定的外,其它参数是Hook的方法对应的参数(按顺序排列)。

AspectPositions:

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// 在原始实现后调用(默认)
    AspectPositionInstead = 1,            /// 将替换原始实现。
    AspectPositionBefore  = 2,            /// 在原始实现之前调用。
    
    AspectOptionAutomaticRemoval = 1 << 3 /// 执行一次后移除Hook
};

AspectInfo:

/// AspectInfo协议是usingBlock的第一个参数。
@protocol AspectInfo <NSObject>
- (id)instance; /// 当前Hook的实例。

- (NSInvocation *)originalInvocation;/// 被 Hook 方法的原始 invocation

- (NSArray *)arguments;/// 被 Hook 方法的所有参数装箱。 这是懒惰的(懒加载的)。
@end

方法有返回值?获取返回值:

    id returnValue;
    [aspectInfo.originalInvocation getReturnValue:&returnValue];

BlockHook:

BlockHook是由杨萧玉编写并开源的框架,基于 libffi 实现了对 Objective-C Block 的 hook。

基本用法:
[clazz aspect_hookSelector:selektor
               withOptions:AspectPositionBefore //当block是__NSStackBlock__类型的情况下要在这个方法执行前(AspectPositionBefore)copy到堆上
                usingBlock:^(id<AspectInfo> aspectInfo) {
                        
                    __unsafe_unretained id block = [self getLastArgument:aspectInfo];
                    [block block_hookWithMode:BlockHookModeAfter//在block执行完之后调用
                                   usingBlock:^(BHToken *token, NSInteger code){
                                       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                                                      ^{
                                                          //需要执行的代码...
                                                      });
                                           
                                   }];
                        
                }
                                       error:nil];

BlockHookMode:

typedef NS_ENUM(NSUInteger, BlockHookMode) {
    BlockHookModeAfter,      /// 在原始实现后调用
    BlockHookModeInstead,    /// 将替换原始实现
    BlockHookModeBefore,     /// 在原始实现之前调用
    BlockHookModeDead,       /// 在block销毁之后调用
};

BlockHook的API是参照Aspects写的,所以懂得Aspects的一看就懂。和Aspects一样,方法返回的BHToken对象可以通过remove方法取消Hook。BlockHookMode代表何时执行usingBlock的代码。usingBlock的参数是动态参数,除了第一个参数BHToken是固定的外,其它参数是Hook的Block对应的参数(按顺序排列)。

但是需要注意的是,当block是__NSStackBlock__类型的情况下要在这个方法执行前(AspectPositionBefore)让系统把Block copy,否则Hook不到这个Block。而且需要调用NSInvocationretainArguments方法,主动让NSInvocation把Block copy到堆上,否则从NSInvocation获取的__NSStackBlock__类型block不会销毁。

-(id)getLastArgument:(id<AspectInfo>)aspectInfo{
    [aspectInfo.originalInvocation retainArguments];
    __unsafe_unretained id block;
    //取最后一个参数(网络请求成功的blcok)
    NSInteger index = aspectInfo.originalInvocation.methodSignature.numberOfArguments - 1;
    [aspectInfo.originalInvocation getArgument:&block atIndex:index];
    return block;
}

参考资料:

面向切面编程之 Aspects 源码解析及应用
从 Aspects 源码中我学到了什么?
iOS 如何实现Aspect Oriented Programming (上)
Hook Objective-C Block with Libffi

写在最后:

原文:https://www.hlzhy.com/?p=109

当初为了让BlockHook配合Aspects可没少折腾啊,集成libffi.a问题(现在作者直接把libffi.a和相关头文件集成在项目里了),__NSStackBlock__的问题也让我困惑了好久。现在写出这篇文章了似乎也并不是现象中的那么复杂😅。
最后,如果此文章对你有帮助,希望给个❤️。有什么问题欢迎在评论区探讨

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据