前言
不知为啥, 发了几遍都找不到这篇文章
最近找工作受挫, 无头苍蝇一般, 挣扎焦虑的状态实在是难受决心改变这样的状态并且提高自己, 那就从最简单的静下心来细扣优秀源码开始吧
Aspects 简介
Aspects 是一个轻量级的面向切面编程 (AOP) 的库它主要提供了三个切入点: before(在原始的方法前执行)/instead(替换原始的方法执行)/after(在原始的方法后执行, 默认), 通过 Runtime 消息转发实现 Hook 它支持 Hook 某个实例对象的方法并且它的内部考虑到了大量的可能触发的问题并进行相应的处理来确保安全相比于单纯交换两个 IMP 的 Method Swizzling 优势还是很明显的
带着问题看源码
阅读源码前还是要自己先去试用一下, 一般在这个试用的过程当中你或多或少的都是会有一些疑问的带着这些疑问去阅读源码的时候你就可能会有一些针对性从某个具体的细节问题切入进去比单纯泛泛的看源码的效果来的好我这里抛砖引玉的提两个问题
1.Aspects 是如何 Hook 某个特定实例对象的方法的
上面这张对象的内存布局的图我想大家应该见过, 我们都知道实例对象的方法列表都是存在类对象里的, 并且类对象其实是一个单例对象那么当不同实例对象调用相同方法的时候最后找到方法其实是一样的
2.Aspects 如何 Hook 类方法
Aspects 提供了两个方法, 一个对象方法一个是类方法
- + (id<AspectToken>)aspect_hookSelector:(SEL)selector
- withOptions:(AspectOptions)options
- usingBlock:(id)block
- error:(NSError **)error;
- - (id<AspectToken>)aspect_hookSelector:(SEL)selector
- withOptions:(AspectOptions)options
- usingBlock:(id)block
- error:(NSError **)error;
看了一些文章的介绍都是说 - 号的方法是 Hook 实例方法的, + 方法是 Hook 类方法的, 但是这里有个疑问 Aspects 其实提供的是三种功能的 Hook
Hook 某个特定实例对象的某个对象方法
Hook 所有实例对象的某个对象方法
Hook 类方法
三种功能对应的是两个方法, 咋做区分呢(其实了解对象内存布局的, 应该马上能反应过来要怎么操作)
宏观粗略的感受一下代码主要逻辑结构
一般常见的源码分析的文章喜欢从某个接口切入, 从上往下的看整个代码的执行过程, 然后最后在得出一个结论或者框架图但是我感觉这样的方式对应读者来说相对是不太友好的, 有的时候读者还没对整个框架大致了解, 这时候一大推的源码贴上来读者是一脸蒙蔽的我这里先对整个框架的结构大致做一层介绍, 省略了一些细节具体的过程
通过上面这张大致的流程图, 我们知道最后的方法调用都是会走消息转发, 并且 forwardInvocation
的 IMP 已经指向了我们新写的方法, 所以最后的 before/instead/after 的逻辑都是在我们新写的方法里了
回头看疑问
看我上面大致的流程图, 然后结合自己的 runtime 的知识再回过头来看看上面的两个问题(如果对 runtime 不太熟悉的推荐霜神的博客链接 1 链接 2 链接 3 )
问题 1
从上面的流程图中我们看到, 当 Hook 实例对象的时候其实是创建了一个新的 class, 然后让当前实例对象的 isa 指向了这个新类所以和未被 Hook 的实例对象的 isa 指向的其实是两个类对象了并且原先类对象并未做任何处理
问题 2
这个问题其实就更简单了, 我们都知道其实类方法是存在 metaClass 里的, 所以想要 Hook 类对象, 拿到 metaClass 就可以了
实现细节
[xxx class] 和
object_getClass(xxx)
的差别需要注意一下
[xxx class] 当 xxx 是实例对象的时候返回的是类对象, 其它情况返回的是自己
object_getClass(xxx)
返回的是当前对象 isa 指向的对象
OK, 现在我们大致已经对整个流程有了一点点了解了接下来我们就需要去深挖一些细节了 Aspects 内部的注释还是非常全的~
1. 协议介绍
- AspectToken
- @protocol AspectToken <NSObject>
- // 注销一个 Hook
- - (BOOL)remove;
- @end
这是个协议, 内部就一个 remove 方法遵循这个协议需要实现 remove 方法去注销 Hook
- AspectInfo
- /// Hook 的 Block 的第一个参数, 遵循这个协议
- @protocol AspectInfo <NSObject>
- /// 当前 Hook 的对象
- - (id)instance;
- /// Hook 的原方法的 Invocation
- - (NSInvocation *)originalInvocation;
- /// 所有的方法参数
- - (NSArray *)arguments;
- @end
我们添加的 Hook 的 block 的第一个参数, 遵循这个协议
2. 类介绍
AspectInfo
AspectInfo 协议遵循上面 AspectInfo 协议三个属性和协议上一一对应
- @interface AspectInfo : NSObject <AspectInfo>
- - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation;
- @property (nonatomic, unsafe_unretained, readonly) id instance;
- @property (nonatomic, strong, readonly) NSArray *arguments;
- @property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
- @end
这里着重看一下 arguments 也就是方法的参数获取内部的获取逻辑是 originalInvocation 调用了 NSInvocation 分类的方法
- - (NSArray *)aspects_arguments
- - (NSArray *)aspects_arguments {
- NSMutableArray *argumentsArray = [NSMutableArray array];
- for (NSUInteger idx = 2; idx <self.methodSignature.numberOfArguments; idx++) {
- [argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null];
- }
- return [argumentsArray copy];
- }
你可以看到上面方法的逻辑很简单就是遍历 methodSignature 的 Arguments , 但是你肯定也注意到了 idx 是从 2 开始的通过查看官方文档可以看到这么一句话
A method signature consists of one or more characters for the method return type, followed by the string encodings of the implicit arguments self and _cmd, followed by zero or more explicit arguments
也就是说一个方法的签名是由
返回值 + self + _cmd + 方法参数的 encodings 值组成
但是这里方法参数是从 3 开始的, 我们接下去看到获取到具体类型是通过
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx
方法 OK, 我们又在这个方法的文档里看到了这么一句话
Indexes begin with 0. The implicit arguments self (of type id) and _cmd (of type SEL) are at indexes 0 and 1; explicit arguments begin at index 2.
我们发现 0 对应的是 self 并不是返回值所以很显然了获取参数要从 2 开始啦
总结: AspectInfo 主要是对 NSInvocation 的保存和封装
- AspectIdentifier
- @interface AspectIdentifier : NSObject
- + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- - (BOOL)invokeWithInfo:(id<AspectInfo>)info;
- @property (nonatomic, assign) SEL selector;
- @property (nonatomic, strong) id block;
- @property (nonatomic, strong) NSMethodSignature *blockSignature;
- @property (nonatomic, weak) id object;
- @property (nonatomic, assign) AspectOptions options;
- @end
从这个类的初始化方法里我们能看出来, 这个类主要是保存了 Hook 的一些信息, hook 的执行时间方法参数等等的信息这个类里需要关注的地方是怎么解析出传入 Block 的 blockSignature 主要通过下面的方法
- static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
- AspectBlockRef layout = (__bridge void *)block;
- if (!(layout->flags & AspectBlockFlagsHasSignature)) {
- NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
- AspectError(AspectErrorMissingBlockSignature, description);
- return nil;
- }
- void *desc = layout->descriptor;
- desc += 2 * sizeof(unsigned long int);
- if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
- desc += 2 * sizeof(void *);
- }
- if (!desc) {
- NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
- AspectError(AspectErrorMissingBlockSignature, description);
- return nil;
- }
- const char *signature = (*(const char **)desc);
- return [NSMethodSignature signatureWithObjCTypes:signature];
- }
在上面的方法里我们注意到了 AspectBlockRef 这么一个结构体, 定义如下
- typedef struct _AspectBlock {
- __unused Class isa;
- AspectBlockFlags flags;
- __unused int reserved;
- void (__unused *invoke)(struct _AspectBlock *block, ...);
- struct {
- unsigned long int reserved;
- unsigned long int size;
- // requires AspectBlockFlagsHasCopyDisposeHelpers
- void (*copy)(void *dst, const void *src);
- void (*dispose)(const void *);
- // requires AspectBlockFlagsHasSignature
- const char *signature;
- const char *layout;
- } *descriptor;
- // imported variables
- } *AspectBlockRef;
在回去头去理一下方法逻辑拿到 descriptor 的指针, 对照结构体中 signature 的位置偏移
2 * sizeof(unsigned long int)
的位置, 然后在判断是否包含 Copy 和 Dispose 函数(copy 函数把 Block 从栈上拷贝到堆上, dispose 函数是把堆上的函数在废弃的时候销毁掉参考霜神的博客), 包含的话再偏移 2 * sizeof(void *) 位置, 最后拿到 signature 的位置拿到 blockSignature 后续还对其进行了一下校验
- static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature * blockSignature, id object, SEL selector, NSError * *error) {
- NSCParameterAssert(blockSignature);
- NSCParameterAssert(object);
- NSCParameterAssert(selector);
- BOOL signaturesMatch = YES;
- NSMethodSignature * methodSignature = [[object class] instanceMethodSignatureForSelector: selector];
- if (blockSignature.numberOfArguments> methodSignature.numberOfArguments) {
- signaturesMatch = NO;
- } else {
- if (blockSignature.numberOfArguments> 1) {
- const char * blockType = [blockSignature getArgumentTypeAtIndex: 1];
- if (blockType[0] != '@') {
- signaturesMatch = NO;
- }
- }
- // Argument 0 is self/block, argument 1 is SEL or id<AspectInfo>. We start comparing at argument 2.
- // The block can have less arguments than the method, thats ok.
- if (signaturesMatch) {
- for (NSUInteger idx = 2; idx <blockSignature.numberOfArguments; idx++) {
- const char * methodType = [methodSignature getArgumentTypeAtIndex: idx];
- const char * blockType = [blockSignature getArgumentTypeAtIndex: idx];
- // Only compare parameter, not the optional type data.
- if (!methodType || !blockType || methodType[0] != blockType[0]) {
- signaturesMatch = NO;
- break;
- }
- }
- }
- }
- if (!signaturesMatch) {
- NSString * description = [NSString stringWithFormat: @"Block signature %@ doesn't match %@.", blockSignature, methodSignature];
- AspectError(AspectErrorIncompatibleBlockSignature, description);
- return NO;
- }
- return YES;
- }
仔细走一遍上面的逻辑, 主要是判断 block 有参数的情况下必须是
id< AspectInfo> + 原始方法的参数顺序
(参数可以不全, 但是顺序必须是对的)
总结: AspectIdentifier 是一个 Hook 的具体内容里面会包含了单个的 Hook 的具体信息, 包括执行时机, 要执行 block 所需要用到的具体信息: 包括方法签名参数等等
- AspectsContainer
- @interface AspectsContainer : NSObject
- - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- - (BOOL)removeAspect:(id)aspect;
- - (BOOL)hasAspects;
- @property (atomic, copy) NSArray *beforeAspects;
- @property (atomic, copy) NSArray *insteadAspects;
- @property (atomic, copy) NSArray *afterAspects;
- @end
总结: 这个类还是很简单的, 就是存了一些 Hook 的 AspectIdentifier
- AspectTracker
- @interface AspectTracker : NSObject
- - (id)initWithTrackedClass:(Class)trackedClass;
- @property (nonatomic, strong) Class trackedClass;
- @property (nonatomic, readonly) NSString *trackedClassName;
- @property (nonatomic, strong) NSMutableSet *selectorNames;
- @property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers;
- - (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- - (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- - (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName;
- - (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName;
- @end
总结: 这个类主要的作用是追踪每个类 Hook 的 selector 情况确保一条继承链上只有一个类 Hook 了这个方法
3. 具体流程
通过头文件看见公开的 API 就两个
- + (id<AspectToken>)aspect_hookSelector:(SEL)selector
- withOptions:(AspectOptions)options
- usingBlock:(id)block
- error:(NSError **)error {
- return aspect_add((id)self, selector, options, block, error);
- }
- /// @return A token which allows to later deregister the aspect.
- - (id<AspectToken>)aspect_hookSelector:(SEL)selector
- withOptions:(AspectOptions)options
- usingBlock:(id)block
- error:(NSError **)error {
- return aspect_add(self, selector, options, block, error);
- }
这两个 API 最后走的都是同一个方法
- static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
- NSCParameterAssert(self);
- NSCParameterAssert(selector);
- NSCParameterAssert(block);
- __block AspectIdentifier *identifier = nil;
- aspect_performLocked(^{// 锁保证线程安全
- if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {// 判断是否可以 Hook
- // 根据方法拿到 AspectIdentifier 容器
- AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
- // 根据 selector self options block 生成 AspectIdentifier
- identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
- if (identifier) {
- // 把生成 AspectIdentifier 添加进容器
- [aspectContainer addAspect:identifier withOptions:options];
- // 处理类
- aspect_prepareClassAndHookSelector(self, selector, error);
- }
- }
- });
- return identifier;
- }
这里我们只关注
aspect_prepareClassAndHookSelector
这个方法
- static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
- NSCParameterAssert(selector);
- Class klass = aspect_hookClass(self, error);
- Method targetMethod = class_getInstanceMethod(klass, selector);
- IMP targetMethodIMP = method_getImplementation(targetMethod);
- if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
- // Make a method alias for the existing method implementation, it not already copied.
- const char *typeEncoding = method_getTypeEncoding(targetMethod);
- SEL aliasSelector = aspect_aliasForSelector(selector);
- if (![klass instancesRespondToSelector:aliasSelector]) {
- __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
- NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
- }
- // We use forwardInvocation to hook in.
- class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
- AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
- }
- }
我们先忽略
Class klass = aspect_hookClass(self, error);
来看一下下面的逻辑相对还是比较直观的, 主要的操作就是将当前的 selector 指向 _objc_msgForward , 那么当调用方法的时候会跳过前面的通过 isa 查找 IMP 的流程, 直接就走消息转发了最后我们来看一下 aspect_hookClass 这个核心方法
- static Class aspect_hookClass(NSObject *self, NSError **error) {
- NSCParameterAssert(self);
- Class statedClass = self.class;
- Class baseClass = object_getClass(self);
- NSString *className = NSStringFromClass(baseClass);
- // 类对象
- if ([className hasSuffix:AspectsSubclassSuffix]) {
- return baseClass;
- // We swizzle a class object, not a single object.
- }else if (class_isMetaClass(baseClass)) {
- return aspect_swizzleClassInPlace((Class)self);
- // Probably a KVO ed class. Swizzle in place. Also swizzle meta classes in place.
- }else if (statedClass != baseClass) {
- return aspect_swizzleClassInPlace(baseClass);
- }
- // 实例对象
- const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
- Class subclass = objc_getClass(subclassName);
- if (subclass == nil) {
- subclass = objc_allocateClassPair(baseClass, subclassName, 0);
- if (subclass == nil) {
- NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
- AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
- return nil;
- }
- aspect_swizzleForwardInvocation(subclass);
- aspect_hookedGetClass(subclass, statedClass);
- aspect_hookedGetClass(object_getClass(subclass), statedClass);
- objc_registerClassPair(subclass);
- }
- object_setClass(self, subclass);
- return subclass;
- }
从方法的逻辑中我们看到
1. 类对象
调用的是
aspect_swizzleClassInPlace
方法, 这个方法主要的操作是
class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
也就是将消息转发的方法替换成我们自己的方法
__ASPECTS_ARE_BEING_CALLED__
2. 实例对象
如果你熟悉 KVO 的底层实现的话, 你一定知道 isa 混写, 也就是我们偷偷摸摸生成了一个新的类对象, 然后我们对这个类对象做了和 1 相同的操作, 并且我们 Hook 了 class 方法让外面看起来我们好像并没有做这个操作最后我们将实例对象的 isa 指针指向了这个对象你感兴趣的话, 可以照着这个逻辑尝试下自己实现 KVO
最后理一下, 当我们调用方法的时候就会直接走消息转发, 消息转发的 forwardInvocation 已经替换成了我们的
__ASPECTS_ARE_BEING_CALLED__
所以最后的具体执行逻辑就走这个方法里面了
最后
我这里只是对 Aspects 做了一个很浅的介绍, 希望能对大家有所帮助, 也请大家多多指教~
来源: https://www.thinksaas.cn/group/topic/839010/