Swizzle应用性研究

作者: 那年那月那猪在简书 | 来源:发表于2018-01-17 18:37 被阅读215次

    Swizzle的常见错误及基本原理

    示例1

    
    @implementation UIImageView(TestContentMode_Origin)
    
    + (void)load {
    
        Method originMethod = class_getInstanceMethod([UIImageView class], @selector(setContentMode:));
    
        Method swizzledMethod = class_getInstanceMethod([UIImageView class], @selector(nty_setContentMode:));
    
        method_exchangeImplementations(originMethod, swizzledMethod);
    
    }
    
    - (void)nty_setContentMode:(UIViewContentMode)contentMode {
    
        NSLog(@"swizzle contentmode %@", self);
    
        [self nty_setContentMode:contentMode];
    
    }
    
    @end
    
    

    效果:程序崩溃

    崩溃原因分析

    method_exchangeImplementations是将两个SEL指向的IMP互相替换。

    originMethod想指向UIImageView的方法setContentMode,然而该方法是UIImageView的父类UIView实现的,所以UIImageView分类中的方法实际上是与UIView的setContentMode做了替换。在UIView的实例调用setContentMode时,会调用nty_setContentMode的SEL,UIView中没有实现此方法,导致崩溃.

    见图1,2

    图1 图2

    引申:Method, SEL, IMP

    
    // Method 在头文件 objc_class.h中定义如下:
    
    typedef struct objc_method *Method;
    
    typedef struct objc_method {
    
        SEL method_name;
    
        char *method_types;
    
        IMP method_imp;
    
    };
    
    // SEL的定义为:
    
    typedef struct objc_selector  *SEL; 
    
    // IMP 的含义:
    
    typedef id (*IMP)(id, SEL, ...);
    
    

    SEL的定义为:是一个指向 objc_selector 指针,表示方法的名字/签名。

    IMP 的含义:是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。

    引申:class

    
    struct objc_class {
    
        struct objc_class super_class;  /*父类*/
    
        const char *name;                /*类名字*/
    
        long version;                  /*版本信息*/
    
        long info;                        /*类信息*/
    
        long instance_size;              /*实例大小*/
    
        struct objc_ivar_list *ivars;    /*实例参数链表*/
    
        struct objc_method_list **methodLists;  /*方法链表*/
    
        struct objc_cache *cache;              /*方法缓存*/
    
        struct objc_protocol_list *protocols;  /*协议链表*/
    
    };
    
    

    methodLists方法链表里面存储的是Method 类型。selector 就是指 Method的 SEL, address就是指Method的 IMP。

    示例1优化

    示例1证明,直接使用method_exchangeImplementations进行swizzle,有可能出现崩溃问题。使用第三方库JRSwizzle的方法jr_swizzleMethod:withMethod:error:对该问题进行了优化。

    
    + (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_ {
    
    #if OBJC_API_VERSION >= 2
    
    Method origMethod = class_getInstanceMethod(self, origSel_);
    
    if (!origMethod) {
    
    ...(容错处理,节约篇幅,省略)
    
    return NO;
    
    }
    
    Method altMethod = class_getInstanceMethod(self, altSel_);
    
    if (!altMethod) {
    
    ...(容错处理,节约篇幅,省略)
    
    return NO;
    
    }
    
    class_addMethod(self,
    
    origSel_,
    
    class_getMethodImplementation(self, origSel_),
    
    method_getTypeEncoding(origMethod));
    
    class_addMethod(self,
    
    altSel_,
    
    class_getMethodImplementation(self, altSel_),
    
    method_getTypeEncoding(altMethod));
    
    method_exchangeImplementations(class_getInstanceMethod(self, origSel_), class_getInstanceMethod(self, altSel_));
    
    return YES;
    
    #else
    
    ...(低版本API的配置方式,节约篇幅,省略)
    
    #endif
    
    }
    
    

    该方法通过class_addMethod保证在父类实现原生方法或被swizzle方法而子类没有实现的情况下,重新生成一个新的Method,SEL不变,IMP指向父类方法的IMP,保存在子类的method_list中(即将子类中实现同样的方法)。

    class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现

    示例2

    通过jr_swizzleMethod:withMethod:error:进行setContentMode的swizzle

    
    @implementation UIImageView (TestContentMode_JR)
    
    + (void)load {   
    
        [[UIImageView class] jr_swizzleMethod:@selector(setContentMode:) withMethod:@selector(nty_setContentMode:) error:nil];
    
    }
    
    - (void)nty_setContentMode:(UIViewContentMode)contentMode {
    
        NSLog(@"swizzle contentmode(JR) %@", self);
    
        [self nty_setContentMode:contentMode];
    
    }
    
    @end
    
    

    该方法中,在class_addMethod时,见图3.

    图3

    在method_exchangeImplementations后,见图4.

    图4

    当前,可以完美解决方问题

    示例3

    针对示例1, 如果不使用jr_swizzleMethod:withMethod:error:的方式,仍有办法解决此问题。

    示例1之所以崩溃是因为在UIView执行setContentMode时,会调用UIView不存在的方法nty_setContentMode。那么,将swizzle的方法从UIImageView的分类中改为写在UIView的分类中,即可解决此问题。

    
    @implementation UIView(TestContentMode_Origin)
    
    + (void)load {
    
        Method originMethod = class_getInstanceMethod([UIView class], @selector(setContentMode:));
    
        Method swizzledMethod = class_getInstanceMethod([UIView class], @selector(nty_setContentMode:));
    
        method_exchangeImplementations(originMethod, swizzledMethod);
    
    }
    
    - (void)nty_setContentMode:(UIViewContentMode)contentMode {
    
        if ([self isKindOfClass:[UIImageView class]]) {
    
          NSLog(@"swizzle contentmode %@", self);
    
        }
    
        [self nty_setContentMode:contentMode];
    
    }
    
    @end
    
    

    示例4

    若由于需求原因,既有针对UIView的setContentMode的swizzle方法,也有针对UIImageView的swizzle方法(即示例2与示例3共存)。将会发生逻辑错误。

    两个swizzle都是写在分类的+load方法中,两方法的调用顺序与build phase中的文件编绎顺序有关。此处,我们假设UIView (TestContentMode_Origin)的+load先被调用

    见图5

    图5

    UIImageView(TestContentMode_Origin)的+load再被调用

    见图6、7

    图6 图7

    那么此时,若UIView调用setContentMode不会有问题,UIImageView调用时会出现无限调用循环的问题

    拓展:RSSwizzle提供了另外一种更加健壮的Swizzle方式,如以下代码所示。但此代码在我们项目中没有普及,我也没有确认此方法是否会出现其他问题,此处列出仅供参考。

    
    RSSwizzleInstanceMethod([UIView class],
    
                                @selector(setContentMode:),
    
                                RSSWReturnType(void),
    
                                RSSWArguments(UIViewContentMode contentMode),
    
                                RSSWReplacement({
    
        // Returning modified return value.
    
        NSLog(@"swizzle contentmode %@", @(contentMode));
    
        // 先执行原始方法
    
        RSSWCallOriginal();
    
                                }), 0, NULL);
    
    

    示例5

    针对示例4的需求,建议将UIImageView的swizzle方法写到UIView的分类中。即示例3的代码。那么代码会变成以下的样式。

    
    @implementation UIView(ForUIViewSwizzle)
    
    + (void)load {
    
        Method originMethod = class_getInstanceMethod([UIView class], @selector(setContentMode:));
    
        Method swizzledMethod = class_getInstanceMethod([UIView class], @selector(nty_setContentMode:));
    
        method_exchangeImplementations(originMethod, swizzledMethod);
    
    }
    
    - (void)nty_setContentMode:(UIViewContentMode)contentMode {
    
        // 执行针对UIImageView的swizzle的逻辑
    
        [self nty_setContentMode:contentMode];
    
    }
    
    @end
    
    @implementation UIView(ForUIImageViewSwizzle)
    
    + (void)load {
    
        Method originMethod = class_getInstanceMethod([UIView class], @selector(setContentMode:));
    
        Method swizzledMethod = class_getInstanceMethod([UIView class], @selector(nty_setContentMode:));
    
        method_exchangeImplementations(originMethod, swizzledMethod);
    
    }
    
    - (void)nty_setContentMode:(UIViewContentMode)contentMode {
    
        if ([self isKindOfClass:[UIImageView class]]) {
    
          // 执行针对UIImageView的swizzle的逻辑
    
        }
    
        [self nty_setContentMode:contentMode];
    
    }
    
    @end
    
    

    见图8

    图8

    由于两个分类的swizzle名字相同,通过class_getInstanceMethod获得nty_setContentMode的Method将一直是同一个(该问题出现原因需要详细了解class、category实现机制,此处不多做缀述),所以相当于两个Method互相swizzle了两次,最终SEL与IMP的连接仍为图8的结果。

    示例6

    将示例5的代码做一点点调整,将UIView(ForUIImageViewSwizzle)中替换nty_setContentMode方法名改为nty2_setContentMode

    见图9、10、11

    图9 图10 图11

    最终成功完成需求

    Swizzle在项目中应用出现的问题

    iOS项目在很多方法中如果传参不对,会直接导致crash。比如NSString的substringToIndex:方法在数组越界时、NSDictionary传入nil值时、NSArray数组越界时。这些情况,我们可能用swizzle将这些系统方法进行swizzle,加入数据空值、数组越界情况的容错处理,有效减少崩溃率。

    此处,以NSString的substringToIndex:方法为例。

    示例1

    
    @implementation NSString (AvoidCrash)
    
    + (void)load {
    
        static dispatch_once_t onceToken;
    
        dispatch_once(&onceToken, ^{
    
            [[NSString class] jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];
    
        };
    
    }
    
    - (NSString*)nty_substringToIndex:(NSUInteger)to {
    
        if (to <= self.length) {
    
            return [self nty_substringToIndex:to];
    
        }
    
        return self;
    
    }
    
    @end
    
    

    在Demo中写下测试代码测试此功能

    
    - (void)testCrash {
    
        NSString *testStr = @"asdf";
    
        [testStr substringToIndex:100];
    
    }
    
    

    然后,崩溃了,发现此swizzle方法完全没有被调用。

    类簇

    类簇 是一群隐藏在通用接口下的与实现相关的类,使得我们编写的代码可以独立于底层实现(因为接口是稳定的)。

    示例2

    将代码改成如下形式

    
    + (void)load {
    
        static dispatch_once_t onceToken;
    
        dispatch_once(&onceToken, ^{
    
            Class clazz = nil;
    
            id obj;
    
            /* 普通方法 */
    
            obj = [[NSString alloc] init];
    
            clazz = [obj class];
    
            [obj release];
    
            ACSwizzle(clazz,substringToIndex:);
    
        });
    
    }
    
    

    然而,根据友盟上统计的crash结果,仍有substringToIndex导致的崩溃问题。

    示例3

    示例2的崩溃问题是由于,不同形式声明的NSString产生的类簇有可能不同。为避免此问题,写了一个Demo去读取出不同NSString声明方式会出现的所有类。

    2017-12-26 15:19:39.378849+0800 TestClassType[3787:1570162] [NSString alloc] 's class is NSPlaceholderString

    2017-12-26 15:19:39.378881+0800 TestClassType[3787:1570162] [[NSString alloc] init] 's class is __NSCFConstantString

    2017-12-26 15:19:39.378896+0800 TestClassType[3787:1570162] @"as" 's class is __NSCFConstantString

    2017-12-26 15:19:39.378908+0800 TestClassType[3787:1570162] @"" 's class is __NSCFConstantString

    2017-12-26 15:19:39.378918+0800 TestClassType[3787:1570162] @"as".copy 's class is __NSCFConstantString

    2017-12-26 15:19:39.378942+0800 TestClassType[3787:1570162] ([NSString stringWithFormat:@"aa%@", @"a"]) 's class is NSTaggedPointerString

    2017-12-26 15:19:39.378998+0800 TestClassType[3787:1570162] [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] 's class is NSTaggedPointerString

    2017-12-26 15:19:39.379032+0800 TestClassType[3787:1570162]

    然后将所有的类簇都进行swizzle

    
    + (void)load {
    
        static dispatch_once_t onceToken;
    
        dispatch_once(&onceToken, ^{
    
            /* 普通方法 */
    
            NSArray *classNameList = @[
    
                                      @"__NSCFConstantString",
    
                                      @"NSTaggedPointerString"
    
                                      ];
    
            for (NSString *className in classNameList) {
    
                Class clazz = NSClassFromString(className);
    
                if (clazz) {
    
                    [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];
    
                }
    
            }
    
        });
    
    }
    
    

    经运行,发生了iOS 8设备100%崩溃无法使用的问题。

    示例4

    将自己查询类簇的Demo在iOS 8设备上运行,导出如下结果

    2017-12-26 15:16:37.673 TestClassType[389:48818] [NSString alloc] 's class is NSPlaceholderString

    2017-12-26 15:16:37.673 TestClassType[389:48818] [[NSString alloc] init] 's class is __NSCFConstantString

    2017-12-26 15:16:37.673 TestClassType[389:48818] @"as" 's class is __NSCFConstantString

    2017-12-26 15:16:37.674 TestClassType[389:48818] @"" 's class is __NSCFConstantString

    2017-12-26 15:16:37.674 TestClassType[389:48818] @"as".copy 's class is __NSCFConstantString

    2017-12-26 15:16:37.674 TestClassType[389:48818] ([NSString stringWithFormat:@"aa%@", @"a"]) 's class is __NSCFString

    2017-12-26 15:16:37.674 TestClassType[389:48818] [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] 's class is __NSCFString

    2017-12-26 15:16:37.674 TestClassType[389:48818]

    发现在iOS 8设备上,没有NSTaggedPointerString这种类型,如果对NSTaggedPointerString进行swizzle,就会出现崩溃。

    于是,想出一种复杂的判断各因素的方法,它将会考虑NSString不同声明形式的类簇的排重问题,NSString与NSMutableString的类的相同类簇的排重问题

    
    @implementation NSMutableString (AvoidCrash)
    
    + (void)load {
    
        static dispatch_once_t onceToken;
    
        dispatch_once(&onceToken, ^{
    
            id obj = [NSMutableString alloc];
    
            Class clazz;
    
            NSData*data      = [@"testdata" dataUsingEncoding:NSUTF8StringEncoding];
    
            NSArray *varList = @[
    
                [[[NSString alloc] init] autorelease],
    
                @"as",
    
                @"",
    
                @"as".copy,
    
                [NSString stringWithFormat:@"aa%@", @"a"],
    
                [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]
    
            ];
    
            NSArray *mutaVarList = @[
    
                [[[NSMutableString alloc] init] autorelease],
    
                @"as".mutableCopy,
    
                @"".mutableCopy,
    
                [NSMutableString stringWithString:@"as"],
    
                [[[NSMutableString alloc] initWithString:@"as"] autorelease],
    
                [[[NSMutableString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]
    
            ];
    
            [self swizzleForVarList:varList
    
                        mutaVarList:mutaVarList
    
                          varBlock:^(Class clazz) {
    
                    [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];
    
            } mutaVarBlock:^(Class clazz) {
    
                    [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];
    
            }];
    
        });
    
    }
    
    - (void)swizzleForVarList:(NSArray*)varList
    
                  mutaVarList:(NSArray*)mutaVarList
    
                    varBlock:(void (^)(Class clazz))varSwizzleBlock
    
                mutaVarBlock:(void (^)(Class clazz))mutaVarSwizzleBlock {
    
        // 使用Set,保证数据去重
    
        NSMutableSet *mutaClassList = [NSMutableSet set];
    
        NSMutableSet *classList    = [NSMutableSet set];
    
        for (NSString *var in mutaVarList) {
    
            // 将MutableXXX的变量转成类名存入mutaClassList
    
            [mutaClassList addObject:[var class]];
    
        }
    
        for (NSString *var in varList) {
    
            // 将XXX的变量转成类名存入classList
    
            [classList addObject:[var class]];
    
        }
    
        for (Class clazz in mutaClassList) {
    
            // 遍历MutableXXX类簇的各种隐藏子类,进行swizzle
    
            if (mutaVarSwizzleBlock) {
    
                mutaVarSwizzleBlock(clazz);
    
            }
    
        }
    
        for (Class clazz in classList) {
    
            // 有时MutableXXX与XXX类簇中的隐藏子类有相同的(比如NSString与NSMutableString都有__NSCFString)
    
            // 此处确保不会被swizzle两处
    
            if (![mutaClassList containsObject:clazz]
    
                && varSwizzleBlock) {
    
                varSwizzleBlock(clazz);
    
            }
    
        }
    
    }
    
    @end
    
    

    此时,无明显的问题。但在编写Unit Test遍历各种错误情况时,发现@"sa"这种形式的NSString在执行数组越界时仍会崩溃。

    经分析,@"sa"形式的类簇是__NSCFConstantString。而__NSCFConstantString的父类是__NSCFString。__NSCFConstantString的substringToIndex方法是实现在__NSCFString中的。此处就会发生父类、子类两次swizzle引起的问题,导致__NSCFConstantString的substringToIndex方法仍指向系统方法的IMP。

    Demo5

    而我们很难去识别类簇之间是否有继承关系,而继承关系的类簇的方法是否是只在父类中实现。

    所以最终,对避免crash想使用的高级辩别类簇的功能全线失败。我们使用简单的网络上归纳好的类簇进行swizzle,并对这些方法进行了详进的Unit Test编写测试。最终发现, 此化繁为简的方法,能够完美的解决所有问题。

    
    /* 普通方法 */
    
            // iOS 8是__NSCFConstantString,iOS 11上是__NSCFConstantString
    
            id obj = [[NSString alloc] init];
    
            Class clazz = [obj class];
    
            [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];
    
            // iOS 8上是__NSCFString, iOS 11上是NSTaggedPointerString
    
            id obj2 = [NSString stringWithFormat:@"aa%@", @"a"];
    
            if (![obj2 isKindOfClass:clazz]
    
                && ![obj isKindOfClass:[obj2 class]]) {
    
                // 若obj2与obj的类簇不同且不是继承关系,则进行swizzle
    
                // (__NSCFConstantString的父类是__NSCFString)
    
                clazz = [obj2 class];
    
                [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];
    
            }
    
    

    相关文章

      网友评论

      • 可能是老曹:目前实践经验最完备的一篇文章,没有之一
      • 5b9af1f92331:nice!
      • 夜OS:厉害了,写的一目了然,瞬间收获很多啊,这样确实可以起到皮面很多crash 的作用~
        大牛厉害了~:smile: :smile:

      本文标题:Swizzle应用性研究

      本文链接:https://www.haomeiwen.com/subject/fkwuoxtx.html