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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270