更新 2015-11-16 感謝微博好友@zyyy_000 的評(píng)論,,補(bǔ)充了為什么要在+ (void)load
方法里面做Method Swizzling 。
前言 最近,,在做項(xiàng)目時(shí),,因?yàn)槟撤N原因,突然要“適配”iOS6(也是醉了,。,。。),,保證極少數(shù)的iOS6用戶可以“用上”新的版本,。哪怕界面上有瑕疵,只要功能正常就行,。于是就只好花幾天時(shí)間對(duì)iOS6進(jìn)行緊急適配(心中一萬(wàn)頭駝羊奔跑而過(guò),。。,。)
本文總結(jié)了一些常規(guī)的,,和“非常規(guī)”的iOS項(xiàng)目向老版本兼容的辦法,,結(jié)合了宏定義 、Category 和Runtime ,,大家看著消遣一下就好哈~
重點(diǎn)概念 首先強(qiáng)調(diào)一些概念,。
Deployment Target 和 Base SDK Deployment Target 指的是你的APP能支持的最低系統(tǒng)版本,如要支持iOS6以上,,就設(shè)置成iOS6即可,。
Base SDK 指的是用來(lái)編譯APP的SDK(Software Development Kit)的版本,一般保持當(dāng)前XCode支持的最新的就好,,如iOS8.4,。SDK其實(shí)就是包含了所有的你要用到的頭文件、鏈接庫(kù)的集合,,你的APP里面用的各種類,、函數(shù),能編譯,、鏈接成最后的安裝包,,就要靠它,蘋(píng)果每次升級(jí)系統(tǒng),,新推出的各種API,,也是在SDK里面。所以一般Base SDK肯定是大于等于Deployment Target的版本,。
區(qū)分 既然Base SDK的版本大于等于Deployment Target的版本,那么就要小心了,,因?yàn)?strong>“只要用到的類,、方法,在當(dāng)前的Base SDK版本里面存在,,就可以編譯通過(guò) ,!但是一旦運(yùn)行APP的手機(jī)的系統(tǒng)版本低于這些類、方法的最低版本要求,,APP就會(huì)Crash,!”
所以并不是說(shuō),能編譯通過(guò)的,,就一定能運(yùn)行成功 ,!還要在運(yùn)行時(shí)檢查 !簡(jiǎn)單來(lái)說(shuō),,就是如下圖:
宏只在編譯時(shí)生效,! 宏定義只是純粹的文本替換,只在編譯時(shí)起作用,。如下代碼:
1 2 3 #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 NSLog(@"Tutuge"); #endif
被宏定義包起來(lái)的代碼是否會(huì)執(zhí)行,,在編譯時(shí)就決定好了,,無(wú)論你是用什么系統(tǒng)運(yùn)行,宏定義再也沒(méi)有什么卵用=,。=
編譯時(shí)檢查SDK版本,,運(yùn)行時(shí)檢查系統(tǒng)版本 這個(gè)是最基本的適配手段。
用到的宏如下:
__IPHONE_OS_VERSION_MAX_ALLOWED : 值等于Base SDK,,即用于檢查SDK版本的,。
__IPHONE_OS_VERSION_MIN_REQUIRED : 值等于Deployment Target,檢查支持的最小系統(tǒng)版本,。
運(yùn)行時(shí)檢查系統(tǒng)版本:
1 2 3 if ([UIDevice currentDevice].systemVersion.floatValue > 8.0f) { // ... }
假如我們現(xiàn)在想用iOS8新的UIAlertController 來(lái)顯示提示框,,應(yīng)該如下判斷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 編譯時(shí)判斷:檢查SDK版本 #if __IPHONE_OS_VERSION_MAX_ALLOWED > 80000 // 運(yùn)行時(shí)判斷:檢查當(dāng)前系統(tǒng)版本 if ([UIDevice currentDevice].systemVersion.floatValue > 8.0f) { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Tutuge" message:@"Compatibility" preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { NSLog(@"Cancel"); }]]; [self presentViewController:alertController animated:YES completion:nil]; } else { // 用舊的代替 UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Tutuge" message:@"Compatibility" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:nil]; [alertView show]; } #else // ... #endif
總的來(lái)說(shuō)就是編譯時(shí)、運(yùn)行時(shí)的判斷均不能少,。
Weakly Linked - 運(yùn)行時(shí)檢查類,、方法是否可用 除了用宏、系統(tǒng)版本檢測(cè),,還可以用Weakly Linked 特性做運(yùn)行時(shí)的檢查,。
對(duì)于iOS4.2以上的,有NS_CLASS_AVAILABLE 標(biāo)示的類,,可以如下判斷是否可用:
1 2 3 4 5 6 7 8 #if __IPHONE_OS_VERSION_MAX_ALLOWED > 80000 // Weakly Linked判斷 if ([UIAlertController class]) { // 使用UIAlertController... } else { // 使用舊的方案... } #endif
也可以如下判斷:
1 2 3 4 5 6 Class class = NSClassFromString (@"UIAlertController"); if (class) { // 使用UIAlertController... } else { // 使用舊的方案... }
對(duì)于方法,,如下判斷:
1 2 3 4 5 if ([UITableViewCell instancesRespondToSelector:@selector (setSeparatorInset:)]) { // ... } else { // ... }
至于用哪種方法,統(tǒng)一一下即可,。
用Method Swizzling做兼容 有關(guān)Runtime,、Method Swizzling的資料很多,各位自行閱讀哈~
在+ (void)load
方法里面做替換 這里提一下為什么要在+ (void)load
方法里面做Method Swizzling,。
在Objective-C中,,運(yùn)行時(shí)會(huì)自動(dòng)調(diào)用每個(gè)類的兩個(gè)方法。+ (void)load
會(huì)在類,、Category初始加載時(shí)調(diào)用,,+ (void)initialize
會(huì)在第一次調(diào)用類的類方法或?qū)嵗椒ㄖ氨徽{(diào)用。
但是需要注意的是,,+ (void)initialize
是可以被Category覆蓋重寫(xiě)的,,并且有多個(gè)Category都重寫(xiě)了+ (void)initialize
方法時(shí),只會(huì)運(yùn)行其中一個(gè),,所以在+ (void)initialize
里面做Method Swizzling顯然是不行的,。
而+ (void)load
方法只要實(shí)現(xiàn)了,就一定會(huì)調(diào)用,。具體為什么大家可以自行閱讀Runtime的源碼,,或者查閱相關(guān)文章。
用dispatch_once保證只運(yùn)行一次 因?yàn)镸ethod Swizzling的影響是全局的,,而且一旦多次調(diào)用,,會(huì)出錯(cuò),,所以這個(gè)時(shí)候用dispatch_once就再合適不過(guò)了~
實(shí)例 下面就是利用Method Swizzling做兼容的一個(gè)例子。 有時(shí)候,,不同版本之間,,同一個(gè)類、View控件的默認(rèn)屬性可能都會(huì)變化,,如UILabel的背景色在iOS6上,,默認(rèn)是白色,而iOS6以后是透明的,!如果在每個(gè)用到UILabel的地方,,都手動(dòng)設(shè)置一次背景色,代價(jià)太大,。這個(gè)時(shí)候就需要Runtime的“黑魔法”上場(chǎng),。
就以設(shè)置UILabel的默認(rèn)背景色透明為例,就是在UILabel初始化時(shí),,如initWithFrame之前,,先設(shè)置好透明背景色,簡(jiǎn)單的示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 創(chuàng)建Category @implementation UILabel (TTGCompatibility) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 先判斷系統(tǒng)版本,,盡量減少Runtime的作用范圍 if ([UIDevice currentDevice].systemVersion.floatValue < 7.0f) { // Method Swizzling // initWithFrame Method oriMethod = class_getInstanceMethod(self, @selector(initWithFrame:)); Method newMethod = class_getInstanceMethod(self, @selector(compatible_initWithFrame:)); method_exchangeImplementations(oriMethod, newMethod); // initWithCoder... } }); } // initWithFrame - (id)compatible_initWithFrame:(CGRect)frame { id newSelf = [self compatible_initWithFrame:frame]; // 設(shè)置透明背景色 ((UILabel *)newSelf).backgroundColor = [UIColor clearColor]; return newSelf; } // initWithCoder...
運(yùn)行時(shí)添加“Dummy”方法,,減少代碼改動(dòng) Dummy,意思是“假的,、假動(dòng)作,、假人”,在這里指的是為舊版本不存在的方法提供一個(gè)“假的”替代方法,,防止因新API找不到而導(dǎo)致的Crash,。
以UITableViewCell的“setSeparatorInset:” 方法為例,在iOS6中,,壓根就不存在separatorInset,但是現(xiàn)有的代碼里面大量的調(diào)用了這個(gè)方法,,怎么辦,?難道一個(gè)一個(gè)的去加上判斷條件?代價(jià)太大,。
這個(gè)時(shí)候就可以用Runtime的手段,,在運(yùn)行時(shí)添加一個(gè)Dummy方法,去“代替接收 ”setSeparatorInset消息,,防止在iOS6上的Crash,。
代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @implementation UITableViewCell (TTGCompatibility) + (void)load { // 編譯時(shí)判斷SDK #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_7_0 // 運(yùn)行時(shí)判斷系統(tǒng)版本 if ([UIDevice currentDevice].systemVersion.floatValue < 7.0f) { Method newMethod = class_getInstanceMethod(self, @selector(compatible_setSeparatorInset:)); // 增加Dummy方法 class_addMethod( self, @selector(setSeparatorInset:), method_getImplementation(newMethod), method_getTypeEncoding(newMethod)); } #endif } // setSeparatorInset: 的Dummy方法 - (void)compatible_setSeparatorInset:(UIEdgeInsets) inset { // 空方法都可以,只是為了接收setSeparatorInset:消息,。 }
總結(jié) 在適配舊版本時(shí),,除了基本的宏定義,、[UIDevice currentDevice].systemVersion判斷,適當(dāng)?shù)挠肦untime,,可以大大減少對(duì)現(xiàn)有代碼的“干涉”,,多種方法相結(jié)合才是最好的。
嗯,,還在用iOS6的用戶,,升個(gè)級(jí)唄=。=
參考