前言:
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。而且需要调用NSInvocation
的retainArguments
方法,主动让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__
的问题也让我困惑了好久。现在写出这篇文章了似乎也并不是现象中的那么复杂😅。
最后,如果此文章对你有帮助,希望给个❤️。有什么问题欢迎在评论区探讨