Aspects深度解析-iOS面向切面编程

背景简述

在日常开发过程中是否有过这样的需求:不修改原来的函数,但是又想在函数的执行前后插入一些代码。这个方式就是面向切面(AOP),在iOS开发中比较知名的框架就是Aspects,而饿了么新出的Stinger框架先不讨论,Aspects的源码精炼巧妙,很值得学习深究,本文主要从源码和应用层面来介绍下

源码解析

先提出几个问题

带着问题去阅读更容易理解

Aspects实现的核心原理是什么

哪些方法不能被hook

hook的操作是否可以只对某个实例生效,对同一个类的其他实例不生效?

block是如何被存储和调用的

基本原理

正常来讲想实现AOP,可以利用runtime的特性进行method swizzle,但Aspects就是造好的轮子,而且更好用,下面简述下Aspects的基本原理

runtime的消息转发机制

在OC中,所有的消息调用最后都会通过objc_msgSend()方法进行访问

通过objc_msgSend()进行消息调用,为了加快执行速度,这个方法在runtime源码中是用汇编实现的

然后调用lookUpImpOrForward()方法,返回值是个IMP指针,如果查找到了调用函数的IMP,则进行方法的访问

如果没有查到对于方法的IMP指针,则进行消息转发机制

第一层转发:会调用resolveInstanceMethod:、resolveClassMethod:,这次转发是方法级别的,开发者可以动态添加方法进行补救

第二层转发:如果第一层转发仍然没有找到SEL,则会进行第二层转发,调用forwardingTargetForSelector:,可以把调用转发到另一个对象,这是类级别的转发,调用另一个类的相同的方法

第三层转发:如果第二层转发返回nil,则会进入这一层处理,这层会调用methodSignatureForSelector:、forwardInvocation:,这次是完整的消息转发,因为你可以返回方法签名、动态指定调用方法的Target

如果转发都失败,就会crash

Aspects的基本原理

对外暴露的核心API

/**

作用域:针对所有对象生效

selector: 需要hook的方法

options:是个枚举,主要定义了切面的时机(调用前、替换、调用后)

block: 需要在selector前后插入执行的代码块

error: 错误信息

*/+ (id)aspect_hookSelector:(SEL)selector                          withOptions:(AspectOptions)options                            usingBlock:(id)block                                error:(NSError**)error;/**

作用域:针对当前对象生效

*/- (id)aspect_hookSelector:(SEL)selector                          withOptions:(AspectOptions)options                            usingBlock:(id)block                                error:(NSError**)error;

上面介绍了消息的转发机制,而Aspects就是利用了消息转发机制,通过hook第三层的转发方法forwardInvocation:,然后根据切面的时机来动态调用block。接下来详细分析巧妙的设计

类A的方法m被添加切面方法

创建一个类A的子类B,并hook子类B的forwardInvocation:方法拦截消息转发,使forwardInvocation:的IMP指向事先准备好的__ASPECTS_ARE_BEING_CALLED__函数(后面简称ABC函数),block方法的执行就在ABC函数中

把类A的对象的isa指针指向B,这样就把消息的处理转发到类B上,类似KVO的机制,同时会更改class方法的IMP,把它指向类A的class方法,当外界调用class时获取的还是类A,并不知道中间类B的存在

对于方法m,类B会直接把方法m的IMP指向_objc_msgForward()方法,这样当调用方法m时就会走消息转发流程,触发ABC函数

详细分析

执行入口

- (id)aspect_hookSelector:(SEL)selector                      withOptions:(AspectOptions)options                      usingBlock:(id)block                            error:(NSError**)error {returnaspect_add(self, selector, options, block, error);}staticidaspect_add(idself, SEL selector, AspectOptions options,idblock,NSError**error) {    __block AspectIdentifier *identifier =nil;// 添加自旋锁,block内容的执行时互斥的aspect_performLocked(^{if(aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {// 获取容器,容器的对象以关联对象的方式添加到了当前对象上,key值为`前缀+selector`AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);// 创建标识符,用来存储SEL、block、切面时机(调用前、调用后)等信息identifier = [AspectIdentifier identifierWithSelector:selector object:selfoptions:options block:block error:error];if(identifier) {                [aspectContainer addAspect:identifier withOptions:options];// Modify the class to allow message interception.aspect_prepareClassAndHookSelector(self, selector, error);            }        }    });returnidentifier;}

执行入口调用了aspect_add(self, selector, options, block, error)方法,这个方法时线程安全的,接下来一步步解析具体做了什么

过滤拦截:aspect_isSelectorAllowedAndTrack()

精简版的源码,已经添加了注释

staticBOOLaspect_isSelectorAllowedAndTrack(NSObject*self, SEL selector, AspectOptions options,NSError**error) {staticNSSet*disallowedSelectorList;staticdispatch_once_tpred;dispatch_once(&pred, ^{// 初始化黑名单列表,有些方法时禁止hook的disallowedSelectorList = [NSSetsetWithObjects:@"retain",@"release",@"autorelease",@"forwardInvocation:",nil];    });// 第一步:检查是否在黑名单内NSString*selectorName =NSStringFromSelector(selector);if([disallowedSelectorList containsObject:selectorName]) {        ...returnNO;    }// 第二步: dealloc方法只能在调用前插入AspectOptions position = options&AspectPositionFilter;if([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {        ...returnNO;    }// 第三步:检查类是否存在这个方法if(![selfrespondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {        ...returnNO;    }// 第四步:如果是类而非实例(这个是类,不是类方法,是指hook的作用域对所有对象都生效),则在整个类即继承链中,同一个方法只能被hook一次,即对于所有实例对象都生效的操作,整个继承链中只能被hook一次if(class_isMetaClass(object_getClass(self))) {        ...    }else{returnYES;    }returnYES;}

不允许hookretain、release、autorelease、forwardInvocation:,这些不多解释

允许hookdealloc,但是只能在dealloc执行前,这都是为了程序的安全性设置的

检查这个方法是否存在,不存在则不能hook

Aspects对于hook的生效作用域做了区分:所有实例对象&某个具体实例对象。对于所有实例对象在整个继承链中,同一个方法只能被hook一次,这么做的目的是为了规避循环调用的问题(详情可以了解下supper关键字)

关键类结构

AspectOptions

是个枚举,用来定义切面的时机,即原有方法调用前、调用后、替换原有方法、只执行一次(调用完就删除切面逻辑)

typedefNS_OPTIONS(NSUInteger, AspectOptions) {    AspectPositionAfter  =0,/// 原有方法调用前执行 (default)AspectPositionInstead =1,/// 替换原有方法AspectPositionBefore  =2,/// 原有方法调用后执行AspectOptionAutomaticRemoval =1<<3/// 执行完之后就恢复切面操作,即撤销hook};

AspectIdentifier类

简单理解话就是一个存储model,主要用来存储hook方法的相关信息,如原有方法、切面block、切面时机等

@interfaceAspectIdentifier:NSObject...其他省略@property(nonatomic,assign) SEL selector;// 原来方法的SEL@property(nonatomic,strong)idblock;// 保存要执行的切面block,即原方法执行前后要调用的方法@property(nonatomic,strong)NSMethodSignature*blockSignature;// block的方法签名@property(nonatomic,weak)idobject;// target,即保存当前对象@property(nonatomic,assign) AspectOptions options;// 是个枚举,表示切面执行时机,上面已经有介绍@end

AspectsContainer类

容器类,以关联对象的形式存储在当前类或对象中,主要用来存储当前类或对象所有的切面信息

@interface AspectsContainer : NSObject...其他省略@property (atomic, copy) NSArray *beforeAspects; // 存储原方法调用前要执行的操作@property (atomic, copy) NSArray *insteadAspects;// 存储替换原方法的操作@property (atomic, copy) NSArray *afterAspects;// 存储原方法调用后要执行的操作@end

存储切面信息

存储切面信息主要用到了上面介绍的AspectsContainer、AspectIdentifier这两个类,主要操作如下(注释写的已经很详细)

获取当前类的容器对象aspectContainer,如果没有则创建一个

创建一个标识符对象identifier,用来存储原方法信息、block、切面时机等信息

把标识符对象identifier添加到容器中

staticidaspect_add(idself, SEL selector, AspectOptions options,idblock,NSError**error) {    ...// 获取容器对象,主要用来存储当前类或对象所有的切面信息,容器的对象以关联对象的方式添加到了当前对象上,key值为`前缀+selector`AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);// 创建标识符,用来存储SEL、block、切面时机(调用前、调用后)等信息identifier = [AspectIdentifier identifierWithSelector:selector object:selfoptions:options block:block error:error];if(identifier) {// 把identifier添加到容器中[aspectContainer addAspect:identifier withOptions:options];        ...    }returnidentifier;}

创建中间类

这一步的操作类似kvo的机制,隐式的创建一个中间类,一:可以做到hook只对单一对象有效,二:避免了对原有类的侵入

这一步主要做了几个操作

如果已经存在中间类,则直接返回

如果是类对象,则不用创建中间类,并把这个类存储在swizzledClasses集合中,标记这个类已经被hook了

如果存在kvo的情况,那么系统已经帮我们创建好了中间类,那就直接使用

对于不存在kvo且是实例对象的,则单独创建一个继承当前类的中间类midcls,并hook它的forwardInvocation:方法,并把当前对象的isa指针指向midcls,这样就做到了hook操作只针对当前对象有效,因为其他对象的isa指针指向的还是原有类

staticClass aspect_hookClass(NSObject*self,NSError**error) {Class statedClass =self.class;Class baseClass = object_getClass(self);NSString*className =NSStringFromClass(baseClass);// Already subclassedif([className hasSuffix:AspectsSubclassSuffix]) {returnbaseClass;// We swizzle a class object, not a single object.}elseif(class_isMetaClass(baseClass)) {returnaspect_swizzleClassInPlace((Class)self);        }elseif(statedClass != baseClass) {// Probably a KVO class. Swizzle in place. Also swizzle meta classes in place.returnaspect_swizzleClassInPlace(baseClass);        }// Default case. Create dynamic subclass.constchar*subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;Class subclass = objc_getClass(subclassName);if(subclass ==nil) {    subclass = objc_allocateClassPair(baseClass, subclassName,0);// hook forwardInvocation方法aspect_swizzleForwardInvocation(subclass);// hook class方法,把子类的class方法的IMP指向父类,这样外界并不知道内部创建了子类aspect_hookedGetClass(subclass, statedClass);    aspect_hookedGetClass(object_getClass(subclass), statedClass);    objc_registerClassPair(subclass);}// 把当前对象的isa指向子类,类似kvo的用法object_setClass(self, subclass);returnsubclass;}

替换forwardInvocation:方法

从下面的代码可以看到,主要功能就是把当前类的forwardInvocation:替换成__ASPECTS_ARE_BEING_CALLED__,这样当触发消息转发的时候,就会调用__ASPECTS_ARE_BEING_CALLED__方法

对于__ASPECTS_ARE_BEING_CALLED__方法是Aspects的核心操作,主要就是做消息的调用和分发,控制方法的调用的时机,下面会详细介绍

// hook forwardInvocation方法,用来拦截消息的发送staticvoidaspect_swizzleForwardInvocation(Class klass) {// If there is no method, replace will act like class_addMethod.IMP originalImplementation = class_replaceMethod(klass,@selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__,"v@:@");if(originalImplementation) {        class_addMethod(klass,NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation,"v@:@");    }    AspectLog(@"Aspects: %@ is now aspect aware.",NSStringFromClass(klass));}

自动触发消息转发机制

Aspects的核心原理是消息转发,那么必要出的就是怎么自动触发消息转发机制

runtime中有个方法_objc_msgForward,直接调用可以触发消息转发机制,著名的JSPatch框架也是利用了这个机制

假如要hook的方法叫m1,那么把m1的IMP指向_objc_msgForward,这样当调用方法m1时就自动触发消息转发机制了,详细实现如下

staticvoidaspect_prepareClassAndHookSelector(NSObject*self, SEL selector,NSError**error) {    Method targetMethod = class_getInstanceMethod(klass, selector);    IMP targetMethodIMP = method_getImplementation(targetMethod);if(!aspect_isMsgForwardIMP(targetMethodIMP)) {        ...// We use forwardInvocation to hook in. 把函数的调用直接触发转发函数,转发函数已经被hook,所以在转发函数时进行block的调用class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);    }}

核心转发函数处理

上面一切准备就绪,那么怎么触发之前添加的切面block呢,首先我们梳理下整个流程

方法m1的IMP指向了_objc_msgForward,调用m1则会自动触发消息转发机制

替换forwardInvocation:,把它的IMP指向__ASPECTS_ARE_BEING_CALLED__方法,消息转发时触发的就是__ASPECTS_ARE_BEING_CALLED__

上面操作可以直接看出调用方法m1则会直接触发__ASPECTS_ARE_BEING_CALLED__方法,而__ASPECTS_ARE_BEING_CALLED__方法就是处理切面block用和原有函数的调用时机,详细看下面实现步骤

根据调用的selector,获取容器对象AspectsContainer,这里面存储了这个类或对象的所有切面信息

AspectInfo会存储当前的参数信息,用于传递

首先触发函数调用前的block,存储在容器的beforeAspects对象中

接下来如果存在替换原有函数的block,即insteadAspects不为空,则触发它,如果不存在则调用原来的函数

触发函数调用后的block,存在在容器的afterAspects对象中

staticvoid__ASPECTS_ARE_BEING_CALLED__(__unsafe_unretainedNSObject*self, SEL selector,NSInvocation*invocation) {    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);    AspectInfo *info = [[AspectInfo alloc] initWithInstance:selfinvocation:invocation];// Before hooks. 方法执行之前调用aspect_invoke(classContainer.beforeAspects, info);    aspect_invoke(objectContainer.beforeAspects, info);// Instead hooks. 替换原方法或者调用原方法BOOLrespondsToAlias =YES;if(objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {        aspect_invoke(classContainer.insteadAspects, info);        aspect_invoke(objectContainer.insteadAspects, info);    }else{        Class klass = object_getClass(invocation.target);do{if((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {                [invocation invoke];break;            }        }while(!respondsToAlias && (klass = class_getSuperclass(klass)));    }// After hooks. 方法执行之后调用aspect_invoke(classContainer.afterAspects, info);    aspect_invoke(objectContainer.afterAspects, info);    ...// Remove any hooks that are queued for deregistration.[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];}

总结

Aspects的核心原理是利用了消息转发机制,通过替换消息转发方法来实现切面的分发调用,这个思想很巧妙而且应用很广泛,很多三方库都利用了这个原理,值得学习

正在跳转(iOS交流裙 密码:123)

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