继续创造,加快成长!这是我参与「日新方案 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的映射联系

iOS 小技能:Method Swizzling (交换方法的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逆向