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
引申: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.
在method_exchangeImplementations后,见图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
UIImageView(TestContentMode_Origin)的+load再被调用
见图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
由于两个分类的swizzle名字相同,通过class_getInstanceMethod获得nty_setContentMode的Method将一直是同一个(该问题出现原因需要详细了解class、category实现机制,此处不多做缀述),所以相当于两个Method互相swizzle了两次,最终SEL与IMP的连接仍为图8的结果。
示例6
将示例5的代码做一点点调整,将UIView(ForUIImageViewSwizzle)中替换nty_setContentMode方法名改为nty2_setContentMode
见图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];
}