APM - iOS 基础功能 Hook - Method Swizzle

简介

AOP

跟面向对象编程(OOP)一样,面向切面编程(AOP)是一种编程范例。这种编程思想旨在通过横切面,提高项目的模块化程度。通过对现有代码进行切入,在切入点单独指定和实现代码,通常是非业务逻辑的核心行为,比如日志记录。这种行为不影响原有业务逻辑,不会使核心代码变得混乱。

AOP 的编程思想特别适合比如日志记录,又比如 APM 功能的开发,实现底层通用抽象能力并且对业务无侵入。

Hook方式

在 iOS 开发中,由于 Objective-C 动态语言特性和 Swift 语言的特性以及动态库的加载特性,通常有以下几种实现 Hook 的方式。

从 NSObject 集成下来的 Swift 类,或者说方法派发中动态派发的 Swift 类或者对象,看成是 Objective-C 对应分支,图中 Swift 分支是指纯 Swift 的情况。

当然,结合编译过程来说,宏或者是代码扫描中按规则完成代码替换,也算是一种变相的实现 AOP 的手段,不过从常用实践和绝对意义上暂不把这些方式并入本文的介绍中。本文主要介绍 Method Swizzle 存在的问题和实践。

Method Swizzle

原理

Method本质是objc_method结构体,method swizzle 本质是修改了 selector 跟 IMP 的映射关系。

ObjectiveC 复制代码
/// An opaque type that represents a method in a class definition.typedef struct objc_method *Method; // 本质是一个结构体
struct objc_method {
    SEL method_name;        // 方法名称
    char *method_types;     // 参数和返回类型的描述字串
    IMP method_imp;         // 方法的具体的实现的指针
}

问题和解决

关于 Method Swizzle 有个三方库叫做 RSSwizzle ,结合该库作者提出的几个 Method Swizzle 中常见的问题,加上平时使用中应注意的问题来一一梳理。

  • 改变不属于本身的代码的行为
  • 难以理解(代码阅读起来感觉是递归的)
  • 难以调试(函数堆栈看起来有些跳跃)

重复执行

重复执行会导致 Swizzle 过来的方法又还原回去,好在有函数可以保证只执行一次

ObjectiveC 复制代码
 static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self hook];
    });

线程安全

在进行动态函数修改的时候,有可能其他线程也在做同样的操作

Method Swizzle 的执行的确不是线程安全的,这表明在多线程并发的情况下会导致 Crash,有几种保障线程安全的方式。

Load 方法

+ (void)load 中执行,虽然可能会对启动时长有影响,好在影响很小

ObjectiveC 复制代码
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self hook];
    });
}

主线程

Hook UI 相关类的方法的时候,也可以程序执行时,即在 main 函数之后,在主线程做 Hook

ObjectiveC 复制代码
func startHook() {
    UIViewController.startHook()
    UINavigationController.startHook()
}

子类未实现被替换的方法

子类尝试 Hook 子类中的方法,结果子类没有实现被 Hook 的方法,但是父类的方法中实现了这个方法。导致子类中 swizzle 了父类的原方法,而不是子类的原方法。

当父类的原方法被另外调用的时候,会出现两种问题

  • 找不到方法
  • 陷入死循环

父类未实现hook后的方法

被Hook方法 调用的新方法
ParentViewController sayHello -
ChildViewController - swizzled_sayHello

父类被Hook的方法不会被另外调用的话,不会出问题。

ObjectiveC 复制代码
// 调用:
    ChildViewController *vc = [[ChildViewController alloc] init];
    [vc sayHello];

// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]

当父类调用自身被Hook的方法时,会因为找不到对应的swizzled方法而报错。

ObjectiveC 复制代码
// 调用:
    ChildViewController *vc = [[ChildViewController alloc] init];
    [vc sayHello];
    
    ParentViewController *pvc = [[ParentViewController alloc] init];
    [pvc sayHello];
    
// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]

-[ChildViewController swizzled_sayHello]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ParentViewController swizzled_sayHello]: unrecognized selector sent to instance 0x1057071c0'

父类实现了hook后的方法

被Hook方法 调用的新方法
ParentViewController sayHello swizzled_sayHello
ChildViewController - swizzled_sayHello

父类被Hook的方法不会被另外调用的话,不会出问题。

ObjectiveC 复制代码
// 调用:
    ChildViewController *vc = [[ChildViewController alloc] init];
    [vc sayHello];

// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]

当父类调用自身被Hook的方法时,会陷入死循环。

ObjectiveC 复制代码
// 调用:
    ChildViewController *vc = [[ChildViewController alloc] init];
    [vc sayHello];
    
    ParentViewController *pvc = [[ParentViewController alloc] init];
    [pvc sayHello];
    
// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]
-[ChildViewController swizzled_sayHello]
// 开始死循环
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
//...

父类也未实现被hook的方法

这样会导致 Hook 的 method swizzle 方法中的 originalMethod 为 nil,输出如下。

ObjectiveC 复制代码
// 打印:
__func__ -[ChildViewController swizzled_sayHello] _cmd sayHello
// 开始死循环
__func__ -[ChildViewController swizzled_sayHello] _cmd swizzled_sayHello
__func__ -[ChildViewController swizzled_sayHello] _cmd swizzled_sayHello
__func__ -[ChildViewController swizzled_sayHello] _cmd swizzled_sayHello
__func__ -[ChildViewController swizzled_sayHello] _cmd swizzled_sayHello
//...

解决办法

确保被 Hook 的类中,存在被 Hook 的方法

判断被 Hook 的方法存在,即 originalMethod 不为 nil。当父类中存在被 Hook 的方法时,originalMethod 就是存在的,并不能直接确定子类是否存在被 Hook 的方法。

ObjectiveC 复制代码
    if (!originalMethod) {
        class_addMethod([self class], originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        method_setImplementation(swizzledMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }

向子类中添加被 Hook 的方法,如果已经存在,添加失败则直接 exchangeImplementation 。如果添加成功,使用 class_replaceMethod 交换。这种方式就不需要判断父类中是否还存在对应的方法,即不需要使用上面的写法。

ObjectiveC 复制代码
    BOOL didAddMethod = class_addMethod([self class], originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        class_replaceMethod([self class], swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }

函数 _cmd 变化

OC 的方法中,默认会传递 self 和 _cmd 两个参数,一个是接收消息的对象实例,一个是 selector。

ObjectiveC 复制代码
// 转换成
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("sayHello"));
// 相当于
objc_msgSend(self, @selector("sayHello")) ; 

经过 method swizzle 之后,_cmd 参数发生变化。如果有方法继续使用原来的 _cmd 参数,可能会导致问题。

ObjectiveC 复制代码
__func__ -[ChildViewController swizzled_sayHello] _cmd sayHello
__func__ -[ChildViewController sayHello] _cmd swizzled_sayHello

命名冲突

命名冲突不算是 Mehod Swizzle 独有的问题,本身 OC 在没有命名空间的情况下,所有的方法都存在命名冲突的问题。

通常我们会加类似 swizzled_ 这样的前缀来区分和避免命名冲突

ObjectiveC 复制代码
@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end@implementation NSView (MyViewAdditions)

- (void)my_setFrame:(NSRect)frame {
    // do custom work
    [self my_setFrame:frame];
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}

@end

也可以使用静态函数和函数指针的方法来彻底避免

ObjectiveC 复制代码
@implementation NSView (MyViewAdditions)static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);

static void MySetFrame(id self, SEL _cmd, NSRect frame) {
    // do custom work
    SetFrameIMP(self, _cmd, frame);
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}

@end

把 swizzling method 提取出来定义如下

ObjectiveC 复制代码
typedef IMP *IMPPointer;

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end

未调回原方法

Method Swizzle 中需要调回原方法,但是也经常出现忘记调回原方法的情况。就像在OC中的一些生命周期忘记调用Super的生命周期方法一样,也是需要注意的问题,否则其他依赖于原方法的逻辑可能会出现bug。

Swizzle 顺序

方法交换的顺序有很大的影响。假设 setFrame:方法只定义在 UIView 中,方法交换的顺序如下。

ObjectiveC 复制代码
[UIButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[UIControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[UIView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

当先交换 UIButton 中的方法的时候,由于 UIButton 本身未实现 setFrame:方法,需要添加一个新的 setFrame:方法在 UIButton 类中。新的setFrame:方法直接从 UIView 中对应的方法拷贝过来。交换 UIControl 方法的时候也是如此。

这时候在调用一个 button 中的setFrame:方法时,首先调用 swizzle 后的方法 my_buttonSetFrame: ,然后就会直接调用自身实现的,原本在 UIView 中的 setFrame:方法。这样 UIControl 和 UIView 中 swizzle 后的方法 my_controlSetFrame:my_viewSetFrame:就都不会调用到。

如果方法交换的顺序如下。

ObjectiveC 复制代码
[UIView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[UIControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[UIButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

由于 UIView 在交换的最前面,所以可以正确的触发my_viewSetFrame:。类似的由于 UIControl 在UIButton 前面,所以可以正确的触发my_controlSetFrame:方法。

如何才能保证这样的顺序呢,使用+(void)load来调用方法交换,因为 load 方法保证了父类的方法先被调用。

实践

iOS method swizzle的实践,也需要看具体的使用场景,总体来说避免以上的问题即可。

通常我们 hook UIViewController的生命周期,UINavigationController的一些方法,这些方法是明确已实现,也不存在调用顺序的问题。主要需要注意调回原方法,避免耗时操作。

  • 通常会对Swizzle进行封装
ObjectiveC 复制代码
#import "NSObject+Swizzle.h"
#import <objc/runtime.h>

@implementation NSObject (Swizzle)

+ (BOOL)swizzle:(Class)originalClass Method:(SEL)originalSelector withMethod:(SEL)swizzledSelector
{
    if (!(originalClass && originalSelector && swizzledSelector)) {
        return NO;
    }
    
    Class class = [self class];
    
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethod = class_addMethod(originalClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    
    return YES;
}

@end
  • 针对被 Hook 方法的不同,在 load 方法或者主线程中调用。

  • 也可以更加严谨的使用 RSSwizzle 中的实现,避免命名冲突和 _cmd 改变的问题

总结

method swizzle 作为 hook 的一种方式和AOP编程思想的实现,有利有弊,既然没有银弹,我们还是需要知晓原理尽量避开一些常见的坑。

引用

stackoverflow.com/questions/5...

相关推荐
洛卡卡了2 小时前
从单层到 MVC,再到 DDD:架构演进的思考与实践
架构·mvc
乌恩大侠2 小时前
O-RAN Fronthual CU/Sync/Mgmt 平面和协议栈
5g·平面·fpga开发·架构
木宛哥9 小时前
代码背后的智慧:20条编程感悟
java·后端·架构
无尽的大道12 小时前
Java反射原理及其性能优化
jvm·性能优化
58沈剑16 小时前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构
萌面小侠Plus17 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
想进大厂的小王19 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
阿伟*rui20 小时前
认识微服务,微服务的拆分,服务治理(nacos注册中心,远程调用)
微服务·架构·firefox
ZHOU西口20 小时前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
人工智能培训咨询叶梓21 小时前
探索开放资源上指令微调语言模型的现状
人工智能·语言模型·自然语言处理·性能优化·调优·大模型微调·指令微调