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<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

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

aop1.png

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

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 第1章 熟悉Objective-C 第1条 了解Objective-C语言的起源 Objective-C是一种“消...
    __silhouette阅读 618评论 0 4
  • 讲个吓人的故事 昨天晚上小为同学睡前还在念叨圣诞老人什么时候来啊,怎么来啊。等他睡着了,我把礼物摆在圣诞树下。 早...
    小为的日常阅读 135评论 0 0
  • 去年这个时候,他在西藏,天高蓝阔,他说牦牛很丑,酥油茶不好喝 今年这个时候,他去了内蒙,我以为草原无边...
    朝酒阅读 273评论 0 0