简介

APM - iOS 基础功能 Hook - Method Swizzle

AOP

跟面向目标编程(OOP)相同,面向切面编程(AOP)是一种编程典范。这种编程思维旨在经过横切面,进步项目的模块化程度。经过对现有代码进行切入,在切入点独自指定和完结代码,一般是非事务逻辑的中心行为,比如日志记录。这种行为不影响原有事务逻辑,不会使中心代码变得混乱。

AOP 的编程思维特别适合比如日志记录,又比如 APM 功用的开发,完结底层通用笼统才能而且对事务无侵入。

Hook办法

iOS 开发中,由于 Objective-C 动态言语特性和 Swift 言语的特性以及动态库的加载特性,一般有以下几种完结 Hook 的办法。

APM - iOS 基础功能 Hook - Method Swizzle

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

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

Method Swizzle

原理

APM - iOS 基础功能 Hook - Method Swizzle

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

/// 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 过来的办法又还原回去,好在有函数能够确保只履行一次

 static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self hook];
    });

线程安全

在进行动态函数修改的时分,有或许其他线程也在做相同的操作

Method Swizzle 的履行确实不是线程安全的,这表明在多线程并发的情况下会导致 Crash,有几种确保线程安全的办法。

Load 办法

+ (void)load 中履行,虽然或许会对发动时长有影响,好在影响很小

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self hook];
    });
}

主线程

Hook UI 相关类的办法的时分,也能够程序履行时,即在 main 函数之后,在主线程做 Hook

func startHook() {
    UIViewController.startHook()
    UINavigationController.startHook()
}

子类未完结被替换的办法

子类测验 Hook 子类中的办法,成果子类没有完结被 Hook 的办法,可是父类的办法中完结了这个办法。导致子类中 swizzle 了父类的原办法,而不是子类的原办法。

当父类的原办法被别的调用的时分,会出现两种问题

  • 找不到办法
  • 堕入死循环

父类未完结hook后的办法

被Hook办法 调用的新办法
ParentViewController sayHello
ChildViewController swizzled_sayHello

父类被Hook的办法不会被别的调用的话,不会出问题。

// 调用:
    ChildViewController *vc = [[ChildViewController alloc] init];
    [vc sayHello];
// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]

当父类调用本身被Hook的办法时,会由于找不到对应的swizzled办法而报错。

// 调用:
    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的办法不会被别的调用的话,不会出问题。

// 调用:
    ChildViewController *vc = [[ChildViewController alloc] init];
    [vc sayHello];
// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]

当父类调用本身被Hook的办法时,会堕入死循环。

// 调用:
    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,输出如下。

// 打印:
__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 的办法。

    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 交流。这种办法就不需求判别父类中是否还存在对应的办法,即不需求运用上面的写法。

    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。

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

经过 method swizzle 之后,_cmd 参数发生变化。假如有办法持续运用原来的 _cmd 参数,或许会导致问题。

__func__ -[ChildViewController swizzled_sayHello] _cmd sayHello
__func__ -[ChildViewController sayHello] _cmd swizzled_sayHello

命名抵触

命名抵触不算是 Mehod Swizzle 独有的问题,本身 OC 在没有命名空间的情况下,一切的办法都存在命名抵触的问题。

一般咱们会加相似 swizzled_ 这样的前缀来区分和防止命名抵触

@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

也能够运用静态函数和函数指针的办法来彻底防止

@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 提取出来界说如下

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 中,办法交流的次序如下。

[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:就都不会调用到。

假如办法交流的次序如下。

[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进行封装
#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…