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——面向AOP编程(方法交换)

iOS——面向AOP编程(方法交换)

iOS 方法交换 method_exchangeImplementations

ios开发runtime学习二:runtime交换方法

iOS 为何使用runtime多次方法交换后却能按照交换顺序依次执行代码逻辑?

iOS 运行时使用(交换两个方法)