iOS 方法交换的原理
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 方法交换的原理相关的知识,希望对你有一定的参考价值。
参考技术A 基础条件使用runtime的特性,进行方法交换
原理:
在ios中,使用函数,可以把函数分为两个部分,即方法名与方法体(selector and method),一个方法名对应其方法体
eg:print()
log ("A")
print即为方法名,大括号里面的即为方法体。
当我们进行方法交换的时候,首先通过runtime的特性获取其方法名。然后根据其方法名获取其方法体,最后实现交换的方法
例如A类中有两个方法printA与printB。
//获取printA和printB的Selector
let printASelector = #selector(A.printA)
let printBSelector = #selector(A.printB)
//获取printA和printB的Method
let printAMethod = class_getInstanceMethod(self, printASelector)
let printBMethod = class_getInstanceMethod(self, printBSelector)
//交换两个方法的Method
method_exchangeImplementations(printAMethod, printBMethod)
这样就是黑魔法发的本质,其实还是跟语言特性有关,Objective-C的消息机制,虽然Swift已经完全不同Objective-C,但是为了兼容,只要该类继承与NSObject,依然保留其消息机制的特性,需要注意的是在Swift中使用的时候,需要在加入@objc,dynamic关键字。
iOS——方法交换 快速掌握
前言:
本文主要介绍ios中方法交换的各种方式具体使用,帮助新手快速入门并且上手。(毕竟我自己学的时候,找资料挺辛苦的,网上的资料大部分都是互相cv 😭)
本文主要包括以下几个部分:
- 概念
- 分类实现方法交换
- 类之间的方法交换(内部交换)
- 类之间的方法交换(外部交换)
- C语言实现的方法交换
- 方法交换实现全局点击拦截
概念
方法交换:
要理解方法交换的话,要了解一些概念:
Objective-C 是一个动态语言,它有runtime运行时系统来动态得创建类和对象。然后oc的类的结构如图所示:
每一个类都有对应的类方法列表,即methodList,methodList中有不同的方法即Method,其中 :SEL 是指向对外的命名
char * 型 的方法类型,
IMP 是方法指针,指向具体的函数实现。
每个方法中包含了方法的sel和IMP
方法交换的核心方法是exchangeImplementations,通过观察源码可以发现
方法交换的本质就是将两个方法的sel和imp原本的对应断开,并将sel和新的IMP生成对应关系
分类:
分类的概念比较简单,主要是给iOS中的系统类或者我们自定义的类增加扩展方法,类似kotlin的扩展函数和dart的扩展方法。
1.分类实现方法交换
首先,通过分类实现方法交换是比较常见的方式,旨在将系统类的方法替换为我们自己定义的方法,这样可以做一些判空,数组越界,异常处理等操作,这里以NSURL为例子,在我们设置URLWithString增加一个判空处理。
NSURL+Extension.h
#import <Foundation/Foundation.h>
@interface NSURL(ExchangeMethod)
@end
NSURL+Extension.m
#import "NSURL+Extension.h"
#import <objc/runtime.h>
@implementation NSURL (ExchangeMethod)
+(void)load{
//获取系统方法结构体
Method system = class_getClassMethod([self class], @selector(URLWithString:));
//获取自己方法结构体
Method own = class_getClassMethod([self class], @selector(SJUrlWithStr:));
// 交换方法 系统的 URLWithString 和自己的 SJUrlWithStr
//交换自己方法和系统方法
method_exchangeImplementations(system, own);
//以后再使用 URLWithString 的时候 其实是在调用SJUrlWithStr
}
+(instancetype)SJUrlWithStr:(NSString *)str{
if (str==nil) {
NSLog(@"字符串为空");
return nil;
}
// 下面的 SJUrlWithStr 其实是使用了 URLWithString
NSURL *url = [NSURL SJUrlWithStr:str];
return url;
}
@end
可以看到首先要获取系统方法结构体,然后获取自定义的方法结构体,最后进行交换。
这里需要注意在自己定义的方法结构体最后要调用一下自己的SJUrlWithStr,看起来是自己调自己,但是因为方法已经被互换了,所以其实是使用了 URLWithString。
最后测试一下功能:
// 1.分类实现方法交换
NSString *str = nil;
NSURL *url = [NSURL URLWithString:str];
NSLog(@"%@", url);
运行:
可以看到被替换的方法成功起作用了,会输出我们加入的语句。
类之间的方法交换
类与类之间的方法交换,有两种实现方式,第一种是内部实现。
我们定义两个类,TestClass1和TestClass2,他们分别有自己的对象方法- (void)test1,- (void)test2和类方法+ (void)sharedClass1和+ (void)sharedClass2,那么我们可以在TestClass1或者TestClass2中的load方法内部进行方法交换操作。
但是load方法是可能存在执行多次的,所以这里可以通过单例设计原则,使方法交换只执行一次,在OC中可以通过dispatch_once实现单例。
基于此,TestClass2的load方法可以如下设计:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//交换不同类 的 对象方法 (与自定义类TestClass1的test1交换)
HookUtilsDifferentClassInstanceMethod(NSClassFromString(@"TestClass1"),
[self class], @selector(test1),
@selector(test2));
//交换不同类 的 对象方法 (与NSArray的containsObject:进行交换)
// HookUtilsDifferentClassInstanceMethod(NSArray.class, [self class],
// @selector(containsObject:), @selector(test3:)); 交换不同类 的 类方法
//友好提示:自定义的方法可以写在任何的自定义类中
HookUtilsDifferentClassClassMethod(NSClassFromString(@"TestClass1"),
[self class], @selector(sharedClass1),
@selector(sharedClass2));
});
}
中间注掉了一段,是和系统类的方法交换尝试,测试也是可行的。
测试验证:
// 2.交换不同的类 load实现
[TestClass1 sharedClass1];
TestClass1 *testClass1 = [[TestClass1 alloc] init];
[testClass1 test1];
结果:
可以看到,我们只调用了TestClass1的类方法和对象方法,但是TestClass2的也同样被调用到了。
看到这里,可以发现,只要拿到两个类的类对象,就可以拿到他们各自的方法并进行方法交换,所以从外部去操作两个类,让他们进行方法交换,是可以抽象成通用的方法的:
/**
@brief 交换不同类中两个 对象方法 友好提示:自定义的方法可以写在任何的自定义类中 )
@param originalCls 被交换的类
@param swizzledCls 用来交换的类
@param originalSelector 被交换的方法
@param swizzledSelector 用来交换的方法
*/
void HookUtilsDifferentClassInstanceMethod(Class originalCls,Class swizzledCls,SEL originalSelector, SEL swizzledSelector) {
if (!originalCls || !swizzledCls) {
NSLog(@"交换方法失败--请保证交换的类名不为空");
return;
}
if (!originalSelector || !swizzledSelector) {
NSLog(@"交换方法失败--请保证交换的方法名不为空");
return;
}
//通过class_getClassMethod 获取两个方法Method
Method originalMethod = class_getInstanceMethod(originalCls, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledCls, swizzledSelector);
//交换之前,先对自定义方法进行添加
BOOL didAddMethod = class_addMethod(originalCls,
swizzledSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
//如果添加成功,则进行交换
if (didAddMethod) {
method_exchangeImplementations(originalMethod, class_getInstanceMethod(originalCls, swizzledSelector));
} else {
NSLog(@"交换方法失败--添加方法失败");
}
}
这里有个很重要的判断 didAddMethod
为什么要做这个判断呢?
因为如果目标类里面的originalCls没有实现(子类没有实现,父类实现了,所以可以调用但是空实现),然后在方法交换时,始终都找不到oriMethod(原方法),然后交换了寂寞,即交换失败。这样当我们最后去调用被交换的方法的时候,目标类内部就找不到oriMethod,就会变成swizzledMethod(待交换方法)自己无限调自己,导致栈溢出。
C语言实现的方法交换
可以通过C语言实现交换,这样做的好处是可以尽量避免被AppStore监测到(我没有试过。。存疑~-~)。因为JSPatch的原因,苹果封杀了所有带有热更新功能的代码,所以方法交换其实是要慎重使用的,避免被判断为热更新而导致app下架。
C语言实现功能类似,就不赘述了,大家看代码吧:
CHookUtils.h
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#include <objc/message.h>
#ifdef __cplusplus
extern "C" {
#endif
bool OBJC_EXCHANGE_METHOD_SAFE(const char* orgClassStr,
const char* orgSelector,
const char* newClassStr,
const char* newSelector);
bool OBJC_EXCHANGE_CLASS_METHOD_SAFE(const char* orgClassStr,
const char* orgSelector,
const char* newClassStr,
const char* newSelector);
#ifdef __cplusplus
}
#endif
CHookUtils.mm
#import "CHookUtils.h"
#import <objc/runtime.h>
bool OBJC_EXCHANGE_METHOD_SAFE(const char* orgClassStr,
const char* orgSelector,
const char* newClassStr,
const char* newSelector)
{
Class orgClass = objc_getClass(orgClassStr);
Class newClass = objc_getClass(newClassStr);
if (!orgClass || !newClass) {
return false;
}
SEL orgMethod = sel_registerName(orgSelector);
SEL newMethod = sel_registerName(newSelector);
//class_getClassMethod
Method newMethodIns = class_getInstanceMethod(newClass, newMethod);
/*在旧的类添加类新方法后,那么只需要交换原有的方法即可*/
Method orgMethodIns = class_getInstanceMethod(orgClass, orgMethod);
/*防止方法不存在*/
if (!orgMethodIns || !newMethodIns) {
return false;
}
/*在已有类添加新方法*/
if (newClass != orgClass) {
IMP newMethodIMP = method_getImplementation(newMethodIns);
const char* newMethodStr = method_getTypeEncoding(newMethodIns);
class_addMethod(orgClass, newMethod, newMethodIMP, newMethodStr);
newMethodIns = class_getInstanceMethod(orgClass, newMethod);
}
method_exchangeImplementations(orgMethodIns, newMethodIns);
return true;
}
bool OBJC_EXCHANGE_CLASS_METHOD_SAFE(const char* orgClassStr,
const char* orgSelector,
const char* newClassStr,
const char* newSelector)
{
Class orgClass = objc_getClass(orgClassStr);
Class newClass = objc_getClass(newClassStr);
if (!orgClass || !newClass) {
return false;
}
SEL orgMethod = sel_registerName(orgSelector);
SEL newMethod = sel_registerName(newSelector);
//class_getClassMethod
Method newMethodIns = class_getClassMethod(newClass, newMethod);
/*在旧的类添加类新方法后,那么只需要交换原有的方法即可*/
Method orgMethodIns = class_getClassMethod(orgClass, orgMethod);
/*防止方法不存在*/
if (!orgMethodIns || !newMethodIns) {
return false;
}
/*在已有类添加新方法*/
if (newClass != orgClass) {
IMP newMethodIMP = method_getImplementation(newMethodIns);
const char* newMethodStr = method_getTypeEncoding(newMethodIns);
class_addMethod(orgClass, newMethod, newMethodIMP, newMethodStr);
newMethodIns = class_getInstanceMethod(orgClass, newMethod);
}
method_exchangeImplementations(orgMethodIns, newMethodIns);
return true;
}
方法交换实现全局点击拦截
最后,博主在学习方法交换的过程中,发现用它是可以来做一些事情的,比如全局的点击统计。通过俺的研究,以及对iOS中事件传递事件的学习(这个以后专门写一篇分享),iOS中的事件主要会收拢在UIResponder和UIControl中,所以分别去hook了这两个类的touchesBegan方法和beginTrackingWithTouch方法,实现了对点击事件的收集,代码就不贴了比较多,感兴趣的同学可以把本文的源码都下载下来跑一跑,不需要积分哟。
以上是关于iOS 方法交换的原理的主要内容,如果未能解决你的问题,请参考以下文章
iOS 方法交换 method_exchangeImplementations