眾所周知,在iOS13中使用KVC設(shè)置UITextField占位符會(huì)導(dǎo)致崩潰,出于好奇,今天我對(duì)崩潰原因進(jìn)行了一番簡(jiǎn)單探索,現(xiàn)將探索過(guò)程記錄如下:
[textfield setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
上面這段代碼在iOS13之前一直工作良好,更新到iOS13后莫名其妙就崩潰了,需要通過(guò)設(shè)置attributedPlaceholder屬性來(lái)代替。很多人覺(jué)得是iOS13對(duì)KVC的支持變?nèi)趿?,我覺(jué)得不太可能,應(yīng)該是蘋(píng)果爸爸偷偷把私有實(shí)例變量名(或?qū)傩悦┙o改了。廢話不多說(shuō),開(kāi)始驗(yàn)證,首先下載一個(gè)Xcode11,分別在iOS13和iOS12系統(tǒng)中運(yùn)行以下代碼,通過(guò)runtime獲取并打印UITextField的所有屬性:
unsigned int count;
objc_property_t *ptr = class_copyPropertyList([textfield class], &count);
for (NSUInteger i = 0; i < count; i++) {
NSLog(@"%s", property_getName(ptr[i]));
}
free(ptr);
運(yùn)行結(jié)果顯示,無(wú)論是在iOS12還是iOS13環(huán)境下,都沒(méi)有“placeholderLabel”相關(guān)的屬性,所以_placeholderLabel不是UITextField的屬性。既然能用KVC訪問(wèn),不是屬性,就應(yīng)該是實(shí)例變量咯,繼續(xù)驗(yàn)證,我們通過(guò)runtime獲取并打印UITextField的所有實(shí)例變量:
unsigned int count2;
Ivar *ptr2 = class_copyIvarList([textfield class], &count2);
for (NSUInteger i = 0; i < count2; i++) {
NSLog(@"%s", ivar_getName(ptr2[i]));
}
free(ptr2);
果然,我們?cè)谌f(wàn)花叢中如愿找到了_placeholderLabel,遺憾的是,無(wú)論在iOS12還是iOS13環(huán)境下,UITextField都存在這個(gè)實(shí)例變量。顯然,我一開(kāi)始的推測(cè)被啪啪打臉了。

一條路走不通,馬上調(diào)整思路,去找其他線索,不防先來(lái)仔細(xì)研究一下蘋(píng)果給的崩潰信息

結(jié)合崩潰堆棧和拋出的異常信息繼續(xù)分析,首先,崩潰是發(fā)生在-[UITextField valueForKey:],也就是說(shuō),UITextField獲取_placeholderLabel時(shí)其實(shí)就已經(jīng)崩潰了;再來(lái)看異常信息,“access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug”,_placeholderLabel被禁止使用,這是一個(gè)應(yīng)用程序bug,不知道大家之前有沒(méi)有遇到這種異常,我是從來(lái)沒(méi)遇到過(guò),通常都是valueForUndefinedKey:異常。
接下來(lái)我們驗(yàn)證一下蘋(píng)果是否修改了valueForKey:的邏輯,我們找一個(gè)和_placeholderLabel同級(jí)別的實(shí)例變量_textContentView,同樣按照按照KVC方式去設(shè)置其背景色
[textfield setValue:[UIColor redColor] forKeyPath:@"_textContentView.backgroundColor"];
在iOS13環(huán)境下運(yùn)行代碼,沒(méi)有崩潰,而且textfield背景色被修改成了紅色,說(shuō)明這段代碼已經(jīng)生效了。至此,valueForKey:也得以沉冤得雪,退出了背鍋群!
還有一種可能,蘋(píng)果在通過(guò)valueForKey:獲取實(shí)例時(shí),對(duì)值為“_placeholderLabel”的key作了特殊處理,當(dāng)發(fā)現(xiàn)key為“_placeholderLabel”時(shí),拋出上文中提到的異常。怎么證明呢,我們知道KVC獲取實(shí)例時(shí),對(duì)下劃線并不敏感,我們可以去掉下劃線來(lái)試試。
[textfield setValue:[UIColor redColor] forKeyPath:@"placeholderLabel.textColor"];
iOS13環(huán)境下運(yùn)行?。?!巧了不是!巧了不是!可以正常工作了。忙活半天,總算找到原因了。天下武功出少林,天下iOS奇淫技巧出runtime,接下來(lái),又輪到萬(wàn)能的runtime上場(chǎng)啦,添加以下代碼,老代碼不用作任何修改,又可以天下太平了:
@implementation UITextField (KVC)
+ (void)load {
Method origin = class_getInstanceMethod([self class], @selector(valueForKey:));
Method swizzing = class_getInstanceMethod([self class], @selector(swizzing_valueForKey:));
if (class_addMethod([self class], @selector(valueForKey:), method_getImplementation(swizzing), method_getTypeEncoding(swizzing))) {
class_replaceMethod([self class], @selector(swizzing_valueForKey:), method_getImplementation(origin), method_getTypeEncoding(origin));
}
method_exchangeImplementations(origin, swizzing);
}
- (id)swizzing_valueForKey:(NSString *)key {
if ([key isEqualToString:@"_placeholderLabel"]) {
Ivar ivar = class_getInstanceVariable([self class], [key UTF8String]);
id value = object_getIvar(self, ivar);
return value;
} else {
return [self swizzing_valueForKey:key];
}
}
當(dāng)然,這段代碼主要是用來(lái)驗(yàn)證猜想,不建議在開(kāi)發(fā)中使用。蘋(píng)果的意圖很明顯,就是為了讓我們使用公開(kāi)的attributedPlaceholder屬性來(lái)代替私有API。
后記
這篇文章并沒(méi)有多少開(kāi)發(fā)干貨,主要是為了記錄一下自己探索的過(guò)程,希望能對(duì)今后定位同類(lèi)型問(wèn)題時(shí)提供一種思路。另外,文中如果有任何紕漏或錯(cuò)誤,歡迎大家批評(píng)指正!