iOS runtime等底层知识iOS开发札记ios

Swizzle应用性研究

2018-01-17  本文已影响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];

        }

上一篇下一篇

猜你喜欢

热点阅读