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)printObj {
NSLog(@"printObj");
}
@end
/// 子类2
@interface SubclassTest2 : SuperClassTest
@end
@implementation SubclassTest2
/// 有没有重写此方法,会呈现不同的结果
- (void)printObj {
// 有没有调用super 也是不同的结果
[super printObj];
NSLog(@"printObj");
}
@end
/// 子类1 分类实现交换
+ (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];
}
示例代码,实现了printObj
与 swiprintObj
的交换。
- 问题1:父类的实例对象正常调用printObj,也会造成swiprintObj优先调用,然后再调用printObj,这不是我们想要的,如果你想监控父类,那么完全可以直接交换父类的方法;
- 问题2:假设sub2(子类2)没有实现printObj,但它的实例对象也调用了printObj,正常应该是能够调用父类的printObj方法,但是由于被交换,会调用sub1的swiprintObj,swiprintObj的实现里面有
[self swiprintObj]
,这里的self是sub2,sub2是没有实现swiprintObj
的,直接崩溃。 - 问题3:sub2子类重写了printObj,一切正常,sub2实例对象调用正常,但是如果在printObj里面调用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 UIViewController
的viewDidLoad
为例
上面说到,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时,都要认真推演一遍,计算下可能产生的影响。
OC方法交换swizzle详细介绍——不再有盲点的更多相关文章
- iOS开发——实用OC篇&多种定时器详细介绍
多种定时器详细介绍 在软件开发过程中,我们常常需要在某个时间后执行某个方法,或者是按照某个周期一直执行某个方法.在这个时候,我们就需要用到定时器. 然而,在iOS中有很多方法完成以上的任务,到底有 ...
- react-native热更新之CodePush详细介绍及使用方法
react-native热更新之CodePush详细介绍及使用方法 2018年03月04日 17:03:21 clf_programing 阅读数:7979 标签: react native热更新co ...
- 04-vi使用方法详细介绍
vi使用方法详细介绍 vi编辑器是所有Unix及Linux系统下标准的编辑器,它的强大不逊色于任何最新的文本编辑器,这里只是简单地介绍一下它的用法和一小部分指令.由于对Unix及Linux系统的任何版 ...
- WQL语言简介和WQL测试工具wbemtest.exe使用方法详细介绍
这篇文章主要介绍了WQL语言简介和WQL测试工具wbemtest.exe使用方法详细介绍,WQL是指Windows管理规范查询语言,需要的朋友可以参考下 WQL就是WMI中的查询语言,WQL的全称是W ...
- 在vs2010中编译log4cxx-0.10.0详细方法(从下载、编译、解决错误详细介绍)
在vs2010中编译log4cxx-0.10.0详细方法(从下载.编译.解决错误详细介绍) http://blog.sina.com.cn/s/blog_a459dcf501013tbn.html
- PHP之十六个魔术方法详细介绍
PHP中把以两个下划线__开头的方法称为魔术方法(Magic methods),这些方法在PHP中充当了举足轻重的作用.这里进行详细介绍,感兴趣的小伙伴们可以参考一下. PHP中把以两个下划线__开头 ...
- python模块之datetime方法详细介绍
datetime Python提供了许多内置模块用于操作时间日期,如calendar,time,datetime,这篇文章主要是对datetime进行汇总,datetime模块的借口实现原则更加直观, ...
- java中的compareto方法以及LIst列表排序的详细介绍【转】
java中的compareto方法的详细介绍 javacompareTo java中的compareto方法,返回参与比较的前后两个字符串的asc码的差值,看下面一组代码 String a=&quo ...
- java中的compareto方法的详细介绍
java中的compareto方法的详细介绍 Java Comparator接口实例讲解(抽象方法.常用静态/默认方法) 一.java中的compareto方法 1.返回参与比较的前后两个字符串的as ...
随机推荐
- node.js守护进程问题的解决
最近自己写了一个node.js来读取redis数据,编写完成后按理来说加& 应该是有效的 nohup node redis.js & 但是每次关闭终端后这个进程就自动停止了,百度了下 ...
- python中可变与不可变类型的全局变量
python中的不可变类型的全局变量如int a=1,str b='hello', 若需要修改必须加global申明, 而全局变量是可变类型的,如list, dict ,则直接修改list.app ...
- LG3092 「USACO2013NOV」No Change 状压DP
问题描述 https://www.luogu.org/problem/P3092 题解 观察到 \(k \le 16\) ,自然想到对 \(k\) 状压. 设 \(opt[i]\) 代表使用硬币状况为 ...
- Codeforces Round #599 (Div. 2) B1. Character Swap (Easy Version) 水题
B1. Character Swap (Easy Version) This problem is different from the hard version. In this version U ...
- Hbase内存磁盘大致关系
转自: https://blog.csdn.net/wuwenxiang91322/article/details/51595771 Hbase内存磁盘关系磁盘数 diskNum磁盘容量 diskCa ...
- 解决邮件发送错误:503 Error: need EHLO and AUTH first
引用文章 https://blog.csdn.net/lingfeian/article/details/96731620 问题描述 2019-07-21 16:14:00.449 ERROR 966 ...
- python 实现自定义切片类
import numbers class Group: #支持切片操作 def __init__(self, group_name, company_name, staffs): self.group ...
- Netty—TCP的粘包和拆包问题
一.前言 虽然TCP协议是可靠性传输协议,但是对于TCP长连接而言,对于消息发送仍然可能会发生粘贴的情形.主要是因为TCP是一种二进制流的传输协议,它会根据TCP缓冲对包进行划分.有可能将一个大数据包 ...
- Disruptor系列(二)— disruptor使用
本文译自Dirsruptor在github上的wiki中文章:Getting Started 获取Disruptor Disruptor jar包可以从maven仓库mvnrepository获取,可 ...
- Linux 网络相关命令 Cheat Sheet
以下漫画形式呈现的常用 Linux 网络相关命令速查表来自 twitter -