Objetive-C中的方法调包(Method Swizzling)

LogicAetherX
• 阅读 3032

原文链接:http://nshipster.com/method-swizzling/

方法调包(Method Swizzling)应用于改变某个SEL(该SEL已有实现)的方法实现。这个技术让OC中的方法调用在运行时通过改变SEL与类分发表中的函数映射关系从而来指定你真正想要调用的函数。

举个栗子,我们视图统计我们XX应用中的每个视图控制器被弹出来几次【用户行为统计。。一般都有。。。】。

我们可能需要把统计代码添加到viewDidAppear:,这样会导致出现成吨的重复代码。继承可能是另外一个解决方案,但是需要各种各样的继承:UIViewController, UITableViewController, UINavigationController,与此同时你仍然无法避免重复代码。

幸好我们有另外一种解决方案:类别中的方法调包(method swizzling)。代码如下:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        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);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

现在,当任意UIViewController的实例或者子类调用viewWillAppear:时候便会打印一个LOG。

【注:用途】
往视图控制器的生命周期中注入行为,事件响应者,画图,或者加入网络层都是方法调包的用武之地。(当然还有许多使用场景,对于OC开发者来说也是装逼必备)。

继续来看应该怎么玩

+load vs. +initialize

调包的处理应该一直在+load中搞定

有两个方法是在OC运行时自动被调用的。一个是+load在每一个类在最初被加载的时候,而另外一个是+initialize在应用第一次调用这个类或者这个实例的方法的时候。它们都是可选的,只有在被调用方法是已实现的时候被执行。

因为方法调包影响全局状态,所以最小化冲突的概率十分重要。+load在类初始化期间是保证被加载的,在改变系统范围的行为而言提供了一点一致性。相反的是+initialize在将被执行的时候没有这样的保障,事实上,它可能不被调用,如果应用不直接给这个类发消息的话。

dispatch_once

调包应该一直在dispatch_once中进行处理

再次声明因为方法调包的影响是全局性的,我们需要在运行时采取能够想到的预防措施。原子性就是这样的一个预防,保证代码仅被执行一次,即便是跨线程。GCD的dispatch_once提供了我们想要原子性与执行唯一性。

Selectors, Methods, & Implementations

在OC中,selectorsmethods以及implementations涉及到运行时中比较特殊的一块领域,尽管在大部分场景这些东西都可以互通,通常是指消息发送的过程。

引用下苹果官方说明

  • Selector (typedef struct objc_selector *SEL): Selectors are used to represent the name of a method at runtime. A method selector is a C
    string that has been registered (or "mapped") with the Objective-C
    runtime. Selectors generated by the compiler are automatically mapped
    by the runtime when the class is loaded .

    • Method (typedef struct objc_method *Method): An opaque type that represents a method in a class definition.
    • Implementation (typedef id (*IMP)(id, SEL, ...)): This data type is a pointer to the start of the function that implements the method.
      This function uses standard C calling conventions as implemented
      for the current CPU architecture. The first argument is a pointer
      to self (that is, the memory for the particular instance of this
      class, or, for a class method, a pointer to the metaclass). The
      second argument is the method selector. The method arguments
      follow.

最好的理解这些概念的方式如下:
一个类(Class)维护着一张(方法)分发表来在运行时处理消息分发;表中的每个入口都是一个方法(Method),其映射从一个专有的名字,也就是选择器(SEL)到一个具体实现(IMP),也就是指向C函数的一个指针。

调包一个方法就是该笔爱你一个类得分发表也就是调包方式改变SEL与IMP的映射关系。
【ASEL -> AIMP BSEL -> BIMP 调包后 ASEL -> BIMP BSEL -> AIMP】

调用_cmd

下列代码似乎会导致死循环:

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

但是好神奇啊,居然不会耶。在调包的处理中,xxx_viewWillAppear:被重新分配到UIViewController -viewWillAppear:的原始实现。递归调用是会问题的,但是在这个场景是没问题的哦,因为在运行时的时候xxx_viewWillAppear:真正的实现已经被调包了。(意译)

这里有个代码规范,记得在调包方法名字中加前缀,同理在其他类别中。

思索

调包一直被认作为一种巫术技术(黑魔法),容易导致不可预料的行为和结果,尽管不是最安全的,但是如果你遵循如下法则,安全性还是可以接受的:

  • Always invoke the original implementation of a method (unless you have a good reason not to)
  • Avoid collisions
  • Understand what's going on
  • Proceed with caution

总结是Method Swizzling好用但要少用。。。。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
3年前
Python+Selenium自动化篇
本篇文字主要学习selenium定位页面元素的集中方法,以百度首页为例子。0.元素定位方法主要有:id定位:find\_element\_by\_id('')name定位:find\_element\_by\_name('')class定位:find\_element\_by\_class\_name(''
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
Java日期时间API系列30
  实际使用中,经常需要使用不同精确度的Date,比如保留到天2020042300:00:00,保留到小时,保留到分钟,保留到秒等,常见的方法是通过格式化到指定精确度(比如:yyyyMMdd),然后再解析为Date。Java8中可以用更多的方法来实现这个需求,下面使用三种方法:使用Format方法、 使用Of方法和使用With方法,性能对比,使用
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这