OC方法交换swizzle详细介绍——不再有盲点

这篇具有很好参考价值的文章主要介绍了OC方法交换swizzle详细介绍——不再有盲点。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

原文链接:https://www.cnblogs.com/mddblog/p/11105450.html

如果对方法交换已经比较熟悉,可以跳过整体介绍,直接看常见问题部分

整体介绍

方法交换是runtime的重要体现,也是"消息语言"的核心。OC给开发者开放了很多接口,让开发者也能全程参与这一过程。

原理

oc的方法调用,比如[self test]会转换为objc_msgSend(self,@selfector(test))。objc_msgsend会以@selector(test)作为标识,在方法接收者(self)所属类(以及所属类继承层次)方法列表找到Method,然后拿到imp函数入口地址,完成方法调用。

typedef struct objc_method *Method;

// oc2.0已废弃,可以作为参考
struct objc_method {
    SEL _Nonnull method_name;
    char * _Nullable method_types;
    IMP _Nonnull method_imp;
}

基于以上铺垫,那么有两种办法可以完成交换:

  • 一种是改变@selfector(test),不太现实,因为我们一般都是hook系统方法,我们拿不到系统源码,不能修改。即便是我们自己代码拿到源码修改那也是编译期的事情,并非运行时(跑题了。。。)
  • 所以我们一般修改imp函数指针。改变sel与imp的映射关系;
系统为我们提供的接口

typedef struct objc_method *Method;Method是一个不透明指针,我们不能够通过结构体指针的方式来访问它的成员,只能通过暴露的接口来操作。

接口如下,很简单,一目了然:

#import <objc/runtime.h>

/// 根据cls和sel获取实例Method
Method _Nonnull * _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);

/// 给cls新增方法,需要提供结构体的三个成员,如果已经存在则返回NO,不存在则新增并返回成功
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)

/// method->imp
IMP _Nonnull method_getImplementation(Method _Nonnull m);

/// 替换
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types)

/// 跟定两个method,交换它们的imp:这个好像就是我们想要的
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
简单使用

假设交换UIViewController的viewDidLoad方法

/// UIViewController 某个分类

+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    method_exchangeImplementations(originMethod, swizzledMethod);
}

+ (void)load {
    [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
}
/// hook
- (void)swizzle_viewDidLoad {
    [self swizzle_viewDidLoad];
}

交换本身简单:原理简单,接口方法也少而且好理解,因为结构体定义也就三个成员变量,也难不到哪里去!

但是,具体到使用场景,叠加上其它外部的不稳定因素,想要稳定的写出通用或者半通用交换方法,上面的"简单使用"远远不够的。

下面就详细介绍下几种常见坑,也是为啥网上已有很多文章介绍方法交换,为什么还要再写一篇的原因:不再有盲点

常见问题一、被多次调用(多次交换)

"简单使用"中的代码用于hook viewDidload一般是没问题的,+load 方法一般也执行一次。但是如果一些程序员写法不规范时,会造成多次调用。

比如写了UIViewController的子类,在子类里面实现+load方法,又习惯性的调用了super方法

+ (void)load {
    // 这里会引起UIViewController父类load方法多次调用
    [super load];
}

又或者更不规范的调用,直接调用load,类似[UIViewController load]

为了没盲点,我们扩展下load的调用:
  • load方法的调用时机在dyld映射image时期,这也符合逻辑,加载完调用load。
  • 类与类之间的调用顺序与编译顺序有关,先编译的优先调用,继承层次上的调用顺序则是先父类再子类;
  • 类与分类的调用顺序是,优先调用类,然后是分类;
  • 分类之间的顺序,与编译顺序有关,优先编译的先调用;
  • 系统的调用是直接拿到imp调用,没有走消息机制;

手动的[super load]或者[UIViewController load]则走的是消息机制,分类的会优先调用,如果你运气好,另外一个程序员也实现了UIViewController的分类,且实现+load方法,还后编译,则你的load方法也只执行一次;(分类同名方法后编译的会“覆盖”之前的)

为了保险起见,还是:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
    });
}

继续扩展:多次调用的副作用是什么呢?
  • 根据原理,如果是偶数次

结果就是方法交换不生效,但是有遗留问题,这时手动调用

- (void)swizzle_viewDidLoad {
    [self swizzle_viewDidLoad];
}

会引起死循环。

其实,方法交换后,任何时候都不要尝试手动调用,特别是交换的系统方法。实际开发中,也没人会手动调用,这里我们只讨论这种场景的技术及后果,帮助理解

  • 奇数次调用

奇数次之后一切正常。但是,奇数次之前,它会先经历偶数次。

比如,第一次交换,正常,第二次交换,那么相当于没有交换,如果你手动调用了swizzle_viewDidLoad,很明显死循环了,然后你又在其它线程进行第三次交换,又不死循环了。哈哈,好玩,但你要保重,别玩失火了玩到线上了!!!

这种情况还是有可能发生的,比如交换没有放在load方法,又没有dispatch_once,而是自己写了个类似start的开始方法,被自己或者他人误调用。

最后:为了防止多次交换始终加上dispatch_once,除非你清楚你自己在干啥。

再次扩展:常见的多次交换

这里说的多次交换,和上面说的不一样,交换方法不一样,比如我们开发中经常遇到的。

我们自己交换了viewDidLoad,然后第三方库也交换了viewDidLoad,那么交换前(箭头代表映射关系):

sysSel -> sysImp
ourSel -> ourImp
thirdSel -> thirdImp

第一步,我们与系统交换:

sysSel -> ourImp
ourSel -> sysImp
thirdSel -> thirdImp

第二步,第三方与系统交换:

sysSel -> thirdImp
ourSel -> sysImp
thirdSel -> ourImp

假设,push了一个VC,首先是系统的sysSel,那么调用顺序:

thirdImp、ourImp、sysImp

没毛病!

多次交换这种场景是真实存在的,比如我们监控viewDidload/viewWillappear,在程序退到后台时,想停止监控,则再进行一次(偶数)交换也是一种取消监控的方式。当再次进入前台时,则再次(奇数)交换,实现监控。(通过标志位实现用的更多,更简单)

问题二、被交换的类没有实现该方法

我们还是在分类里面添加方法来交换

情况一:父类实现了被交换方法

我们本意交换的是子类方法,但是子类没有实现,父类实现了class_getInstanceMethod(target, swizzledSelector);执行的结果返回父类的Method,那么后续交换就相当于和父类的方法实现了交换。

一般情况下也不会出问题,可是埋下了一系列隐患。如果其它程序员也继承了这个父类。举例代码如下

/// 父类
@interface SuperClassTest : NSObject
- (void)printObj;
@end
@implementation SuperClassTest
- (void)printObj {
    NSLog(@"SuperClassTest");
}
@end

/// 子类1
@interface SubclassTest1 : SuperClassTest
@end
@implementation SubclassTest1
- (void)swiprintObj {
    NSLog(@"printObj");
}
@end

/// 子类1 分类实现交换
@interface SubclassTest1(swiTest)
@end
@implementation SubclassTest1(swiTest)
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    method_exchangeImplementations(originMethod, swizzledMethod);
}
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:[SubclassTest1 class] original:@selector(printObj) swizzled:@selector(swiprintObj)];
    });
}

- (void)swiprintObj {
    NSLog(@"swi1:%@",self);
    [self swiprintObj];
}
@end

/// 子类2
@interface SubclassTest2 : SuperClassTest
@end
@implementation SubclassTest2
/// 有没有重写此方法,会呈现不同的结果
//- (void)printObj {
//    // 有没有调用super  也是不同的结果
//    [super printObj];
//    NSLog(@"printObj");
//}
@end

示例代码,实现了printObjswiprintObj的交换。

  • 问题1:父类的实例对象调用selector(printObj),也会造成imp(swiprintObj)优先调用,但是父类并没有实现swiprintObj,因此会造成找不到方法崩溃;
  • 问题2:假设sub2(子类2)没有实现printObj,但它的实例对象也调用了selector(printObj),正常应该是能够调用父类的imp(printObj)方法,但是由于被交换,会尝试调用[sub2 swiprintObj],因为 swiprintObj的实现里面有[self swiprintObj],这里的self是sub2,sub2和super都没有实现swiprintObj的,所以方法找不到崩溃。
  • 问题3:sub2子类重写了printObj,一切正常,sub2实例对象调用正常,但是如果在sub2的imp(printObj)里面调用super方法还是会崩溃,原因是selector(printObj)会尝试调用imp(swiprintObj),但是sub2 和 super都没有实现该方法

那么如何避免这种情况呢?

使用class_addMethod方法来避免。再次优化后的结果:

+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    }
    else {
        method_exchangeImplementations(originMethod, swizzledMethod);
    }
}

分步骤详细解析如下:

  • class_addMethod 执行前

superSel -> superImp
sub1SwiSel -> sub1SwiImp

  • class_addMethod 执行后,给子类增加了sel,但是对应的imp实现还是swizzledMethod的imp即交换方法的imp

superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> sub1SwiImp

被交换的方法sub1Sel已经指向了交换方法的imp实现,下一步将交换方法的sel 指向被交换方法的imp即可。被交换方法不是没有实现吗??? 有的,OC继承关系,父类的实现就是它的实现superImp

  • class_replaceMethod,将sub1SwiSel的实现替换为superImp

superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> superImp

系统在给对象发送sel消息时,执行sub1SwiImp,sub1SwiImp里面发送sub1SwiSel,执行superImp,完成hook。

我们说的给子类新增method,其实并不是一个全新的,而是会共享imp,函数实现没有新增。这样的好处是superSel对应的imp没有改变,它自己的以及它的其它子类不受影响,完美解决此问题;但是继续往下看其它问题

情况2:父类也没有实现

尴尬了,都没有实现方法,那还交换个锤子???

先说结果吧,交换函数执行后,方法不会被交换,但是手动调用下面这些,同样会死循环。

- (void)swiprintObj {
    NSLog(@"swi1:%@",self);
    [self swiprintObj];
}

所以我们要加判断,然后返回给方法调用者一个bool值,或者更直接一点,抛出异常。

/// 交换类方法的注意获取meta class, object_getClass。class_getClassMethod
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    if (originMethod && swizzledMethod) {
        if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
            class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
        }
        else {
            method_exchangeImplementations(originMethod, swizzledMethod);
        }
    }
    else {
        @throw @"originalSelector does not exit";
    }
}

再加上 dispatch_once 上面已经算是比较完美了,但是并没有完美,主要是场景不同,情况就不同。我们只有理解原理,不同场景不同对待。

新建类来交换系统方法

上面说的都是在分类里面实现交换方法,这里新建"私有类"来交换系统方法。

在写SDK时,分类有重名覆盖问题,编译选项还要加-ObjC。出问题编译阶段还查不出来。那么我们可以用新建一个私有类实现交换,类重名则直接编译报错。交换方法和上面的分类交换稍不一样

比如hook viewDidload,代码如下:

@interface SwizzleClassTest : NSObject
@end

@implementation SwizzleClassTest
+ (void)load {
    /// 私有类,可以不用dispatch_once
    Class target = [UIViewController class];
    Method swiMethod = class_getInstanceMethod(self, @selector(swi_viewDidLoad));
    Method oriMethod = class_getInstanceMethod(target, @selector(viewDidLoad));
    if (swiMethod && oriMethod) {
        if (class_addMethod(target, @selector(swi_viewDidLoad), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod))) {
            // 这里获取给UIViewController新增的method
            swiMethod = class_getInstanceMethod(target, @selector(swi_viewDidLoad));
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    }
}

- (void)swi_viewDidLoad {
    // 不能调用,这里的self是UIViewController类或者子类的实例,调用test的话直接崩溃。或者做类型判断 [self isKindOfClass:[SwizzleClassTest class]],然后再调用
    // [self test];
    [self swi_viewDidLoad];
}

- (void)test {
    NSLog(@"Do not do this");
}

@end

这里也用到class_addMethod,给UIViewController新增了一个swi_viewDidLoad sel及其imp实现,共享了SwizzleClassTest 的imp实现。

另外系统发送viewdidload消息进而调用swi_viewDidLoad方法,里面的self是UIViewController,所以不能再[self test],否则崩溃。也不能在其它地方手动[self swi_viewDidLoad];会死循环,因为这时候self是SwizzleClassTest,而它的method是没有被交换的,好处是我们可以通过self的类型判断来避免。

可以比较下交换前后,

交换前:

SwizzleClassTest_swi_viewDidLoadSel -> SwizzleClassTest_swi_viewDidLoadImp

UIViewController_viewDidLoadSel -> UIViewController_viewDidLoadImp

交换后:

SwizzleClassTest_swi_viewDidLoadSel -> SwizzleClassTest_swi_viewDidLoadImp

UIViewController_swi_viewDidLoadSel -> UIViewController_viewDidLoadImp
UIViewController_viewDidLoadSel -> UIViewController_swi_viewDidLoadImp

可以看出 SwizzleClassTest 没有受影响,映射关系不变。

这种想取消的话,也很简单method_exchangeImplementations

最后补充一点:C函数 实现交换

这里讲的是用C函数交换系统类的方法。而不是fishhook的hook C的函数,目标不一样。原理也不一样

还以hook UIViewControllerviewDidLoad为例

上面说到,oc方法调用会转换为objc_msgSend(self,_cmd,param)这种形式,这里再补充一点,objc_msgSend找到imp函数指针后,最终会是imp(self,_cmd,param)调用C函数,imp其实就是个C函数指针。

那么我们可以定义一个C函数,让sel和我们新建的C函数(imp)形成映射。另外还需要记录之前的imp实现,可以定义一个函数指针来保存sel之前的imp实现;大概示意:

之前:
pOriImp = NULL
vcSel -> vcImp
Cfun(){};

之后:

pOriImp = vcImp;
vcSel -> cFun;// 函数名即为函数指针

详细如下:

/// 准备1. 定义一个函数指针,用于记录系统原本的IMP实现,并初始化为NULL
void (*origin_test_viewDidload)(id,SEL) = NULL;

/// 准备2. 定义要交换的函数,里面会调用系统的IMP
static void swizzle_test_viewDidload(id self, SEL _cmd)
{
    // 这里打印的self为UIViewController或者子类实例
    NSLog(@"%@",self);
    if (origin_test_viewDidload) {
        origin_test_viewDidload(self, _cmd);
    }
}

/// 开始交换。startHook可以是某个类的方法或实例方法或C函数都可以
+ (void)startHook {
   static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class target = [UIViewController class];
        SEL oriSel = @selector(viewDidLoad);
        // 要交换的函数
        IMP swiImp = (IMP)swizzle_test_viewDidload;
        Method origMethod = class_getInstanceMethod(target, oriSel);
        // 替换之前的先保留
        origin_test_viewDidload = (void *)method_getImplementation(origMethod);
        if (origin_test_viewDidload) {
            // 最后替换,这里用到了set
            method_setImplementation(origMethod, swiImp);
        }
    });
}

这种hook,没有给类的MethodList新增Method,只是替换了实现,对原类改动最小。

和其它hook方式一样,这种对第三方库 的hook,也是不影响。如果第三方库也交换了,均会得到调用

最后,如果你想取消hook,很简单,method_setImplementation为原来的IMP即可。记着把origin_test_viewDidload也置为NULL.

总结

  • 首先要知道方法交换的原理;
  • 熟悉它常用接口;
  • 被交换方法不存在引发的 父类、子类问题;
  • 以及oc中方法的继承、“覆盖”问题;
  • 可能引发重复交换的问题,以及后果;
  • 理解self只是个隐藏参数,并不一定是当前方法所在的类的实例对象

最后,大概三类hook,至于想用哪种,其实无所谓了,看具体场景。但是原理一定要清楚,每次hook时,都要认真推演一遍,计算下可能产生的影响。文章来源地址https://www.toymoban.com/news/detail-807292.html

到了这里,关于OC方法交换swizzle详细介绍——不再有盲点的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 超低时延交换机,让Infiniband交换机不再是唯一的选择

    近期与同事去某物理所技术交流,满机房的都是IB 交换机,十分壮观。 然而,客户这次叫我们来,目的却是想换掉他们,一台不留。客户无奈地说,IB 交换机的时延方面,没得说,但是“想买到”太难了,由于IB 交换机只支持IB 通信协议,所以每当有扩容的需求时,也只能继

    2024年02月04日
    浏览(33)
  • 一篇文章给你详细介绍交换机堆叠

    1.什么是堆叠? 简单来说,堆叠就是将多个设备的控制平面整合,虚拟成一台逻辑设备,这台设备具有统一的管理地址,L2和L3层协议对外也表现成一台设备。 2.为什么使用堆叠?(优缺点) 优点:扩展接入端口、高可用性、统一管理。 缺点:只能同厂商型号进行堆叠,升级改

    2024年02月04日
    浏览(79)
  • 如何设置网络交换机 网络交换机具体设置方法介绍

    网络交换机的设置方法那是比较多的,要视具体情况而定,本篇文章就不作具体介绍了,本文将要对交换机配置的常见应用--VLAN网络划分、配置方法进行详细介绍,希望大家能认真阅读学习。 如果为交换机配置了名称,则也可以直接在”Telnet“命令后面空一个空格后输入

    2024年02月06日
    浏览(43)
  • H3C和CISCO交换机做聚合配置详细介绍

    一、网络环境 本网络是广东粤电大厦,3台交换机做端口聚合的课题 网络环境:H3C_A交换机H3C_B交换机cisco(3560)交换机 H3C_B交换机做为中心交换机 由于带宽需要,加上该9楼上了很多美国著名的avocentKVM的监控设备,需要很大的带宽,因此做了4组的端口链路聚合的需要 因此以下

    2024年02月05日
    浏览(50)
  • 初始化交换机的密码的方法介绍

    1、断开交换机的电源并重新给交换机加电,在给交换机加电的同时按住交换机前面板上的“模式(Mode)”按钮几秒钟,仔细观察超级终端程序中显示的交换机启动信息。 2、待出现交换机提示符switch:后,输入flash_init命令。 3、待上述命令执行完成后,输入load_helper命令。 4、待

    2024年02月05日
    浏览(39)
  • 交换机下以拓扑结构接无线路由器的相关布线和设置图文详细介绍

    假设某一网络拓扑结构如下图所示,该网络使用DSL/Cable Modem/LAN等方式连接到Internet,由Modem或路由器共享Internet连接,内部网络则通过交换机连接起来,以实现多台电脑接入上网。假设交换机已开启DHCP功能自动分配IP地址,IP段是192.168.1.0,则以太网的电脑A、B、C的IP为192.168.

    2024年02月06日
    浏览(45)
  • 拓扑优化丨99行拓扑优化详细解释(OC优化准则)

    设计域离散化初始化,有限元分析,灵敏度分析,网格过滤,OC优化准则设计变量 1 %%%% A 99 LINE TOPOLOGY OPTIMIZATION CODE BY OLESIGMUND, OCTOBER 1999 %%%  99行程序代码 2 function top(nelx,nely,volfrac,penal,rmin);水平方向上的离散单元,竖直方向的离散单元,材料体积与设计域体积之比,惩罚因子

    2023年04月20日
    浏览(37)
  • 新安装的交换机连接路由器使用,详细配置方法

    1.确定交换机的IP地址 在默认情况下,交换机可能具有默认的IP地址,例如192.168.0.1或192.168.1.1。如果不知道它是什么,请查看交换机说明书或联系制造商以获取帮助。 2.连接电缆 使用网线将交换机端口连接到路由器端口。确保电缆插入正确的端口,并且连接牢固。 3.访问交换

    2024年02月11日
    浏览(38)
  • 方法的详细介绍

    前言: 👏作者简介:我是阿柽,一名热爱技术的在校学生。 📕系列专栏:JAVA专栏 📧如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀 🔥如果感觉博主的文章还不错的话,👍点赞👍 + 👀关注👀 + 🤏收藏🤏  目录 一、方法概述 1.1 方法是什么 1.2 方

    2024年02月02日
    浏览(31)
  • springboot 异步执行方法详细介绍

            在Spring Boot中,异步执行方法是一种提高应用程序性能和响应性的技术。通过异步执行,你可以在处理耗时的业务逻辑时,不需要阻塞当前线程,从而提高应用程序的吞吐量和并发处理能力。         基本概念         在Spring中,异步执行通常是通过`@Asy

    2024年02月21日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包