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源码解析相关推荐

  1. 消息转发机制与Aspects源码解析

    前言 最近在搞重构相关的事情,遇到了不少这样的场景: 进入一个界面,在viewWillAppear:的时候做相应判断,如果满足条件则执行对应代码. 这类业务有一个特点,业务内容是对应整个App的,与对 ...

  2. Aspects源码解析

    Aspects使用方法 Aspects源码只有两个文件:Aspects.h和Aspects.m文件.使用的方式就是对NSObject添加了一个Category,其中有两个方法分别为类和对象添加切面bl ...

  3. Spring源码深度解析(郝佳)-学习-源码解析-创建AOP静态代理实现(八)

    继上一篇博客,我们继续来分析下面示例的 Spring 静态代理源码实现. 静态 AOP使用示例 加载时织入(Load -Time WEaving,LTW) 指的是在虚拟机载入字节码时动态织入 Aspe ...

  4. 【特征匹配】ORB原理与源码解析

    相关 : Fast原理与源码解析 Brief描述子原理与源码解析 Harris原理与源码解析 http://blog.csdn.net/luoshixian099/article/details/48 ...

  5. Guava RateLimiter限流源码解析和实例应用

    2019独角兽企业重金招聘Python工程师标准>>> 在开发高并发系统时有三把利器用来保护系统:缓存.降级和限流 缓存 缓存的目的是提升系统访问速度和增大系统处理容量 降级 降级是 ...

  6. Alibaba-Dexposed Bug框架原理及源码解析

    目录(?)[+] Alibaba的AndFix热修复:  Alibaba-AndFix Bug热修复框架的使用  Alibaba-AndFix Bug热修复框架原理及源码解析 上一篇中已经介绍了Ali ...

  7. Mybatis运行原理及源码解析

    Mybatis源码解析 一.前言 本文旨在mybatis源码解析,将整个mybatis运行原理讲解清楚,本文代码地址: https://github.com/lchpersonal/mybatis-l ...

  8. Android Glide图片加载框架(二)源码解析之into()

    文章目录 一.前言 二.源码解析 1.into(ImageView) 2.GlideContext.buildImageViewTarget() 3.RequestBuilder.into(Targe ...

  9. spring事务源码解析

    前言 在spring jdbcTemplate 事务,各种诡异,包你醍醐灌顶!最后遗留了一个问题:spring是怎么样保证事务一致性的? 当然,spring事务内容挺多的,如果都要讲的话要花很长时间, ...

最新文章

  1. 青源 Forum | 人工智能的数理基础前沿系列报告 · 第 5 期
  2. 毕飞宇:我是靠阅读支撑起来的作家 因为生活没有给我那么多
  3. 【攻防世界002】EasyRE
  4. mybatis 中 foreach collection的三种用法
  5. c语言整形除法是五舍六入吗,四舍六入五成双 - C/C++论坛 - 51CTO技术论坛_中国领先的IT技术社区...
  6. 算法踩坑4-冒泡排序
  7. 【Hbase】HBase界面简介
  8. Git如何配置多个SSH-Key呢?
  9. java栈和队列实现删除,数据结构学习--Java栈和队列
  10. Android 属性动画简单分析(二)
  11. 微型计算机原理及应用技术ppt,微型计算机原理及应用技术.ppt
  12. 逻辑回归之ROC曲线的绘制
  13. redis下载安装教程
  14. vue 实现点击图片放大
  15. java 压力测试_记一次完整的java项目压力测试
  16. 如何远程连接服务器?
  17. word使用学习总结
  18. U盘容量变小实用解决方案
  19. vi中跳到首行或尾行
  20. 主流浏览器发展史及其内核初探

热门文章

  1. 学习笔记:获取字符串中数字的两种方法
  2. 2016年上半年信息系统项目管理师真题之上午题小虎趣味解答第26-30题
  3. 压电雨量传感器特点介绍 单个雨量自动监测
  4. Robots协议(爬虫协议、机器人协议)
  5. 2023(Q2)起重司机(限门座式)模拟一[安考星]
  6. LibreOffice具体用法
  7. FineReport使用
  8. 计算机技术与软件专业技术资格(水平)考试—— 软考中级 网络工程师笔记one
  9. Application.mk详解
  10. JavaScript全解析