原创:知识进阶型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、Aspects 的使用
- 二、Aspects 的开发技巧
- 三、Aspects 的源码解析
一、Aspects 的使用
什么是AOP
开发中总会遇到这样的需求,需要对某一个类的所有方法进行统一的操作,例如需要统计用户在每个控制器中停留的时间,大概的实现方法有但不限于以下几种:
- 使用
category
为类添加方法,然后在每个控制器的相关方法调用; - 在控制器基类中添加相关统计的方法,然后让每个控制器继承该控制器基类;
- 使用运行时的方法
Hook
相关方法,添加自己的实现
针对上面的方法:
- 1 需要手动添加的地方太多,不易维护,而且对于大型项目来讲,手动添加调用很容易遗漏
- 2 需要额外的沟通成本,耦合严重,在一定程度上破坏了类的封装性
- 3 可以实现不修改原始类的实现无入侵式改变应用行为,相对来讲,实现简单,易于维护。
之前我们聊过使用运行时hook
方法实现的原理,我们针对的就是在需要的某一个类或实例中添加一些我们自己的实现,只针对某个切面进行Hook
操作,这个就是面向切面的概念(AOP),针对这个概念有一个非常著名的框架Aspects
。Aspects
是一个面向切面编程轻量级类库,主要用于在切面中添加或者已有实现,该类库提供了可选的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次的方法使用的,所以在可能的情况下,不要将高频率地调用Aspects
的hook
方法,不过线程是安全的,可以放心使用。
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
以及copy
和dispose
函数的指针及签名和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
操作过。