更新 2015-11-16 感謝微博好友@zyyy_000的評論,補充了為什么要在+ (void)load 方法里面做Method Swizzling。 前言 |
最近,在做項目時,因為某種原因,突然要“適配”iOS6(也是醉了。。。),保證極少數的iOS6用戶可以“用上”新的版本。哪怕界面上有瑕疵,只要功能正常就行。於是就只好花幾天時間對iOS6進行緊急適配(心中一萬頭駝羊奔跑而過。。。) 本文總結了一些常規的,和“非常規”的iOS項目向老版本兼容的辦法,結合了宏定義、Category和Runtime,大家看着消遣一下就好哈~ 重點概念 首先強調一些概念。 Deployment Target 和 Base SDK Deployment Target 指的是你的APP能支持的最低系統版本,如要支持iOS6以上,就設置成iOS6即可。 Base SDK 指的是用來編譯APP的SDK(Software Development Kit)的版本,一般保持當前XCode支持的最新的就好,如iOS8.4。SDK其實就是包含了所有的你要用到的頭文件、鏈接庫的集合,你的APP里面用的各種類、函數,能編譯、鏈接成最后的安裝包,就要靠它,蘋果每次升級系統,新推出的各種API,也是在SDK里面。所以一般Base SDK肯定是大於等於Deployment Target的版本。 區分 既然Base SDK的版本大於等於Deployment Target的版本,那么就要小心了,因為“只要用到的類、方法,在當前的Base SDK版本里面存在,就可以編譯通過!但是一旦運行APP的手機的系統版本低於這些類、方法的最低版本要求,APP就會Crash!” 所以並不是說,能編譯通過的,就一定能運行成功!還要在運行時檢查!簡單來說,就是如下圖:  宏只在編譯時生效! 宏定義只是純粹的文本替換,只在編譯時起作用。如下代碼:
1 2 3 |
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 NSLog(@"Tutuge"); #endif |
被宏定義包起來的代碼是否會執行,在編譯時就決定好了,無論你是用什么系統運行,宏定義再也沒有什么卵用=。= 編譯時檢查SDK版本,運行時檢查系統版本 這個是最基本的適配手段。 用到的宏如下:
- __IPHONE_OS_VERSION_MAX_ALLOWED: 值等於Base SDK,即用於檢查SDK版本的。
- __IPHONE_OS_VERSION_MIN_REQUIRED: 值等於Deployment Target,檢查支持的最小系統版本。
運行時檢查系統版本:
1 2 3 |
if ([UIDevice currentDevice].systemVersion.floatValue > 8.0f) { // ... } |
假如我們現在想用iOS8新的UIAlertController來顯示提示框,應該如下判斷:
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 |
// 編譯時判斷:檢查SDK版本 #if __IPHONE_OS_VERSION_MAX_ALLOWED > 80000 // 運行時判斷:檢查當前系統版本 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 |
總的來說就是編譯時、運行時的判斷均不能少。 Weakly Linked - 運行時檢查類、方法是否可用 除了用宏、系統版本檢測,還可以用Weakly Linked特性做運行時的檢查。 對於iOS4.2以上的,有NS_CLASS_AVAILABLE標示的類,可以如下判斷是否可用:
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 { // 使用舊的方案... } |
對於方法,如下判斷:
1 2 3 4 5 |
if ([UITableViewCell instancesRespondToSelector:@selector (setSeparatorInset:)]) { // ... } else { // ... } |
至於用哪種方法,統一一下即可。 用Method Swizzling做兼容 有關Runtime、Method Swizzling的資料很多,各位自行閱讀哈~ 在+ (void)load 方法里面做替換 這里提一下為什么要在+ (void)load 方法里面做Method Swizzling。 在Objective-C中,運行時會自動調用每個類的兩個方法。+ (void)load 會在類、Category初始加載時調用,+ (void)initialize 會在第一次調用類的類方法或實例方法之前被調用。 但是需要注意的是,+ (void)initialize 是可以被Category覆蓋重寫的,並且有多個Category都重寫了+ (void)initialize 方法時,只會運行其中一個,所以在+ (void)initialize 里面做Method Swizzling顯然是不行的。 而+ (void)load 方法只要實現了,就一定會調用。具體為什么大家可以自行閱讀Runtime的源碼,或者查閱相關文章。 用dispatch_once保證只運行一次 因為Method Swizzling的影響是全局的,而且一旦多次調用,會出錯,所以這個時候用dispatch_once就再合適不過了~ 實例 下面就是利用Method Swizzling做兼容的一個例子。 有時候,不同版本之間,同一個類、View控件的默認屬性可能都會變化,如UILabel的背景色在iOS6上,默認是白色,而iOS6以后是透明的!如果在每個用到UILabel的地方,都手動設置一次背景色,代價太大。這個時候就需要Runtime的“黑魔法”上場。 就以設置UILabel的默認背景色透明為例,就是在UILabel初始化時,如initWithFrame之前,先設置好透明背景色,簡單的示例如下:
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 |
// 創建Category @implementation UILabel (TTGCompatibility)
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 先判斷系統版本,盡量減少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]; // 設置透明背景色 ((UILabel *)newSelf).backgroundColor = [UIColor clearColor]; return newSelf; }
// initWithCoder... |
運行時添加“Dummy”方法,減少代碼改動 Dummy,意思是“假的、假動作、假人”,在這里指的是為舊版本不存在的方法提供一個“假的”替代方法,防止因新API找不到而導致的Crash。 以UITableViewCell的“setSeparatorInset:”方法為例,在iOS6中,壓根就不存在separatorInset,但是現有的代碼里面大量的調用了這個方法,怎么辦?難道一個一個的去加上判斷條件?代價太大。 這個時候就可以用Runtime的手段,在運行時添加一個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 { // 編譯時判斷SDK #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_7_0 // 運行時判斷系統版本 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:消息。 } |
總結 在適配舊版本時,除了基本的宏定義、[UIDevice currentDevice].systemVersion判斷,適當的用Runtime,可以大大減少對現有代碼的“干涉”,多種方法相結合才是最好的。 嗯,還在用iOS6的用戶,升個級唄=。= 參考
|