首页 » 技术分享 » Aspects框架------使用

Aspects框架------使用

 
文章目录

目录

1.1 什么是AOP?

1.2 Aspects如何使用?

1.3 虽然不想说,但是总有同学会问,为啥我hook类的方法就不会成功呢?难道这个著名的框架就只能hook实例方法吗?

1.4 使用Aspects需要注意的问题

 

1.1 什么是AOP?

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

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

针对上面的方法,1)需要手动添加的地方太多,不易维护,而且对于大型项目来讲,手动添加调用很容易遗漏;2)需要额外的沟通成本,耦合严重,在一定程度上破坏了类的封装性;3)可以实现不修改原始类的实现无入侵式改变应用行为,相对来讲,实现简单,易于维护。之前我们聊过使用运行时hook方法实现的原理,我们针对的就是在需要的某一个类或实例中添加一些我们自己的实现,只针对某个切面进行Hook操作,这个就是面向切面的概念(AOP,aspect-oriented programming),针对这个概念有一个非常著名的框架Aspects.

Aspects是一个面向切面编程轻量级类库,主要用于在切面中添加或者已有实现,该类库提供了可选的options选项,来确定执行自定义实现的时机.

1.2 Aspects如何使用?

这个类库的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);
}
/**
 替换某个类对象的方法实现
 
 @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(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])];
};

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

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

1.3  Aspects是否可以hook类方法?

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

@interface Person : NSObject
/**
 定义一个需要hook的类方法
 @param str 参数
 @return 返回值
 */
+ (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;
    //可以使用object_getClass方法传入对应的类
    id metalClass =  object_getClass([Person class]);
    //也可以使用objc_getMataClass传入对应类的c字符串
    //    id metalClass = objc_getMetaClass(@"Person".UTF8String);
    [metalClass aspect_hookSelector:@selector(combineDescription:) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){
        NSLog(@"info == %@", info.arguments);
    } error:&error];
    [Person combineDescription:@" Here "];
    // Override point for customization after application launch.
    return YES;
}

1.4  AOP主要有哪些应用场景?

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

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

1.5 使用Aspects需要注意的问题

1.5.1 性能问题

在Aspects中我们找到了这样一段话:

/**
 Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second.

 Adding aspects returns an opaque token which can be used to deregister again. All calls are thread safe.
 */

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

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

并不是,Aspects有一个sel的"黑名单",要求

disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];

所以forwardInvocation:不能被hook,这是因为Aspects主要就是使用了objc_msgForward来实现的,在下一节中我们会聊到.

1.5.3 如果我需要hook类中的dealloc方法,有什么注意点?

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

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

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

1.5.5  在x86-64模拟器上使用时可能会出现闪退问题?

Aspects中关于将需要hook方法的实现实现指向_objc_msgForward还是_objc_msgForward_stret的处理有瑕疵,导致在64位处理器的模拟器上会出现闪退问题.详细原因见 Aspects(三) 3.5.

了解了Aspects的基本使用,下一次我们将探索关于Aspects的源码实现.

转载自原文链接, 如需删除请联系管理员。

原文链接:Aspects框架------使用,转载请注明来源!

0