Swizzle的常見(jiàn)錯(cuò)誤及基本原理
示例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是將兩個(gè)SEL指向的IMP互相替換。
originMethod想指向UIImageView的方法setContentMode乌叶,然而該方法是UIImageView的父類UIView實(shí)現(xiàn)的急鳄,所以UIImageView分類中的方法實(shí)際上是與UIView的setContentMode做了替換徐钠。在UIView的實(shí)例調(diào)用setContentMode時(shí)偷仿,會(huì)調(diào)用nty_setContentMode的SEL烁焙,UIView中沒(méi)有實(shí)現(xiàn)此方法抵蚊,導(dǎo)致崩潰.
見(jiàn)圖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的定義為:是一個(gè)指向 objc_selector 指針施绎,表示方法的名字/簽名。
IMP 的含義:是一個(gè)函數(shù)指針贞绳,這個(gè)被指向的函數(shù)包含一個(gè)接收消息的對(duì)象id(self 指針), 調(diào)用方法的選標(biāo) SEL (方法名)谷醉,以及不定個(gè)數(shù)的方法參數(shù),并返回一個(gè)id冈闭。也就是說(shuō) IMP 是消息最終調(diào)用的執(zhí)行代碼俱尼,是方法真正的實(shí)現(xiàn)代碼 。
引申:class
struct objc_class {
struct objc_class super_class; /*父類*/
const char *name; /*類名字*/
long version; /*版本信息*/
long info; /*類信息*/
long instance_size; /*實(shí)例大小*/
struct objc_ivar_list *ivars; /*實(shí)例參數(shù)鏈表*/
struct objc_method_list **methodLists; /*方法鏈表*/
struct objc_cache *cache; /*方法緩存*/
struct objc_protocol_list *protocols; /*協(xié)議鏈表*/
};
methodLists方法鏈表里面存儲(chǔ)的是Method 類型萎攒。selector 就是指 Method的 SEL, address就是指Method的 IMP遇八。
示例1優(yōu)化
示例1證明,直接使用method_exchangeImplementations進(jìn)行swizzle耍休,有可能出現(xiàn)崩潰問(wèn)題刃永。使用第三方庫(kù)JRSwizzle的方法jr_swizzleMethod:withMethod:error:對(duì)該問(wèn)題進(jìn)行了優(yōu)化。
+ (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_ {
#if OBJC_API_VERSION >= 2
Method origMethod = class_getInstanceMethod(self, origSel_);
if (!origMethod) {
...(容錯(cuò)處理羊精,節(jié)約篇幅斯够,省略)
return NO;
}
Method altMethod = class_getInstanceMethod(self, altSel_);
if (!altMethod) {
...(容錯(cuò)處理,節(jié)約篇幅喧锦,省略)
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的配置方式读规,節(jié)約篇幅,省略)
#endif
}
該方法通過(guò)class_addMethod保證在父類實(shí)現(xiàn)原生方法或被swizzle方法而子類沒(méi)有實(shí)現(xiàn)的情況下燃少,重新生成一個(gè)新的Method束亏,SEL不變,IMP指向父類方法的IMP阵具,保存在子類的method_list中(即將子類中實(shí)現(xiàn)同樣的方法)碍遍。
class_addMethod:如果發(fā)現(xiàn)方法已經(jīng)存在扶欣,會(huì)失敗返回艰毒,也可以用來(lái)做檢查用,我們這里是為了避免源方法沒(méi)有實(shí)現(xiàn)的情況;如果方法沒(méi)有存在,我們則先嘗試添加被替換的方法的實(shí)現(xiàn)
示例2
通過(guò)jr_swizzleMethod:withMethod:error:進(jìn)行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時(shí)隅要,見(jiàn)圖3.
在method_exchangeImplementations后惜论,見(jiàn)圖4.
當(dāng)前凯旭,可以完美解決方問(wèn)題
示例3
針對(duì)示例1, 如果不使用jr_swizzleMethod:withMethod:error:的方式惫霸,仍有辦法解決此問(wèn)題封恰。
示例1之所以崩潰是因?yàn)樵赨IView執(zhí)行setContentMode時(shí)矮烹,會(huì)調(diào)用UIView不存在的方法nty_setContentMode越庇。那么,將swizzle的方法從UIImageView的分類中改為寫(xiě)在UIView的分類中奉狈,即可解決此問(wèn)題卤唉。
@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
若由于需求原因,既有針對(duì)UIView的setContentMode的swizzle方法仁期,也有針對(duì)UIImageView的swizzle方法(即示例2與示例3共存)桑驱。將會(huì)發(fā)生邏輯錯(cuò)誤。
兩個(gè)swizzle都是寫(xiě)在分類的+load方法中跛蛋,兩方法的調(diào)用順序與build phase中的文件編繹順序有關(guān)熬的。此處,我們假設(shè)UIView (TestContentMode_Origin)的+load先被調(diào)用
見(jiàn)圖5
UIImageView(TestContentMode_Origin)的+load再被調(diào)用
見(jiàn)圖6?7
那么此時(shí)赊级,若UIView調(diào)用setContentMode不會(huì)有問(wèn)題押框,UIImageView調(diào)用時(shí)會(huì)出現(xiàn)無(wú)限調(diào)用循環(huán)的問(wèn)題
拓展:RSSwizzle提供了另外一種更加健壯的Swizzle方式,如以下代碼所示理逊。但此代碼在我們項(xiàng)目中沒(méi)有普及橡伞,我也沒(méi)有確認(rèn)此方法是否會(huì)出現(xiàn)其他問(wèn)題,此處列出僅供參考晋被。
RSSwizzleInstanceMethod([UIView class],
@selector(setContentMode:),
RSSWReturnType(void),
RSSWArguments(UIViewContentMode contentMode),
RSSWReplacement({
// Returning modified return value.
NSLog(@"swizzle contentmode %@", @(contentMode));
// 先執(zhí)行原始方法
RSSWCallOriginal();
}), 0, NULL);
示例5
針對(duì)示例4的需求兑徘,建議將UIImageView的swizzle方法寫(xiě)到UIView的分類中。即示例3的代碼羡洛。那么代碼會(huì)變成以下的樣式挂脑。
@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 {
// 執(zhí)行針對(duì)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]]) {
// 執(zhí)行針對(duì)UIImageView的swizzle的邏輯
}
[self nty_setContentMode:contentMode];
}
@end
見(jiàn)圖8
由于兩個(gè)分類的swizzle名字相同,通過(guò)class_getInstanceMethod獲得nty_setContentMode的Method將一直是同一個(gè)(該問(wèn)題出現(xiàn)原因需要詳細(xì)了解class翘县、category實(shí)現(xiàn)機(jī)制最域,此處不多做綴述),所以相當(dāng)于兩個(gè)Method互相swizzle了兩次锈麸,最終SEL與IMP的連接仍為圖8的結(jié)果镀脂。
示例6
將示例5的代碼做一點(diǎn)點(diǎn)調(diào)整,將UIView(ForUIImageViewSwizzle)中替換nty_setContentMode方法名改為nty2_setContentMode
見(jiàn)圖9?10?11
最終成功完成需求
Swizzle在項(xiàng)目中應(yīng)用出現(xiàn)的問(wèn)題
iOS項(xiàng)目在很多方法中如果傳參不對(duì)忘伞,會(huì)直接導(dǎo)致crash薄翅。比如NSString的substringToIndex:方法在數(shù)組越界時(shí)沙兰、NSDictionary傳入nil值時(shí)、NSArray數(shù)組越界時(shí)翘魄。這些情況鼎天,我們可能用swizzle將這些系統(tǒng)方法進(jìn)行swizzle,加入數(shù)據(jù)空值暑竟、數(shù)組越界情況的容錯(cuò)處理斋射,有效減少崩潰率。
此處但荤,以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中寫(xiě)下測(cè)試代碼測(cè)試此功能
- (void)testCrash {
NSString *testStr = @"asdf";
[testStr substringToIndex:100];
}
然后,崩潰了腹躁,發(fā)現(xiàn)此swizzle方法完全沒(méi)有被調(diào)用桑包。
類簇
類簇 是一群隱藏在通用接口下的與實(shí)現(xiàn)相關(guān)的類,使得我們編寫(xiě)的代碼可以獨(dú)立于底層實(shí)現(xiàn)(因?yàn)榻涌谑欠€(wěn)定的)纺非。
示例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:);
});
}
然而哑了,根據(jù)友盟上統(tǒng)計(jì)的crash結(jié)果,仍有substringToIndex導(dǎo)致的崩潰問(wèn)題烧颖。
示例3
示例2的崩潰問(wèn)題是由于弱左,不同形式聲明的NSString產(chǎn)生的類簇有可能不同。為避免此問(wèn)題倒信,寫(xiě)了一個(gè)Demo去讀取出不同NSString聲明方式會(huì)出現(xiàn)的所有類科贬。
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]
然后將所有的類簇都進(jìn)行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];
}
}
});
}
經(jīng)運(yùn)行,發(fā)生了iOS 8設(shè)備100%崩潰無(wú)法使用的問(wèn)題鳖悠。
示例4
將自己查詢類簇的Demo在iOS 8設(shè)備上運(yùn)行榜掌,導(dǎo)出如下結(jié)果
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]
發(fā)現(xiàn)在iOS 8設(shè)備上,沒(méi)有NSTaggedPointerString這種類型乘综,如果對(duì)NSTaggedPointerString進(jìn)行swizzle憎账,就會(huì)出現(xiàn)崩潰。
于是卡辰,想出一種復(fù)雜的判斷各因素的方法胞皱,它將會(huì)考慮NSString不同聲明形式的類簇的排重問(wèn)題,NSString與NSMutableString的類的相同類簇的排重問(wèn)題
@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九妈,保證數(shù)據(jù)去重
NSMutableSet *mutaClassList = [NSMutableSet set];
NSMutableSet *classList = [NSMutableSet set];
for (NSString *var in mutaVarList) {
// 將MutableXXX的變量轉(zhuǎn)成類名存入mutaClassList
[mutaClassList addObject:[var class]];
}
for (NSString *var in varList) {
// 將XXX的變量轉(zhuǎn)成類名存入classList
[classList addObject:[var class]];
}
for (Class clazz in mutaClassList) {
// 遍歷MutableXXX類簇的各種隱藏子類反砌,進(jìn)行swizzle
if (mutaVarSwizzleBlock) {
mutaVarSwizzleBlock(clazz);
}
}
for (Class clazz in classList) {
// 有時(shí)MutableXXX與XXX類簇中的隱藏子類有相同的(比如NSString與NSMutableString都有__NSCFString)
// 此處確保不會(huì)被swizzle兩處
if (![mutaClassList containsObject:clazz]
&& varSwizzleBlock) {
varSwizzleBlock(clazz);
}
}
}
@end
此時(shí),無(wú)明顯的問(wèn)題萌朱。但在編寫(xiě)Unit Test遍歷各種錯(cuò)誤情況時(shí)宴树,發(fā)現(xiàn)@"sa"這種形式的NSString在執(zhí)行數(shù)組越界時(shí)仍會(huì)崩潰。
經(jīng)分析晶疼,@"sa"形式的類簇是__NSCFConstantString酒贬。而__NSCFConstantString的父類是__NSCFString又憨。__NSCFConstantString的substringToIndex方法是實(shí)現(xiàn)在__NSCFString中的。此處就會(huì)發(fā)生父類锭吨、子類兩次swizzle引起的問(wèn)題蠢莺,導(dǎo)致__NSCFConstantString的substringToIndex方法仍指向系統(tǒng)方法的IMP。
Demo5
而我們很難去識(shí)別類簇之間是否有繼承關(guān)系零如,而繼承關(guān)系的類簇的方法是否是只在父類中實(shí)現(xiàn)躏将。
所以最終,對(duì)避免crash想使用的高級(jí)辯別類簇的功能全線失敗埠况。我們使用簡(jiǎn)單的網(wǎng)絡(luò)上歸納好的類簇進(jìn)行swizzle耸携,并對(duì)這些方法進(jìn)行了詳進(jìn)的Unit Test編寫(xiě)測(cè)試棵癣。最終發(fā)現(xiàn)辕翰, 此化繁為簡(jiǎn)的方法,能夠完美的解決所有問(wèn)題狈谊。
/* 普通方法 */
// 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的類簇不同且不是繼承關(guān)系河劝,則進(jìn)行swizzle
// (__NSCFConstantString的父類是__NSCFString)
clazz = [obj2 class];
[clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];
}