美文网首页iOS学习开发iOS程序猿iOS学习笔记
iOS 面向切面(AOP)编程 —— Aspects &

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

作者: Hank_Zhong | 来源:发表于2019-04-23 14:58 被阅读5次

    前言:

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

    相关文章

      网友评论

        本文标题:iOS 面向切面(AOP)编程 —— Aspects &

        本文链接:https://www.haomeiwen.com/subject/yhvcwqtx.html