iOS 小技能:Method Swizzling (交换方法的IMP)

1,816 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第31天,点击查看活动详情

前言

利用Objective-C Runtimee的动态绑定特性,将一个方法的实现与另一个方法的实现进行交换。交换两个方法的实现一般写在分类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次。

应用场景:数据采集、生命周期、响应事件埋点。

  • 注意事项

load 的加载比main 还要早,所以如果我们再load方法里面做了耗时的操作,那么一定会影响程序的启动时间,所以在load里面一定不要写耗时的代码

不要在load里面取加载对象,因为我们再load调用的时候根本就不确定我们的对象是否已经初始化了,所以不要去做对象的初始化

I Method Swizzling基础

在Objective-C的Runtime中,一个类是用一个名为objc_class的结构体表示的,它的定义如下

developer.apple.com/documentati…

objc_class {    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;#if !__OBJC2__    Class _Nullable super_class                              OBJC2_UNAVAILABLE;    const char * _Nonnull name                               OBJC2_UNAVAILABLE;    long version                                             OBJC2_UNAVAILABLE;    long info                                                OBJC2_UNAVAILABLE;    long instance_size                                       OBJC2_UNAVAILABLE;    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;    struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;#endif} OBJC2_UNAVAILABLE;

从上述结构体中可以发现一个objc_method_list指针,它保存着当前类的所有方法列表。 同时,objc_method_list也是一个结构体,它的定义如下:

struct objc_method_list {    struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;    int method_count                                         OBJC2_UNAVAILABLE;#ifdef __LP64__    int space                                                OBJC2_UNAVAILABLE;#endif    /* variable length structure */    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;}

从上面的结构体中发现一个objc_method字段,它的定义如下:

struct objc_method {    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;    char * _Nullable method_types                            OBJC2_UNAVAILABLE;    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;}

从上面的结构体中还发现,一个方法由如下三部分组成:

method_name:方法名。
method_types:方法类型。
method_imp:方法实现。

使用Method Swizzling交换方法,其实就是修改了objc_method结构体中的mthod_imp,即改变了method_name和method_imp的映射关系

在这里插入图片描述

1.1 字符串驻留

字符串驻留的优化技术: 把一个不可变字符串对象的值拷贝给各个不同的指针。

Objective-C 选择器的名字也是作为驻留字符串储存在一个共享的字符串池当中的。

NSString *stra = @"Hello";
NSString *strb = @"Hello";
BOOL wt = (stra == strb); // YES

Selector(typedef struct objc_selector *SEL):在运行时 Selectors 用来代表一个方法的名字。Selector 是一个在运行时被注册(或映射)的C类型字符串,由编译器产生并且在类被加载进内存时由runtime自动进行名字和实现的映射。

选择器的比较

OBJC_EXPORT BOOL
class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

- (BOOL)respondsToSelector:(SEL)aSelector;

if([device respondsToSelector:@selector(setSmoothAutoFocusEnabled:)]){

}

1.2 Objective-C的hook方案

实现原理:在运行时改变 selector 在消息分发列表中的映射,通过交换 selector 来改变函数指针的引用(名字和实现的映射)。

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。因此我们可以在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。

  1. 利用 method_exchangeImplementations 来交换2个方法中的IMP,
  2. 利用 class_replaceMethod 来修改类,
  3. 利用 method_setImplementation 来直接设置某个方法的IMP,

在运行时,类(Class)维护了一个消息分发列表来保证消息的正确发送,每一个消息列表的入口是一个方法(Method),这个方法映射了一对键值对,其中键是这个方法的名字 selector(SEL),值是指向这个方法实现的函数指针 implementation(IMP)。

Method swizzling 修改了类的消息分发列表使得已经存在的 selector 映射到另一个实现 implementation,同时重命名了原生方法的实现对应一个新的 selector。

#import "NSObject+Swizzle.h"
#include "objc/runtime.h"

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store);
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 (Swizzle)

+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(out IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}

@end

每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

1.3 Method swizzling的时机

如果使用恰当,Method swizzling 还是很安全的,一个简单安全的方法是,仅在load中swizzle ,在 dispatch_once 中完成

+ (void)load {
    Method originAddObserverMethod = class_getInstanceMethod(self, @selector(presentViewController:animated:completion:));
    Method swizzledAddObserverMethod = class_getInstanceMethod(self, @selector(K_presentViewController:animated:completion:));
    method_exchangeImplementations(originAddObserverMethod, swizzledAddObserverMethod);
    
    
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            NSArray *selStringsArray = @[@"shouldAutorotate",@"supportedInterfaceOrientations",@"preferredInterfaceOrientationForPresentation"];
            
            
            [selStringsArray enumerateObjectsUsingBlock:^(NSString *selString, NSUInteger idx, BOOL *stop) {
                NSString *mySelString = [@"sd_" stringByAppendingString:selString];
                
                Method originalMethod = class_getInstanceMethod(self, NSSelectorFromString(selString));
                Method myMethod = class_getInstanceMethod(self, NSSelectorFromString(mySelString));
                method_exchangeImplementations(originalMethod, myMethod);
            }];
        });
    

}
————————————————
版权声明:本文为CSDN博主「iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/z929118967/article/details/78019668

因为load 方法调用在main之前,并且不需要我们初始化,+load`方法是在类或者类别被加载到Objective-C时执行。如果在+load类方法中实现MethodSwizzling,替换的方法会在应用程序运行的整个生命周期中生效,这也是我们期望的结果。

ps :initialize 会在类第一次接收到消息的时候调用

有继承关系的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。因此在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。

1.4 initialize

+initialize本质为objc/_msgSend,如果子类没有实现initialize则会去父类查找,如果分类中实现,那么会覆盖主类,和runtime消息转发逻辑一样

  • 1.initialize 会在类第一次接收到消息的时候调用
  • 2.先调用父类的 initialize,然后调用子类。
  • 3.initialize 是通过 objc_msgSend 调用的
  • 4.如果子类没有实现 initialize,会调用父类的initialize(父类可能被调用多次)
  • 5.如果分类实现了initialize,会覆盖本类的initialize方法

1.5 对象关联(动态添加实例变量)

Since SELs are guaranteed to be unique and constant, you can use _cmd as the key for objc_setAssociatedObject().

blog.csdn.net/z929118967/…

1.6 其他相关方法

  • class_getInstanceMethod: 返回目标类aClass、方法名为aSelector的实例方法
/** 
 * Returns a specified instance method for a given class.
 * 
 * @param cls The class you want to inspect.
 * @param name The selector of the method you want to retrieve.
 * 
 * @return The method that corresponds to the implementation of the selector specified by 
 *  \e name for the class specified by \e cls, or \c NULL if the specified class or its 
 *  superclasses do not contain an instance method with the specified selector.
 *
 * @note This function searches superclasses for implementations, whereas \c class_copyMethodList does not.
 */
OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);


    // 通过方法名获取方法指针
    Method method1 = class_getInstanceMethod(cls, selector1);

  • class_addMethod: 给目标类aClass添加一个新的方法,同时包括方法的实现
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

    // 往类中添加 originalSEL 方法,如果已经存在会添加失败,并返回 NO
    if (class_addMethod(self, originalSEL, originalIMP, originalMethodType)) {
        // 如果添加成功了,重新获取 originalSEL 实例方法
        originalMethod = class_getInstanceMethod(self, originalSEL);
    }

II 例子

2.1 使用method_exchangeImplementations 方法实现交换的简单例子

#import "NSArray+Swizzle.h"  
  
@implementation NSArray (Swizzle)  
  
  
- (id)myLastObject  
{  
    id ret = [self myLastObject];  //method_exchangeImplementations 之后,执行到这里将是调用LastObject 方法
    NSLog(@"**********  myLastObject *********** ");  
    return ret;  
}  
@end  
  Method ori_Method =  class_getInstanceMethod([NSArray class], @selector(lastObject));  
        Method my_Method = class_getInstanceMethod([NSArray class], @selector(myLastObject));  
        method_exchangeImplementations(ori_Method, my_Method);  
          



  • 验证
        NSArray *array = @[@"0",@"1",@"2",@"3"];  
        NSString *string = [array lastObject];  
        NSLog(@"TEST RESULT : %@",string);  

2.2 让所有继承自NSObject的子类,都具有Method Swizzling的能力

Method Swizzling 的封装 --------------------

/**
 让所有继承自NSObject的子类,都具有Method Swizzling的能力。
 */
@interface NSObject (SASwizzler)

/**
交换方法名为 originalSEL 和方法名为 alternateSEL 两个方法的实现
@param originalSEL 原始方法名
@param alternateSEL 要交换的方法名称
*/
+ (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL;


/**
 方式二
 */
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(out IMPPointer)store;


2.3 实现页面浏览事件全埋

实现页面浏览事件全埋

利用Method Swizzling来交换UIViewController的-viewDidAppear:方法,然后在交换的方法中触发$AppViewScreen事件,以实现页面浏览事件的全埋点。

2.4 适配iOS13的模态的的样式问题

  • h
/**
 模态只处理13以上的
 */
@interface UIViewController (ERPPresent13)

/**
Whether or not to set ModelPresentationStyle automatically for instance, Default is [Class K_automaticallySetModalPresentationStyle].
@return BOOL
*/
@property (nonatomic, assign) BOOL K_automaticallySetModalPresentationStyle;

/**
 Whether or not to set ModelPresentationStyle automatically, Default is YES, but UIImagePickerController/UIAlertController is NO.
 @return BOOL
 */
+ (BOOL)K_automaticallySetModalPresentationStyle;


  • m
@implementation UIViewController (ERPPresent13)
+ (void)load {
    Method originAddObserverMethod = class_getInstanceMethod(self, @selector(presentViewController:animated:completion:));
    Method swizzledAddObserverMethod = class_getInstanceMethod(self, @selector(K_presentViewController:animated:completion:));
    method_exchangeImplementations(originAddObserverMethod, swizzledAddObserverMethod);
    
    
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            NSArray *selStringsArray = @[@"shouldAutorotate",@"supportedInterfaceOrientations",@"preferredInterfaceOrientationForPresentation"];
            
            
            [selStringsArray enumerateObjectsUsingBlock:^(NSString *selString, NSUInteger idx, BOOL *stop) {
                NSString *mySelString = [@"sd_" stringByAppendingString:selString];
                
                Method originalMethod = class_getInstanceMethod(self, NSSelectorFromString(selString));
                Method myMethod = class_getInstanceMethod(self, NSSelectorFromString(mySelString));
                method_exchangeImplementations(originalMethod, myMethod);
            }];
        });
    

}

- (void)setK_automaticallySetModalPresentationStyle:(BOOL)K_automaticallySetModalPresentationStyle {
    objc_setAssociatedObject(self, K_automaticallySetModalPresentationStyleKey, @(K_automaticallySetModalPresentationStyle), OBJC_ASSOCIATION_ASSIGN);
}

- (BOOL)K_automaticallySetModalPresentationStyle {
    id obj = objc_getAssociatedObject(self, K_automaticallySetModalPresentationStyleKey);
    if (obj) {
        return [obj boolValue];
    }
    return [self.class K_automaticallySetModalPresentationStyle];
}

+ (BOOL)K_automaticallySetModalPresentationStyle {
    if ([self isKindOfClass:[UIImagePickerController class]] || [self isKindOfClass:[UIAlertController class]]) {
        return NO;
    }
    return YES;
}



- (void)K_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
    if (@available(iOS 13.0, *)) {
        if (viewControllerToPresent.K_automaticallySetModalPresentationStyle) {
            
            
            viewControllerToPresent.modalPresentationStyle = [QCTSession getModalPresentationStyleWith:viewControllerToPresent];
            
            
            
//            viewControllerToPresent.modalPresentationStyle = UIModalPresentationOverFullScreen;// 1、后果:被怼了很惨,因为项目里有很多模态出来的VC是半透明,结果现在变成完全不透明,背景为黑色。2、 修复拜访记录日期控件PGDatePicker蒙版的问题:点击时间输入框,弹窗蒙版变成了空白

//                        viewControllerToPresent.modalPresentationStyle = UIModalPresentationAutomatic;// 1、后果:被怼了很惨,因为项目里有很多模态出来的VC是半透明,结果现在变成完全不透明,背景为黑色。2、 修复拜访记录日期控件PGDatePicker蒙版的问题:点击时间输入框,弹窗蒙版变成了空白

            //            2、后遗症:因为把显示效果修改为:UIModalPresentationOverFullScreen;半透明,全屏覆盖的问题都得到完美解决。但是会引发一个新的问题:前一个页面的viewWillAppear:、viewDidAppear:也无法触发。例如: A   Present  B, 有时因为业务逻辑需要,必须在viewWillAppear, viewDidAppear里写一些代码,当B 调用dismiss方法的时候, A的这个两个方法不会触发,因此会有一些安全隐患。因此如果要求B 调用dismiss方法,A要执行viewWillAppear:、viewDidAppear:这个两个方法,这个时候要把B的modalPresentationStyle设置为:UIModalPresentationFullScreen;

                        
            //      3、      其他:如果要求 B既要半透明,dismiss时,A还要调用viewWillAppear:、viewDidAppear:。我的想法是在B 写一个block,在B调用dismiss之前,利用block回调A相关的业务逻辑代码。如果有其他更好的方法请告诉我。万分感谢!!
                        

            
        }
        [self K_presentViewController:viewControllerToPresent animated:flag completion:completion];
    } else {
        // Fallback on earlier versions
        [self K_presentViewController:viewControllerToPresent animated:flag completion:completion];
    }
}

#pragma mark - ******** 2、【如果用户有打开手机的自动旋转功能 除了签名界面的页面,其余的都是竖屏】:

- (BOOL)sd_shouldAutorotate{
    return YES;
}


- (UIInterfaceOrientationMask)sd_supportedInterfaceOrientations {
    
    return UIInterfaceOrientationMaskPortrait;
}
-(UIInterfaceOrientation)sd_preferredInterfaceOrientationForPresentation{
    
    return UIInterfaceOrientationPortrait;
}
- (BOOL)shouldAutorotate {
    //CRMSignatureViewController4EditMerchantInfo
    //判断类型,签名的上一个界面需要自动旋转回来

    return YES;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}
-(UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
    return UIInterfaceOrientationPortrait;
}
//

III see also

#小程序:iOS逆向