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

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

推荐阅读更多精彩内容