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...

相关推荐
javaDocker6 小时前
业务架构、数据架构、应用架构和技术架构
架构
JosieBook8 小时前
【架构】主流企业架构Zachman、ToGAF、FEA、DoDAF介绍
架构
.生产的驴9 小时前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
丁总学Java9 小时前
ARM 架构(Advanced RISC Machine)精简指令集计算机(Reduced Instruction Set Computer)
arm开发·架构
ZOMI酱11 小时前
【AI系统】GPU 架构与 CUDA 关系
人工智能·架构
乐闻x12 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
青云交14 小时前
大数据新视界 -- 大数据大厂之 Impala 性能优化:跨数据中心环境下的挑战与对策(上)(27 / 30)
大数据·性能优化·impala·案例分析·代码示例·跨数据中心·挑战对策
Winston Wood17 小时前
Perfetto学习大全
android·性能优化·perfetto