objective-c AOP框架 - Aspects源码解析

先看几个概念:


代理:每个业务类都存在核心代码(当且业务独有的逻辑)和非核心代码(每个业务都可能有的通用逻辑)。目标类只完成核心业务,将非核心代码交给代理类完成具体实现。使核心逻辑更加简介、清晰。目标类和代理类要在编译期确定绑定关系。
动态代理:代理类完成通用非核心业务,在运行期动态绑定目标类。不需要修改核心代码,避免一个类对应一个代理,统一代理类对目标类的处理过程,减少重复代码。例如给一些类加统一的权限控制;日志系统;监控系统等都可以用动态代理实现。
AOP(Aspect Oriented Programming):面向切面编程,在不修改核心代码的情况下运行期给原程序添加额外的功能,使核心业务逻辑的附加功能完全隔离开,降低模块耦合度。动态AOP一般用动态代理的思想实现。如性能监测;访问控制;事务管理以及日志记录;线上热修复工具等,都参考了AOP设计。


isa:运行时指向当前对象的类对象。
Class:一个类也是一个对象,描述了一个类的所有实例方法、协议和属性。
meta-class:类对象的类,描述了一个类的所有类方法。class.png
method:一个包含了方法名、参数和返回值类型、imp的结构体。
imp:方法的真正实现(函数指针)。
type encoding:用字符串描述OC所有的数据类型。
selector:OC方法名(包括参数名和参数个数)。
NSMethodSignature:方法签名,记录了一个方法的返回值类型类型和参数。用于创建NSInvocation 。
NSInvocation:OC方法的静态描述,Invocation对象包含了OC方法所有的参数和返回值,用于消息转发(Invocation可以分发到其他target,所有参数都可以修,selector也可以转成另外一个具有相同方法签名的selector)。
方法调用:其实是对objc_msgSend(id self, SEL op, ...)的函数调用,根据self和SEL,先在当前class的方法缓存cache中匹配SEL,再从class的methodLists中匹配,没有找到再去父类中找,直到NSObject,如果都没有找到imp,则走消息转发流程。oc_message.png
消息转发:动态方法解析(动态添加一个方法来响应当前的消息)、快速消息转发(将该条消息发送给其他接收者来处理)、标准消息转发(不仅可以更改消息接受者,还可以通过invocaton更该方法参数和返回值,甚至修改SEL)、抛出异常。msg_forawrd.png

OC中所有具有isa和super_class指针的结构体都是一个对象,类本身也是一个对象,可以在运行时动态创建类对象。我们在运行时能够动态更改一个实例的行为,给他添加新方法或者修改原有方法,甚至修改类的指向,其实都是修改了类所指向的对象,或者修改了类的实例的isa指针,使他指向了其它的类对象。

Aspects正是利用了OC的这个特性,给我们提供了两个接口

+ (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;

第一个类方法:可以动态修改原有类中定义好的方法实,当被修改的类生成实例之后,就具有新的行为,实现了AOP设计。

objective-c AOP框架 - Aspects源码解析_第1张图片
aop1.png

第二个实例方法:可以在类生成实例之后继承之前的类动态创建一个子类,并且修改子类中的方法,创建子类对象,再将原来类实例的isa指向子类对象,通过block将原类的invocation和实参回调给用户。可以实现动态修改一个对象的行为。
以上两个接口的区别是:类方法直接修改了原来的类,该类生成的所有实例都将被修改。第二个实例方法只修改了这个类当前的实例,不会对类其它实例造成影响。

objective-c AOP框架 - Aspects源码解析_第2张图片
aop2.png

Aspects实现流程


1、 Check against the blacklist

  1) 下面这些方法不会hook。 
     @"retain"
     @"release",
     @"autorelease"
     @"forwardInvocation:"

  2) 对@"dealloc"进行hook只能使用  
     AspectPositionBefore

  3) 不存在的方法不会hook。
     如果是meta class还会执行4) 5)
     如果hook的是instance执行跳到第2步

  4) 便利当前类和父类直到NSObject
     定位到需要hook的方法所在的类,在
     缓存(swizzledClassesDict)中查该
     方法是否已经被hook。一个类中的一
     个方法,只能被hook一次。

  5) 再次便利当前类和父类
     将需要hook的方法做标记
     存在缓存(swizzledClassesDict)中。   

  ps:4、5两步是否存在性能问题?

2、 生成AspectsContainer

  1) 给原方法起别名(加aspects__前缀)
     生成AspectsContainer实例。

  2) 以别名为key,将生成AspectsContainer
     作为一个属性添加给当前类。

3、 生成blockSignature

  1) 解析调用者传进来的block
    (block的结构比较复杂,要另外讨论)

  2) 如果调用者没有传入id则不需要生成方法签名

  3) 如果传入了id,则生成方block法签名。

  4) 根据原方法的@selector生成方法签名signature
     并且和blockSignature做匹配

    a.对比参数个数

    b.只校验原方法的signature和blockSignature
      参数类型和个数是否一致。

    c.前2个参数不一样,分别是:
      argument 0(@ 代表self或者block)     
      argument 1 (: 代表selector)。

  5) 校验结束,根据原方法
     selector,block,blockSignature,
     options,self(类或者实例,弱引用)
     生成AspectIdentifier可用于取消hook。

  6) 将生成好的AspectIndetifer对象
     加入第2步生成的AspectsContainer对象中

4、 修改原类,对其hook的方法进行消息转发拦截

  1) 准备hook原类(如果是调用的是类方法则做消息转发拦截,如果是实例方法会动态生成子类)

    a.检测原class是否已经存在_Aspects_前缀
      有的话直接返回原来的class

    b.判断是hook原类还是hook类的实例

      1. hook meta_class

         a) 将原类forwardInvocation方法IPM
            替换成__ASPECTS_ARE_BEING_CALLED__

         b) 如果class_replaceMethod返回原IMP
            在添加一个__aspects_forwardInvocation方法
            方法IMP就是原来forwardInvocation的IMP
            作用是调用unregister的时候在把原来的forwardInvocation方法替换回来
            但这里测试下来发现class_replaceMethod没有返回原IMP?

         c) !!!运行时(其它都是非运行时对class的操作): 

              1)__aspects_forwardInvocation用原方法的invocation
               将invocation的实参替换blockInvocation的参数 

              2).将原方法Invocation、当前self、方法参数列表
                 包装成id对象
                 塞给blockInvocation的第一个参数
                 调用者可以通过AspectInfo在block中取到原方法的Invocation和参数

              3)方法执行,三种模式,调用顺序:
                AspectPositionBefore:执行block、执行原方法invocation。    
                AspectPositionAfter: 执行原方法invocation,执行block。     
                AspectPositionInstead:执行block、要手动执行原方法的invocation。

              4) 执行过程中如果aspectsContainer中找不到需要替换的方法
                 调__aspects_forwardInvocation(IMP:原forwardInvocation)走消息转发
                 forwardInvocation无法响应则掉用doesNotRecognizeSelector
                 !!!b)流程class_replaceMethod的一个bug,它没有返回
                 原forwardInvocation方法的IMP导致__aspects_forwardInvocation的
                 IMP为空,因此一定会走doesNotRecognizeSelector。
                 ps:如果使用了像《JSPatch》这种也将hook的方法换行成forwardInvocation
                 走到这里就会Crash。

       2. hook instance

         a) 根据原类,创建一个名为(originalClassName__Aspects_)的新class和metaclass,并创建类对象

         b) 将新类forwardInvocation方法IPM替换成__ASPECTS_ARE_BEING_CALLED__。

         c) 替换新类的class方法,新IMP是一个"Class (^block)(id self)";
            这个block返回原类的Class;
            替换后新类的实例调用getClass方法时返回的是原类的Class;
            因为self虽然赋值给了新生成的Class;
            但是self的MethodList中记录的Class,SEL,IMP对应关系没变;
            要通过class_getInstanceMethod获取SEL的Method;
            或者instanceMethodSignatureForSelector获取方法MethodSignature
            还需要用原Class。

         d) 替换新类的class方法,同:c)。

         e) 注册新class;
            调用object_setClass方法将当前self实例赋值给成新生成的类。;
            此时新的class的实例已经变成self;
            self的内存地址没变只是类的结构变成了新生成的Class。

         f) !!!运行时(其它都是非运行时对class的操作):
            同meta_class方法

  2) 在全局集合swizzledClasses中加入原类名,做缓存使用。并且返回上一步生成的class。

  3) 根据上一步返回的class,获取原方法的IMP。

  4) 判断上一步的IMP是否是_objc_msgForward
     如果是则,调用原方法就是直接会走消息转发流程,如果不是会进行下一步

  5) 给class新增一个aspects_selector指向原IMP(unregister时候使用)。

  6) 将原IMP替换成_objc_msgForward(作用是:调用原方法直接进行消息转发)。

你可能感兴趣的:(objective-c AOP框架 - Aspects源码解析)