iOS Aspects源码剖析

摘要:Aspects用来干什么?Aspect是一个简洁高效的用于使iOS支持AOP(面向切面编程)的框架。官方描述的很清楚,大致意思如下:你可以使用Aspect为每一个类或者类的实例的某个方法插入一段代码,切入点可以选择before(在原方法执行前执行)/instead(替换原方法)/after(原方法执行完之后执行)。ThinkofAspectsasmethodswizzlingonsteroids.Itallowsyoutoaddcodetoexistingmethodsper

Aspects用来干什么?

Aspect是一个简洁高效的用于使iOS支持AOP(面向切面编程)的框架。官方描述的很清楚,大致意思如下:你可以使用Aspect为每一个类或者类的实例的某个方法插入一段代码,切入点可以选择before(在原方法执行前执行)/instead(替换原方法)/after(原方法执行完之后执行)。

Think of Aspects as method swizzling on steroids. It allows you to

add code to existing methods per class or per instance, whilst thinking

of the insertion point e.g. before/instead/after. Aspects automatically

deals with calling super and is easier to use than regular method

swizzling.

本博文基于 v1.4.2 版本源码进行分析。

技术储备

Aspect是在Runtime的基础上构建的。在学习Aspect前,你需要搞清楚下面的概念:

1. NSMethodSignature 和 NSInvocation

使用NSMethodSignature 和 NSInvocation

不仅可以完成对method的调用,也可以完成block的调用。在Aspect中,正是运用NSMethodSignature,NSInvocation

实现了对block的统一处理。不清楚没关系,先搞清楚NSMethodSignature和NSInvocation的使用方法及如何使用他们执行method

或 block。

对象调用method代码示例

一个实例对象可以通过三种方式调用其方法。

- (void)test{

//type1

[self printStr1:@"hello world 1"];

//type2

[self performSelector:@selector(printStr1:) withObject:@"hello world 2"];

//type3

//获取方法签名

NSMethodSignature *sigOfPrintStr = [self methodSignatureForSelector:@selector(printStr1:)];

//获取方法签名对应的invocation

NSInvocation *invocationOfPrintStr = [NSInvocation invocationWithMethodSignature:sigOfPrintStr];

/**

设置消息接受者,与[invocationOfPrintStr setArgument:(__bridge void * _Nonnull)(self) atIndex:0]等价

*/

[invocationOfPrintStr setTarget:self];

/**设置要执行的selector。与[invocationOfPrintStr setArgument:@selector(printStr1:) atIndex:1] 等价*/

[invocationOfPrintStr setSelector:@selector(printStr1:)];

//设置参数

NSString *str = @"hello world 3";

[invocationOfPrintStr setArgument:&;str atIndex:2];

//开始执行

[invocationOfPrintStr invoke];

}

- (void)printStr1:(NSString*)str{

NSLog(@"printStr1  %@",str);

}

在调用test方法时,会分别输出:

2017-01-11 15:20:21.642 AspectTest[2997:146594] printStr1  hello world 1

2017-01-11 15:20:21.643 AspectTest[2997:146594] printStr1  hello world 2

2017-01-11 15:20:21.643 AspectTest[2997:146594] printStr1  hello world 3

type1和type2是我们常用的,这里不在赘述,我们来说说type3。

NSMethodSignature和NSInvocation是Foundation框架为我们提供的一种调用方法的方式,经常用于消息转发。

NSMethodSignature概述

NSMethodSignature用于描述method的类型信息:返回值类型,及每个参数的类型。 可以通过下面的方式进行创建:

@interface NSObject

//获取实例方法的签名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

//获取类方法的签名

+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector;

@end

-------------

//使用ObjCTypes创建方法签名

@interface NSMethodSignature

+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;

@end

使用NSObject的实例方法和类方法创建NSMethodSignature很简单,不说了。咱撩一撩signatureWithObjCTypes。

在OC中,每一种数据类型可以通过一个字符编码来表示(Objective-C type encodings)。例如字符‘@’代表一个object, 'i'代表int。 那么,由这些字符组成的字符数组就可以表示方法类型了。举个例子:上面提到的printStr1:对应的ObjCTypes 为aliyunzixun@xxx.com:@。

’v‘ : void类型,第一个字符代表返回值类型

’@‘ : 一个id类型的对象,第一个参数类型

’:‘ : 对应SEL,第二个参数类型

’@‘ : 一个id类型的对象,第三个参数类型,也就是- (void)printStr1:(NSString*)str中的str。

printStr1:本来是一个参数,ObjCTypes怎么成了三个参数?要理解这个还必须理解OC中的消息机制。一个method对应的结构体如下,ObjCTypes中的参数其实与IMP

method_imp 函数指针指向的函数的参数相一致。相关内容有很多,不了解的可以参考这篇文章方法与消息。

typedef struct objc_method *Method;

struct objc_method {

SEL method_name                OBJC2_UNAVAILABLE;  // 方法名

char *method_types                  OBJC2_UNAVAILABLE;

IMP method_imp                      OBJC2_UNAVAILABLE;  // 方法实现

}

NSInvocation概述

就像示例代码所示,我们可以通过+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;创建出NSInvocation对象。接下来你设置各个参数信息, 然后调用invoke进行调用。执行结束后,通过- (void)getReturnValue:(void *)retLoc;获取返回值。

这里需要注意,对NSInvocation对象设置的参数个数及类型和获取的返回值的类型要与创建对象时使用的NSMethodSignature对象代表的参数及返回值类型向一致,否则cresh。

使用NSInvocation调用block

下面展示block 的两种调用方式

- (void)test{

void (^block1)(int) = ^(int a){

NSLog(@"block1 %d",a);

};

//type1

block1(1);

//type2

//获取block类型对应的方法签名。

NSMethodSignature *signature = aspect_blockMethodSignature(block1);

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

[invocation setTarget:block1];

int a=2;

[invocation setArgument:&;a atIndex:1];

[invocation invoke];

}

type1 就是常用的方法,不再赘述。看一下type2。 type2和上面调用method的type3用的一样的套路,只是参数不同:由block生成的NSInvocation对象的第一个参数是block本身,剩下的为 block自身的参数。

由于系统没有提供获取block的ObjCTypes的api,我们必须想办法找到这个ObjCTypes,只有这样才能生成NSMethodSignature对象!

block的数据结构 &; 从数据结构中获取 ObjCTypes

oc是一门动态语言,通过编译 oc可以转变为c语言。经过编译后block对应的数据结构是struct。(block中技术点还是挺过的,推荐一本书“Objective-C 高级编程”)

//代码来自 Aspect

// Block internals.

typedef NS_OPTIONS(int, AspectBlockFlags) {

AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),

AspectBlockFlagsHasSignature          = (1 << 30)

};

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;

在此结构体中 const char *signature 字段就是我们想要的。通过下面的方法获取signature并创建NSMethodSignature对象。

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];

}

2. method swizzling

在Objective-C中调用一个方法,其实是向一个对象发送消息。每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向Method具体的实现。

selector-imp.png

通过 method swizzling这种黑科技,你可以改变selector和方法实现的映射关系。

swizzled-imp

此时当执行[objc selectorC]时,实际调用的是 IMPn指针指向的函数。

具体实现代码如下:

代码来源: https://github.com/hejunm/iOS-Tools

@implementation HJMSwizzleTools:NSObject

+ (void)hjm_swizzleWithClass:(Class)processedClass originalSelector:(SEL)originSelector swizzleSelector:(SEL)swizzlSelector{

Method originMethod = class_getInstanceMethod(processedClass, originSelector);

Method swizzleMethod = class_getInstanceMethod(processedClass, swizzlSelector);

//当processedClass实现originSelector时,didAddMethod返回false,否则返回true. 如果当前类没有实现originSelector而父类实现了,这是直接使用method_exchangeImplementations会swizzle父类的originSelector。这样会出现很大的问题。

BOOL didAddMethod = class_addMethod(processedClass, originSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));

if (didAddMethod) {

class_replaceMethod(processedClass, swizzlSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));

}else{

method_exchangeImplementations(originMethod, swizzleMethod);

}

}

@end

可以这样使用

+ (void)load{

static dispatch_once_t onceToken;

dispatch_once(&;onceToken, ^{

[HJMSwizzleTools hjm_swizzleWithClass:self originalSelector:@selector(viewDidLoad) swizzleSelector:@selector(swizzleViewDidLoad)];

});

}

//被替换了。。

- (void)viewDidLoad {

[super viewDidLoad];

}

//现在系统会调用这个方法

- (void)swizzleViewDidLoad {

NSLog(@"do something");

}

3. 消息转发流程

在Objective-C中调用一个方法,其实是向一个对象发送消息。如果这个消息没有对应的实现时就会进行消息转发。转发流程图如下:

forwardMethod.png

下面用代码演示一遍

resolveInstanceMethod

当根据selector没有找到对应的method时,首先会调用这个方法,在该方法中你可以为一个类添加一个方法。并返回yes。下面的代码只是声明了runTo方法,没有实现。

//Car.h

@interface Car : NSObject

- (void)runTo:(NSString *)place;

@end

//Car.m

@implementation Car

+ (BOOL)resolveInstanceMethod:(SEL)sel{

if (sel == @selector(runTo:)) {

class_addMethod(self, sel, (IMP)dynamicMethodIMPRunTo, "aliyunzixun@xxx.com:@");

return YES;

}

return [super resolveInstanceMethod:sel];

}

//动态添加的@selector(runTo:) 对应的实现

static void dynamicMethodIMPRunTo(id self, SEL _cmd,id place){

NSLog(@"dynamicMethodIMPRunTo %@",place);

}

@end

forwardingTargetForSelector

如果resolveInstanceMethod没有实现,返回No,或者没有动态添加方法的话,就会执行forwardingTargetForSelector。

在这里你可以返回一个能够执行这个selector的对象otherTarget,接下来消息会重新发送到这个otherTarget。

//Person.h

@interface Person : NSObject

- (void)runTo:(NSString *)place;

@end

//Person.m

@implementation Person

- (void)runTo:(NSString *)place;{

NSLog(@"person runTo %@",place);

}

@end

//Car.h

@interface Car : NSObject

- (void)runTo:(NSString *)place;

@end

//Car.m

@implementation Car

- (id)forwardingTargetForSelector:(SEL)aSelector{

//将消息转发给Person的实例

if (aSelector == @selector(runTo:)){

return [[Person alloc]init];

}

return [super forwardingTargetForSelector:aSelector];

}

@end

forwardInvocation

如果上面两种情况没有执行,就会执行通过forwardInvocation进行消息转发。

@implementation Car

- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{

//判断selector是否为需要转发的,如果是则手动生成方法签名并返回。

if (aSelector == @selector(runTo:)){

return [NSMethodSignature signatureWithObjCTypes:"aliyunzixun@xxx.com:@"];

}

return [super forwardingTargetForSelector:aSelector];

}

- (void)forwardInvocation:(NSInvocation *)anInvocation{

//判断待处理的anInvocation是否为我们要处理的

if (anInvocation.selector == @selector(runTo:)){

}else{

}

}

@end

在NSInvocation对象中保存着我们调用一个method的所有信息。可以看下其属性和方法:

methodSignature 含有返回值类型,参数个数及每个参数的类型 等信息。

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;获取调用method时传的参数

- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx; 设置第index参数。

- (void)invoke; 开始执行

- (void)getReturnValue:(void *)retLoc; 获取返回值

下面的代码演示如何获取调用method时所传的各参数值

- (void)forwardInvocation:(NSInvocation *)anInvocation{

if (anInvocation.selector == @selector(runTo:)){

void *argBuf = NULL;

NSUInteger numberOfArguments = anInvocation.methodSignature.numberOfArguments;

for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {

const char *type = [anInvocation.methodSignature getArgumentTypeAtIndex:idx];

NSUInteger argSize;

NSGetSizeAndAlignment(type, &;argSize, NULL);

if (!(argBuf = reallocf(argBuf, argSize))) {

NSLog(@"Failed to allocate memory for block invocation.");

return ;

}

[anInvocation getArgument:argBuf atIndex:idx];

//现在argBuf 中保存着第index 参数的值。 你可以使用这些值进行其他处理,例如为block中各参数赋值,并调用。

}

}else{

}

}

通过手动触发消息转发(method已经实现)

前面所描述的消息转发都是在selector没有对应实现时自动进行的,我们称之为自动消息转发。现在有个需求:即使Car类实现了 runTo:,执行[objOfCar runTo:@"shangHai"]; 时也进行消息转发(手动触发),如何实现?

实现方法如下:利用 method swizzling 将selector的实现改变为_objc_msgForward或者_objc_msgForward_stret。在调selector时就会进行消息转发 看下面的代码:

//对 runTo: 进行消息转发

@implementation Car

//进行 method swizzling。此时调用runTo:就会进行消息转发

+ (void)load{

SEL selector = @selector(runTo:);

Method targetMethod = class_getInstanceMethod(self.class, @selector(selector));

const char *typeEncoding = method_getTypeEncoding(targetMethod);

IMP targetMethodIMP = _objc_msgForward;

class_replaceMethod(self.class, selector, targetMethodIMP, typeEncoding);

}

- (void)runTo:(NSString *)place{

NSLog(@"car runTo %@",place);

}

//消息转发,调用这个方法。anInvocation中保存着调用方法时传递的参数信息

- (void)forwardInvocation:(NSInvocation *)anInvocation{

if (anInvocation.selector == @selector(runTo:)){

}else{

}

}

上面提到了_objc_msgForward或者_objc_msgForward_stret,

该如何选择?首先两者都是进行消息转发的,大概是这样:如果转发的消息的返回值是struct类型,就使用_objc_msgForward_stret,否则使用_objc_msgForward。参考资料。简单引用JSPatch作者的解释

大多数CPU在执行C函数时会把前几个参数放进寄存器里,对 obj_msgSend 来说前两个参数固定是 self /

_cmd,它们会放在寄存器上,在最后执行完后返回值也会保存在寄存器上,取这个寄存器的值就是返回值。普通的返回值(int/pointer)很小,放在寄存器上没问题,但有些

struct

是很大的,寄存器放不下,所以要用另一种方式,在一开始申请一段内存,把指针保存在寄存器上,返回值往这个指针指向的内存写数据,所以寄存器要腾出一个位置放这个指针,self

/ _cmd 在寄存器的位置就变了。objc_msgSend 不知道 self / _cmd 的位置变了,所以要用另一个方法

objc_msgSend_stret 代替。原理大概就是这样。在 NSMethodSignature 的 debugDescription

上打出了是否 special struct,只能通过这字符串判断。所以最终的处理是,在非 arm64 下,是 special struct 就走

_objc_msgForward_stret,否则走 _objc_msgForward。

根据selector返回值类型获取_objc_msgForward或者_objc_msgForward_stret 的代码如下:

//代码来自Aspect

static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {

IMP msgForwardIMP = _objc_msgForward;

#if !defined(__arm64__)

Method method = class_getInstanceMethod(self.class, selector);

const char *encoding = method_getTypeEncoding(method);

BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;

if (methodReturnsStructValue) {

@try {

NSUInteger valueSize = 0;

NSGetSizeAndAlignment(encoding, &;valueSize, NULL);

if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {

methodReturnsStructValue = NO;

}

} @catch (__unused NSException *e) {}

}

if (methodReturnsStructValue) {

msgForwardIMP = (IMP)_objc_msgForward_stret;

}

#endif

return msgForwardIMP;

}

Aspects 源码

一直在思考如何使用文字清晰的描述出Aspects的实现原理,最后决定使用在源码上添加注释的形式呈现。自己偷个懒。

Aspects 源码剖析

Aspects 使用场景

app埋点是 Aspects 框架的一个典型应用,你可以通过RunTime应用实例--关于埋点的思考了解 Aspects的使用场景。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容