iOS开发 面向切面编程之 Aspects 源码解析

695 阅读6分钟

1、面向切面编程应用在统计上 业务逻辑和统计逻辑经常耦合在一起,一方面影响了正常的业务逻辑,同时也很容易搞乱打点逻辑,而且要查看打点情况的时候也很分散。在 web 编程时候,这种场景很早就有了很成熟的方案,也就是所谓的AOP 编程(面向切面编程),其原理也就是在不更改正常的业务处理流程的前提下,通过生成一个动态代理类,从而实现对目标对象嵌入附加的操作。在 iOS 中,要想实现相似的效果也很简单,利用 oc 的动态性,通过 swizzling method 改变目标函数的 selector 所指向的实现,然后在新的实现中实现附加的操作,完成之后再回到原来的处理逻辑。 开源框架Aspects是一个非常好的框架。 Aspects

2、基本原理

原理1.png

每一个对象都有一个指向其所属类的isa指针,通过该指针找到所属的类,然后会在所属类中的方法列表中寻找方法的实现,如果在方法列表中查到了和选择子名称相符的方法就会跳转到他的方法实现,如果找不到会向其父类的方法列表中查找,以此类推,直到NSObject类,如果还是查找不到就会执行“消息转发”操作。 另外为了保证消息机制的效率,每一个类都设置一个缓存方法列表,缓存列表中包含了当前类的方法以及继承自父类的方法,在查询方法列的时候,都会先查询本类的缓存列表,再去查询方法类别。这样当一个方法已经被调用过一次,下次调用就会很快的查询到并调用。

方法调用的过程
1.在对象自己缓存的方法列表中去找要调用的方法,找到了就直接执行其实现。
2.缓存里没找到,就去上面说的它的方法列表里找,找到了就执行其实现。
3.还没找到,说明这个类自己没有了,就会通过isa去向其父类里执行1、2。
4.如果找到了根类还没找到,那么就是没有了,会转向一个拦截调用的方法,我们可以自己在拦截调用方法里面做一些处理。
5.如果没有在拦截调用里做处理,那么就会报错崩溃。

从上面我们可以发现,在发消息的时候,如果 selector 有对应的 IMP,则直接执行,如果没有就进行查找,如果最后没有查找到。OC 给我们提供了几个可供补救的机会,依次有 resolveInstanceMethod、forwardingTargetForSelector、forwardInvocation。

Aspects 之所以选择在 forwardInvocation 这里处理是因为,这几个阶段特性都不太一样:

resolvedInstanceMethod 适合给类/对象动态添加一个相应的实现forwardingTargetForSelector 适合将消息转发给其他对象处理 forwardInvocation 是里面最灵活,最能符合需求的

因此 Aspects 的方案就是,对于待 hook 的 selector,将其指向 objc_msgForward,同时生成一个新的 aliasSelector 指向原来的 IMP,并且 hook 住 forwardInvocation 函数,使他指向自己的实现。按照上面的思路,当被 hook 的 selector 被执行的时候,首先根据 selector 找到了 objc_msgForward ,而这个会触发消息转发,从而进入 forwardInvocation。同时由于 forwardInvocation 的指向也被修改了,因此会转入新的 forwardInvocation 函数,在里面执行需要嵌入的附加代码,完成之后,再转回原来的 IMP。

Aspects hook的过程

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)) 
     {
           AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
           identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];

           if (identifier) 
         {
               [aspectContainer addAspect:identifier withOptions:options];

               // Modify the class to allow message interception.
               aspect_prepareClassAndHookSelector(self, selector, error);

           }//if (identifier) 

       }//if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) 

   });
   return identifier;
}

在没有hook之前,ViewController的SEL与IMP关系如下

hook之前.png

hook之后.png

最初的viewWillAppear: 指向了_objc_msgForward
增加了aspects_viewWillAppear:,指向最初的viewWillAppear:的IMP
最初的forwardInvocation:指向了Aspect提供的一个C方法__ASPECTS_ARE_BEING_CALLED__
动态增加了__aspects_forwardInvocation:,
指向最初的forwardInvocation:的IMP

然后,我们再来看看hook后,一个viewWillAppear:的实际调用顺序:

2.object收到selector(viewWillAppear:)的消息
2.找到对应的IMP:_objc_msgForward,执行后触发消息转发机制。
3.object收到forwardInvocation:消息
4.找到对应的IMP:__ASPECTS_ARE_BEING_CALLED__,执行IMP 

//__ASPECTS_ARE_BEING_CALLED__中的逻辑
1.向object对象发送aspects_viewWillAppear:执行最初的viewWillAppear方法的IMP
2.执行插入的block代码
3.如果ViewController无法响应aspects_viewWillAppear,则向object对象发送__aspects_forwardInvocation:来执行最初的forwardInvocation IMP

1、判断能否hook 对Class和MetaClass进行进行合法性检查,判断能否hook,规则如下 1).retain,release,autorelease,forwoardInvocation:不能被hook 2).dealloc只能在方法前hook 3).类的继承关系中,同一个方法只能被hook一次

2.创建AspectsContainer对象, 以"aspects_ "+ SEL为key,作为关联对象依附到被hook 的对象上

3.创建AspectIdentifier对象,并且添加到AspectsContainer对象里存储起来。这个过程分为两步 生成block的方法签名NSMethodSignature 对比block的方法签名和待hook的方法签名是否兼容(参数个数,按照顺序的类型) 4.根据hook实例对象/类对象/类元对象的方法做不同处理。

A)类方法来hook的时候,分为两步

1.hook类对象的forwoardInvocation:方法,指向一个静态的C方法,
2.并且创建一个aspects_ forwoardInvocation:动态添加到之前的类中

3.hook类对象的viewWillAppear:方法让其指向_objc_msgForward,
4.动态添加aspects_viewWillAppear:指向最初的viewWillAppear:实现

B)Hook实例的方法

Aspects支持只hook一个对象的实例方法

只不过在第4步略有出入,当hook一个对象的实例方法的时候:

1.新建一个子类,_Aspects_ViewController,并且按照上述的方式hook forwoardInvocation:

2.hook _Aspects_ViewController的class方法,让其返回ViewController
hook _Aspects_ViewController_MetaClass,让其返回ViewController

3.调用objc_setClass来修改ViewController的类为_Aspects_ViewController

这样做,就可以通过object_getClass(self)获得类名,然后看看是否有前缀类名来判断是否被hook过了

hook实例方法详解

TestClass *testObj = [[TestClass alloc] init];

    [testObj aspect_hookSelector:NSSelectorFromString(@"testSelector")
                     withOptions:AspectPositionBefore
                      usingBlock:^(id<AspectInfo> aspectInfo) {
             
                            NSLog(@"Hook testSelector");
                                                                       }
                           error:NULL];
    [testObj testSelector];

hook之前实例的状态.png

hook的过程: 1、通过statedClass = self.class获取self本来的class (class方法被重写了,用来获取self被hook之前的Class(Target))

2、通过Class baseClass = object_getClass(self)获取self的isa指针实际指向的class (self在运行时实际的class,表面上看这是一个西瓜(statedClass),实际上这是一个苹果(basedClass))

3、如果baseClass(实际指向的class)已经是被hook过的子类,则返回baseClass。

4.如果baseClass是MetaClass或者被KVO过的Class,则不必再生成subClass,直接在其自身上进行method swizzling。

5.如果不是上述3、4 所述情况,默认情况下需要对被hook的Class进行”isa swizzling”:

1)通过subclass = objc_allocateClassPair(baseClass, subclassName, 0)动态创建一个被hook类(TestClass)的子类(TestClass_Aspects); 2)然后对子类(TestClass_Aspects)的forwardInvocation:进行method swizzling,替换为_ASPECTS_ARE_BEING_CALLED_,进行消息转发时,实际执行的是_ASPECTS_ARE_BEING_CALLED_中的方法; 3)重写子类(TestClass_Aspects)的获取类名的方法class,使其返回被hook之前的类的类名(TestClass); 4)将self(TestObj)的isa指针指向子类(TestClass_Aspects)

object_setClass(self, subclass)
//object_setClass将一个对象设置为别的类类型,返回原来的Class

class被hook后的情况:

hook后.png