ios method swizzling

背景

最近在整理项目逻辑的时候,发现一个问题:就是打点统计,常常和代码业务逻辑混在了一块儿,耦合性很强,而且常常容易出错。因而就在思考怎样对这一块进行优化。html

其实,对这方面的讨论一直也比较多,好比继承基类,可是这样很容易使代码变得臃肿。另外一个比较好的办法就是利用 method swizzling, hook 住须要打点的方法,将打点统计从业务逻辑中分离出来,并且额外工做量不大。最后就想从这方面去尝试,固然并无本身造轮子,而是借用了 github 上的一个开源库,Aspects。这个库的代码量比较小,总共就一个类文件,使用起来也比较方便,好比你想统计某个 controller 的 viewwillappear 的调用次数,你只须要引入 Aspect.h 头文件,而后在合适的地方初始化以下代码便可。ios

#pragma mark - addKvLogAspect
    - (void)addKvLogAspect {
        //想法tab打开
        [self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
           //统计打点
            NSLog(@"");
        }error:NULL];
    }

oc的动态性及函数的调用

看到上面这段代码你们应该有所感受了,没错,它基本上就是基于 method swizzling 实现的。本篇文章暂时并不打算对 aspects 的代码进行解析(之后,可能会写一篇这样的文字),在这里就简单的记录一下我我的对于 method swizzling 的理解。git

ios 开发人员都知道, oc 是一门动态语言。这个动态性怎么理解呢,知乎上有网友这么总结过:github

  1. 类和对象都是 id , 在给你一个 id 的前提下没法直观的知道这个对象是类对象仍是类自己. 简单的能够简化成 runtime 管理的都是 id ( id 的本质实际上是 objc_object , objc_class 头部其实就是 id, 也就是 isa ).缓存

  2. Class 在 objc 中是动态建立的, selector、method、 imp、protocol 等都是随后绑定上去的(即所谓的运行时绑定).app

  3. 经过 runtime 可以查出当前运行时环境中全部的类, 每一个类中的方法, 每一个类消息的绑定, 每一个类的实现的协议, 每一个协议的定义, 每一个类当前的消息缓存等一切你想知道的东西.iview

  4. 类的方法(消息)调用是间接的.函数

动态性比较经常使用的地方就是你能够在运行时动态的改变函数调用的执行,能够给对象动态的添加函数,甚至动态生成一个全新的类。method swizzling 就是利用这个动态性,在运行时改变了函数调用的指向,从而使函数最终调用到本身定义的方法中去,那么,这个过程是怎样实现的呢?优化

了解 method swizzling 以前有必要先了解一下 oc 函数的调用过程,这里先简单介绍几个概念:ui

1) oc 的类是由 Class 类型来表示的,定义以下:

typedef struct objc_class *Class;

它实际上是一个指向 objc_class 的指针,结构体以下:

struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__

    Class super_class                       OBJC2_UNAVAILABLE;  // 父类

    const char *name                        OBJC2_UNAVAILABLE;  // 类名

    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0

    long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识

    long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小

    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表

    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表

    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存

    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表

#endif

} OBJC2_UNAVAILABLE;

2)类的实例 也是一个结构体 objc_object

struct objc_object {

    Class isa  OBJC_ISA_AVAILABILITY;

};

typedef struct objc_object *id;

这里也就是咱们说的 id 对象,oc 里面全部的对象都能用 id 表示。

这里的字段含义暂时不作过多的解释,有兴趣的同窗能够去网上找找。这里介绍两个属性值:

  • isa:在 oc 中,类自己也被当成一个对象来处理。对于一个实例对象而言,isa 指针指向了这个对象的类(上面的 objc_class ),而类的 isa 指针指向了它的元类 ( metaclass 元类其实也是一种 objc_class ).,关于metaclass能够参考这里的分析。

  • methodLists:方法列表,记录了全部的方法。(这里只是实例方法,类方法须要经过isa去元类中寻找)

另外oc中一个方法的调用有以下几个关键的部分:

  • sel又叫选择器,它表明了一个方法的selector的指针。selector用于表达运行时的方法的名字。

SEL sel = @selector(method);

sel在一个类中是惟一的,并且是彻底依赖方法名,也就是说下面两个函数

- (void)setDimension:(NSInteger)dimension {
}
 - (void)setDimension:(float)dimension {
}

会提示Duplicate declaration错误,由于尽管它们有不一样的参数类型,可是因为方法名彻底相同会致使sel相同,违背了sel惟一性的原则,这也是oc语法和其余语法的不一样。

  • IMP:一个函数指针,指向了方法实现的首地址

  • Method:它是类定义中表示方法的一个结构体,以下

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

有了上面的铺垫,就能更好的说明函数调用的整个过程了, 在 oc 中函数的调用形式是[ target * ],能够理解为[ receiver message ],也就是向 receiver 发送消息的过程。这个会被解析成以下形式 objc_msgSend(receiver,selector,arg1,...) ,也就是告诉 receiver ,我要发消息给你 selector 对应的方法, arg1 表示要传递给方法的参数。 receiver 收到这个通知后,会根据 objc_object 的(这里先以实例方法为例) isa 指针找到对象对应的 class 结构体,而后遍历 methodlist 找到 method ,最后经过 method 找到对应的 imp 指针,而后 根据 imp 指针找到最终的函数实现。固然具体细节要比这复杂的多,(好比为了提升效率,会对 method 进行缓存等等)。

swizzling method 实现

基于上面的一些基本了解以后,咱们设想一下,若是要在运行时动态的改变函数的调用,改怎么作呢?

上面咱们说过,调用函数时,会动态的根据 sel 寻找响应的 imp 指针,这就给了咱们启发,试想一下,若是咱们改变了 sel 和 imp 的对应关系,那么是否是也就意味着咱们改变了函数的调用关系? 接下来咱们能够用代码来验证。

仍是以 hook uiviewcontroller 的 viewwillappear 为例,实现以下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Class class = [self class];
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(swizzling_viewWillAppear:);
    
    //get method
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    /**
     * 这里实际上是在加了一个保护,若是class_addMethod返回no,说明originalSelector已经有存在的实现了,这个时候,咱们将
     originalMethod,swizzledMethod直接替换掉就号了,若是尚未对应的实现,那么直接添加进去,并更改原来swizzledSelector对应的实现
     */
    //exchange imp
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

// 咱们本身实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
- (void)swizzling_viewWillAppear:(BOOL)animated {
    // 咱们在这里加一个判断,将系统的UIViewController的对象剔除掉
    NSLog(@"swizzling_viewWillAppear");
    [self swizzling_viewWillAppear:animated];
}

- (void)viewWillAppear:(BOOL)animated {
    NSLog(@"viewWillAppear");
    [super viewWillAppear:animated];
}

运行结果以下

2016-06-30 13:56:31.186 Test[50266:6677359] swizzling_viewWillAppear
2016-06-30 13:56:31.187 Test[50266:6677359] viewWillAppear

和上面所说的函数的调用过程对比就会发现实际上是同样的。本质上就是在运行时,就是在运行时更改 sel 对应的 imp 的指向而已。不过这里有几点须要说明:

  1. 这个swizzling只更改本对象的方法的调用,并不会影响起父类,子类的调用状况。也就是在子类controller调用viewWillAppear仍是正常的调用viewWillAppear,可是,当调用[super viewWillAppear:animated]的时候,会调用到上面的 [self swizzling_viewwillAppear:animated].

  2. 细心的朋友或许会发现,上面swizzling_viewwillAppear的实现又调用了[self swizzling_viewwillAppear:animated] , 这样会不会造成循环调用了?其实不会,由于已经更改了@seletor(swizzling_viewwillAppear:)对应的imp,调用[self swizzling_viewwillAppear:animated],实际上至关于调用了[self viewWillAppear:animated],并不会造成循环调用。

相关文章
相关标签/搜索