美文网首页iOS
IOS框架使用:Aspects

IOS框架使用:Aspects

作者: 时光啊混蛋_97boy | 来源:发表于2022-02-11 18:55 被阅读0次

    原创:知识进阶型文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 一、Aspects 的使用
    • 二、Aspects 的开发技巧
    • 三、Aspects 的源码解析

    一、Aspects 的使用

    什么是AOP

    开发中总会遇到这样的需求,需要对某一个类的所有方法进行统一的操作,例如需要统计用户在每个控制器中停留的时间,大概的实现方法有但不限于以下几种:

    • 使用category为类添加方法,然后在每个控制器的相关方法调用;
    • 在控制器基类中添加相关统计的方法,然后让每个控制器继承该控制器基类;
    • 使用运行时的方法Hook相关方法,添加自己的实现

    针对上面的方法:

    • 1 需要手动添加的地方太多,不易维护,而且对于大型项目来讲,手动添加调用很容易遗漏
    • 2 需要额外的沟通成本,耦合严重,在一定程度上破坏了类的封装性
    • 3 可以实现不修改原始类的实现无入侵式改变应用行为,相对来讲,实现简单,易于维护。

    之前我们聊过使用运行时hook方法实现的原理,我们针对的就是在需要的某一个类或实例中添加一些我们自己的实现,只针对某个切面进行Hook操作,这个就是面向切面的概念(AOP),针对这个概念有一个非常著名的框架AspectsAspects是一个面向切面编程轻量级类库,主要用于在切面中添加或者已有实现,该类库提供了可选的options选项,来确定执行自定义实现的时机。


    Aspects如何使用

    这个类库的api的也非常简单,只有两个主要的方法,拥有相同的api,只是一个是类方法,一个是实例方法。

    /**
     全局替换某个类所有的方法实现
     @param selector 原始方法的sel
     @param options 执行block时机选项,可以在原始方法执行前,执行后,或者进行替换
     @param block 需要注入的方法执行
     @param error 如果出现异常,则该值不为空
     @return 返回服从AspectToken协议的对象,可以进行移除等操作
     */
    + (id<AspectToken>)aspect_hookSelector:(SEL)selector
                          withOptions:(AspectOptions)options
                           usingBlock:(id)block
                                error:(NSError **)error {
        return aspect_add((id)self, selector, options, block, error);
    }
    

    需要注意的是,block的方法实现默认第一个参数为block对象自己,而OC的方法默认第一个参数为当前的对象,第二个参数为当前调用方法的SEL,这样的话OC的方法实现转化为block时就会少一个SEL参数,所以在Aspects中作者添加了一个很有意思的操作,将原始实现的相关信息进行封装(服从AspectInfo协议的对象),当block携带参数时,将id<AspectInfo>通过block调用进行返回。

    这么做的好处:使OC方法对应的block实现具有相同数量的参数,在进行方法参数匹配和调用赋值等需要遍历匹配参数时非常便利;可以在需要的时候调用原始的方法实现,尤其是option选项是AspectPositionInstead时,可以根据自己的需要选择执行原始实现的时机。

    所以当定义Block时可以根据自己的需要来选择是否显式携带参数。如果在自定义的Block的实现中不需要原始的实现信息,比如只需要一个时机来做事件统计),则可以将Block定义为不显式携带参数实现。

    void(^block)(void) = ^void(void){
        //your code here!
     };
    

    而更多的时候在自定义信息中需要原始实现的信息,例如:

    • option选项是AspectPositionInstead时,需要根据需求在执行自定义实现之前/只后执行原始操作;
    • 需要用到原始的对象的相关信息,例如统计时需要用到控制器的名字信息等;
    • 在某种情况下,是否执行原始操作以及执行原始实现的时机不确定,需要根据特定条件进行判断;

    这种情况下就需要将原始的实现信息通过Block进行传递:

    // 只需要原始实现的部分信息
    void(^)(id<AspectInfo> info) = ^(id<AspectInfo> info){
        //your code here
    };
    
    // 需要原始实现的完整参数
    void(id<AspectInfo> info,...) = ^(id<AspectInfo> info,...){
        //your code here!
    };
    

    在实现需求统计每个控制器的展示时,就可以通过:

    void(^block)(id<AspectInfo>) = ^(id<AspectInfo> info){
        [TrackingManager screenView:NSSttringFromClass([info.instance class])];
    };
    // 或者
    void(^block)(id<AspectInfo>, BOOL) = ^(id<AspectInfo> info, BOOL animated){
        [TrackingManager screenView:NSSttringFromClass([info.instance class])];
    };
    

    block中可以接收AspectInfo协议的对象,用以获取到当前被hook的实例对象,参数列表,以及对原始方法进行封装的NSInvocation对象等信息,可以对当前对象进行相关操作。

    [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:(AspectPositionBefore) usingBlock:block error:&error];
    

    Aspects是否可以hook类方法

    在之前的文章中,我们谈论过OC中类与元类之间的关系,其中有聊到这样的知识点:实例方法其实并不保存在实例对象中,而是保存在类的结构中,而类方法并不保存在类中,而是保存在类的元类中。这样的设计,使得同一类的实例对象没有必要都保存一份实例方法的备份,使用时只需要去类的结构中获取方法实现,并传入实例对象的参数即可,极大节约了内存空间,同时使得方法查找回溯更有效率,所以如果想要hook类方法,就要去对应的元类中进行hook

    定义一个需要hook的类方法:

    @interface Person : NSObject
    + (NSString *)combineDescription:(NSString *)str;
    @end
    
    @implementation Person
    + (NSString *)combineDescription:(NSString *)str {
        return @"I like China!";
    }
    @end
    

    同样在应用启动时,进行hook

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        NSError *error = nil;
        [metalClass aspect_hookSelector:@selector(combineDescription:) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){
            NSLog(@"info == %@", info.arguments);
        } error:&error];
        [Person combineDescription:@" Here "];
        return YES;
    }
    

    可以使用object_getClass方法传入对应的类,也可以使用objc_getMataClass传入对应类的c字符串:

    id metalClass =  object_getClass([Person class]);
    // id metalClass = objc_getMetaClass(@"Person".UTF8String);
    

    AOP主要有哪些应用场景

    AOP在开发中是一个非常重要的思想,我们希望将需求分离到非业务逻辑的方法中,尽可能的不影响业务逻辑的代码。主要的应用场景大概有以下几种:

    • 参数校验:网络请求前的参数校验,返回数据的格式校验等等;
    • 无痕埋点:统一处理埋点,降低代码耦合度;
    • 页面统计:帮助统计页面访问量;
    • 事务处理:拦截指定事件,添加触发事件;
    • 异常处理:发生异常时使用面向切面的方式进行处理;
    • 热修复:AOP可以让我们在某方法执行前后或者直接替换为另一段代码,我们可以根据这个思路,实现bug修复

    使用Aspects需要注意的问题
    性能问题

    Aspects利用的OC的消息转发机制去hook消息,会有额外的系统开销,不要尝试把Aspects添加到高频率调用的方法中去,Aspects设计用来hook view/controller方法,而不是给那些一秒调用1000次的方法使用的,所以在可能的情况下,不要将高频率地调用Aspectshook方法,不过线程是安全的,可以放心使用。

    Aspects是不是可以hook全部的方法

    并不是,Aspects有一个sel的黑名单,要求forwardInvocation:不能被hook,这是因为Aspects主要就是使用了objc_msgForward来实现的。

    disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
    
    如果我需要hook类中的dealloc方法,有什么注意点

    如果需要hook类中的dealloc方法,则有一个非常重要的点需要留意,那就是AspectOptions参数只能选择AspectPositionBefore,这个应该不用解释了吧。

    多次调用Aspects hook同一个方法,会重复hook执行多次吗

    答案不是,Aspects里有一个全局的字典存储hook过的方法,所以同一个方法不会hook多次。


    二、Aspects 的开发技巧

    如何获取block签名

    block在OC中也是一种特殊的对象,而如何获取block的签名,原始的结构体定义给了我们一个很好的思路。在Aspects中,将原始block的定义进行了自定义(因为原始定义私有)。

    • isa指针:这跟objc_object很像,所以block在多数情况下被认为是对象;
    • flags:就是上面那几个枚举,用来保留block的一些信息,比如是否含有签名信息,是否捕获外部变量等
    • reserved:保留信息
    • invoke:指向函数实现的指针
    • description:block的附加描述信息,主要保存了内存size以及copydispose函数的指针及签名和layout等信息
    typedef struct _AspectBlock {
        __unused Class isa;
        AspectBlockFlags flags;
        __unused int reserved;
        void (__unused *invoke)(struct _AspectBlock *block, ...);
        struct {
            unsigned long int size;
            void (*copy)(void *dst, const void *src);
            void (*dispose)(const void *);
            const char *signature;
            const char *layout;
        } *descriptor;
    } *AspectBlockRef;
    

    这样就可以将我们定义的block强制对齐转化为结构体,我们就可以获取到结构体中的相关变量,在Aspects中主要是为了获取block的方法签名,利用这个结构体,我们还可以做一些好玩的事情,比如我们可以使用这个结构体像方法调用一样运行block

    void(^block)(void) = ^(void){
        NSLog(@"I like China!");
    };
    AspectBlockRef layout = (__bridge void *)block;
    layout->invoke(layout);
    

    这样看block就更加像是一个真实的对象,与objc_msgSend调用类似,只不过与普通对象不同的是:普通对象实现函数有两个默认参数(id, SEL),而block的实现函数默认只有一个block对象自己。Block虽然很强大,但是当我们并不知道block的内部实现时,如果想要知道一个block的详细信息(例如block需要几个参数以及返回值类型)时就会比较麻烦。


    如何能让一个类的某个对象方法重定向不影响到其他对象的实现?

    使用运行时进行方法替换或者重定向用的好是一个神器,用不好就很容易尴尬,由于进行替换操作默认都是全局的,稍不留意就出现一些你意想不到的问题,由于这类问题在运行时才发挥作用,所以很难排查。Aspects在处理对象的方法重定向时,就使用了KVO的思路,使用给当前对象创建中间类的思想,将方法替换的影响范围限定在指定的某些对象中,同时将中间类的class方法和isa指针指向原始的类隐藏这个类的存在,从而减少了影响。


    如何想要拦截或者重定向方法实现,选择什么样的时机比较合适?

    Aspects中,选择了使用将需要拦截的方法实现指向objc_msgForward,使用自定的方法指针指向原始的方法实现,同时拦截原始forwardInvocation:的实现来完成自定义操作,这样的时机具有以下好处:

    • 该方法对源代码的入侵性小,毕竟forwardInvocation:这个方法只有在消息转发时才会调用;
    • 方便加入自定义实现:可以灵活地添加自定义的代码实现;
    • 可以方便地拿到原始方法的签名信息,并在需要的时候调用原始方法的实现;

    在hook方法时,如何保证不会重复hook?

    Aspects中,对需要hook方法的对象进行了区分处理,如果对于全局的所有的方法都需要拦截,就使用注册全局集合的方式保存对应的类名,同时在继承链上标记被hook过的方法来防止重复;如果只是需要hook某个对象的方法,那就可以通过创建中间类的方法,类通过类名确定是否已经hook操作过。

    相关文章

      网友评论

        本文标题:IOS框架使用:Aspects

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