signature=1e781a1658e368bb25d0be29823d232e,Aspects源码解析
A delightful, simple library for aspect oriented programming
关键字:面向切片编程、OC动态性、消息转发、类型编码、Swizzle...
使用场景:
1.统一处理逻辑
2.在不改变源码的情况下,插入代码(如无侵染更改第三方库代码,干一些坏坏的事情)
Aspects只有一个类文件,非常轻量级,在实现的思路上和JSPatch差不多。都主要用到OC的消息转发,最终都交给ForwardInvocation实现。二者很多地方有异曲同工之妙。
基本原理
我们知道 OC 是动态语言,我们执行一个函数的时候,其实是在发一条消息:[receiver message],这个过程就是根据 message 生成 selector,然后根据 selector 寻找指向函数具体实现的指针IMP,然后找到真正的函数执行逻辑。这种处理流程给我们提供了动态性的可能,试想一下,如果在运行时,动态的改变了 selector 和 IMP 的对应关系,那么就能使得原来的[receiver message]进入到新的函数实现了。
还是先来普及一下:
OC上,每个类都是这样一个结构体:
struct objc_class {
struct objc_class * isa;
const char *name;
….
struct objc_method_list **methodLists; /*方法链表*/
};
其中 methodList 方法链表里存储的是 Method类型:
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name;
char *method_types;
IMP method_imp;
};
Method 保存了一个方法的全部信息,包括 SEL 方法名,type各参数和返回值类型,IMP该方法具体实现的函数指针。
通过 Selector 调用方法时,会从methodList 链表里找到对应Method进行调用,这个 methodList上的的元素是可以动态替换的,可以把某个Selector对应的函数指针IMP替换成新的,也可以拿到已有的某个 Selector 对应的函数指针IMP,让另一个Selector 跟它对应,Runtime提供了一些接口做这些事。
比如:
static void viewDidLoadIMP (id slf, SEL sel) {
// Custom Code
}
Class cls = NSClassFromString(@"UIViewController");
SEL selector = @selector(viewDidLoad);
Method method = class_getInstanceMethod(cls, selector);
//获得viewDidLoad方法的函数指针
IMP imp = method_getImplementation(method)
//获得viewDidLoad方法的参数类型
char *typeDescription = (char *)method_getTypeEncoding(method);
//新增一个ORIGViewDidLoad方法,指向原来的viewDidLoad实现
class_addMethod(cls, @selector(ORIGViewDidLoad), imp, typeDescription);
//把viewDidLoad IMP指向自定义新的实现
class_replaceMethod(cls, selector, viewDidLoadIMP, typeDescription);
这样就把 UIViewController 的 -viewDidLoad方法给替换成我们自定义的方法,APP里调用 UIViewController 的 viewDidLoad 方法都会去到上述 viewDidLoadIMP 函数里,在这个新的IMP函数里调用新增的方法,就实现了替换viewDidLoad 方法,同时为 UIViewController新增了个方法 -ORIGViewDidLoad指向原来viewDidLoad 的IMP, 可以通过这个方法调用到原来的实现。
.Aspect要的是实现一个通用的IMP,任意方法任意参数都可以通过这个IMP中转。上面讲的都是针对某一个方法的替换,但如果这个方法有参数,怎样把参数值传给我们新的 IMP 函数呢?例如 UIViewController 的 -viewDidAppear:方法,调用者会传一个 Bool值,我们需要在自己实现的IMP(上述的viewDidLoadIMP)上拿到这个值,怎样能拿到?如果只是针对一个方法写IMP,是可以直接拿到这个参数值的。如何达到通用的效果呢?
如何实现方法替换
va_list实现(一次取出方法的参数)
这段代码摘至JSPatch:
static void commonIMP(id slf, ...)
va_list args;
va_start(args, slf);
NSMutableArray *list = [[NSMutableArray alloc] init];
NSMethodSignature *methodSignature = [cls instanceMethodSignatureForSelector:selector];
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
id obj;
for (NSUInteger i = 2; i < numberOfArguments; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
switch(argumentType[0]) {
case 'i':
obj = @(va_arg(args, int));
break;
case 'B':
obj = @(va_arg(args, BOOL));
break;
case 'f':
case 'd':
obj = @(va_arg(args, double));
break;
…… //其他数值类型
default: {
obj = va_arg(args, id);
break;
}
}
[list addObject:obj];
}
va_end(args);
[function callWithArguments:list];
}
这样无论方法参数是什么,有多少个,都可以通过va_list的一组方法一个个取出来,组成 NSArray 。很完美地解决了参数的问题,一直运行正常,但是在arm64 下 va_list 的结构改变了,导致无法上述这样取参数。
所以需要找到另一种方法。
ForwardInvocation实现
看图说话
从上面我们可以发现,在发消息的时候,如果 selector 有对应的 IMP ,则直接执行,如果没有,oc给我们提供了几个可供补救的机会,依次有 resolveInstanceMethod 、forwardingTargetForSelector、forwardInvocation。
Aspects之所以选择在 forwardInvocation 这里处理是因为,这几个阶段特性都不太一样:
resolvedInstanceMethod: 适合给类/对象动态添加一个相应的实现,
forwardingTargetForSelector:适合将消息转发给其他对象处理,
forwardInvocation: 是里面最灵活,最能符合需求的。
因此 Aspects的方案就是,对于待 hook 的 selector,将其指向 objc_msgForward / _objc_msgForward_stret ,同时生成一个新的 aliasSelector 指向原来的 IMP,并且 hook住 forwardInvocation函数,通过forwardInvocation调用到原来的IMP。
核心原理:按照上面的思路,当被 hook 的 selector 被执行的时候,首先根据 selector找到了 objc_msgForward / _objc_msgForward_stret ,而这个会触发消息转发,从而进入 forwardInvocation。同时由于forwardInvocation 的指向也被修改了,因此会转入新的 forwardInvocation函数,在里面执行需要嵌入的附加代码,完成之后,再转回原来的 IMP。
大致流程如下:
摘至
-forwardInvocation:方法的实现给替换掉了,如果程序里真有用到这个方法对消息进行转发,原来的逻辑怎么办?首先我们在替换 -forwardInvocation:方法前会新建一个方法 -ORIGforwardInvocation:,保存原来的实现IMP,在新的 -forwardInvocation:实现里做了个判断,如果转发的方法是我们想改写的,就走我们的逻辑,若不是,就调 -ORIGforwardInvocation:走原来的流程。
将了这么多可能有些饶。Talk is sheap,show me the code
源码分析
从头文件中可以看到使用aspects有两种使用方式:
1.类方法
2.实例方法
/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
两者的主要原理基本差不多.
先来看看有哪些定义:
AspectOptions
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter = 0, /// Called after the original implementation (default)
AspectPositionInstead = 1, /// Will replace the original implementation.
AspectPositionBefore = 2, /// Called before the original implementation.
AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};
定义切片的调用时机
AspectErrorCode
typedef NS_ENUM(NSUInteger, AspectErrorCode) {
AspectErrorSelectorBlacklisted, /// Selectors like release, retain, autorelease are blacklisted.
AspectErrorDoesNotRespondToSelector, /// Selector could not be found.
AspectErrorSelectorDeallocPosition, /// When hooking dealloc, only AspectPositionBefore is allowed.
AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed.
AspectErrorFailedToAllocateClassPair, /// The runtime failed creating a class pair.
AspectErrorMissingBlockSignature, /// The block misses compile time signature info and can't be called.
AspectErrorIncompatibleBlockSignature, /// The block signature does not match the method or is too large.
AspectErrorRemoveObjectAlreadyDeallocated = 100 /// (for removing) The object hooked is already deallocated.
};
这里定义了在执行的时候的错误码,在平时开发中我们也经常使用这种方式,尤其是在定义网络请求的时候。
AspectsContainer
// Tracks all aspects for an object/class.
@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
一个对象或者类的所有的 Aspects 整体情况,注意这里数组是通过atomic修饰的。
关于atomic需要注意在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备nonatomic特质,则不需要同步锁。
注意一共有两中容器,一个是对象的切片,一个是类的切片。
AspectIdentifier
// Tracks a single aspect.
@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id)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
一个Aspect的具体内容。主要包含了单个的 aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等。其实就是将我们传入的bloc,包装成AspectIdentifier,便于后续使用。通过我们替换的block实例化。也就是将我们传入的block,包装成了AspectIdentifier
AspectInfo
@interface AspectInfo : NSObject
- (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
主要是 NSInvocation 信息。将NSInvocation包装一层,比如参数信息等。便于直接使用。
AspectTracker
@interface AspectTracker : NSObject
- (id)initWithTrackedClass:(Class)trackedClass parent:(AspectTracker *)parent;
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@property (nonatomic, weak) AspectTracker *parentEntry;
@end
用于跟踪所改变的类,打上标记,用于替换类方法,防止重复替换类方法。
流程
读源码是一件辛苦的事情。
signature=1e781a1658e368bb25d0be29823d232e,Aspects源码解析相关推荐
- 消息转发机制与Aspects源码解析
前言 最近在搞重构相关的事情,遇到了不少这样的场景: 进入一个界面,在viewWillAppear:的时候做相应判断,如果满足条件则执行对应代码. 这类业务有一个特点,业务内容是对应整个App的,与对 ...
- Aspects源码解析
Aspects使用方法 Aspects源码只有两个文件:Aspects.h和Aspects.m文件.使用的方式就是对NSObject添加了一个Category,其中有两个方法分别为类和对象添加切面bl ...
- Spring源码深度解析(郝佳)-学习-源码解析-创建AOP静态代理实现(八)
继上一篇博客,我们继续来分析下面示例的 Spring 静态代理源码实现. 静态 AOP使用示例 加载时织入(Load -Time WEaving,LTW) 指的是在虚拟机载入字节码时动态织入 Aspe ...
- 【特征匹配】ORB原理与源码解析
相关 : Fast原理与源码解析 Brief描述子原理与源码解析 Harris原理与源码解析 http://blog.csdn.net/luoshixian099/article/details/48 ...
- Guava RateLimiter限流源码解析和实例应用
2019独角兽企业重金招聘Python工程师标准>>> 在开发高并发系统时有三把利器用来保护系统:缓存.降级和限流 缓存 缓存的目的是提升系统访问速度和增大系统处理容量 降级 降级是 ...
- Alibaba-Dexposed Bug框架原理及源码解析
目录(?)[+] Alibaba的AndFix热修复: Alibaba-AndFix Bug热修复框架的使用 Alibaba-AndFix Bug热修复框架原理及源码解析 上一篇中已经介绍了Ali ...
- Mybatis运行原理及源码解析
Mybatis源码解析 一.前言 本文旨在mybatis源码解析,将整个mybatis运行原理讲解清楚,本文代码地址: https://github.com/lchpersonal/mybatis-l ...
- Android Glide图片加载框架(二)源码解析之into()
文章目录 一.前言 二.源码解析 1.into(ImageView) 2.GlideContext.buildImageViewTarget() 3.RequestBuilder.into(Targe ...
- spring事务源码解析
前言 在spring jdbcTemplate 事务,各种诡异,包你醍醐灌顶!最后遗留了一个问题:spring是怎么样保证事务一致性的? 当然,spring事务内容挺多的,如果都要讲的话要花很长时间, ...
最新文章
- 青源 Forum | 人工智能的数理基础前沿系列报告 · 第 5 期
- 毕飞宇:我是靠阅读支撑起来的作家 因为生活没有给我那么多
- 【攻防世界002】EasyRE
- mybatis 中 foreach collection的三种用法
- c语言整形除法是五舍六入吗,四舍六入五成双 - C/C++论坛 - 51CTO技术论坛_中国领先的IT技术社区...
- 算法踩坑4-冒泡排序
- 【Hbase】HBase界面简介
- Git如何配置多个SSH-Key呢?
- java栈和队列实现删除,数据结构学习--Java栈和队列
- Android 属性动画简单分析(二)
- 微型计算机原理及应用技术ppt,微型计算机原理及应用技术.ppt
- 逻辑回归之ROC曲线的绘制
- redis下载安装教程
- vue 实现点击图片放大
- java 压力测试_记一次完整的java项目压力测试
- 如何远程连接服务器?
- word使用学习总结
- U盘容量变小实用解决方案
- vi中跳到首行或尾行
- 主流浏览器发展史及其内核初探