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];
}