更新 2015-11-16 感謝微博好友@zyyy_000的評論,補充了爲何要在+ (void)load 方法裏面作Method Swizzling。html 前言 |
最近,在作項目時,由於某種緣由,忽然要「適配」iOS6(也是醉了。。。),保證極少數的iOS6用戶能夠「用上」新的版本。哪怕界面上有瑕疵,只要功能正常就行。因而就只好花幾天時間對iOS6進行緊急適配(心中一萬頭駝羊奔跑而過。。。)ios 本文總結了一些常規的,和「很是規」的iOS項目向老版本兼容的辦法,結合了宏定義、Category和Runtime,你們看着消遣一下就好哈~git 重點概念 首先強調一些概念。xcode Deployment Target 和 Base SDK Deployment Target 指的是你的APP能支持的最低系統版本,如要支持iOS6以上,就設置成iOS6便可。app Base SDK 指的是用來編譯APP的SDK(Software Development Kit)的版本,通常保持當前XCode支持的最新的就好,如iOS8.4。SDK其實就是包含了全部的你要用到的頭文件、連接庫的集合,你的APP裏面用的各類類、函數,能編譯、連接成最後的安裝包,就要靠它,蘋果每次升級系統,新推出的各類API,也是在SDK裏面。因此通常Base SDK確定是大於等於Deployment Target的版本。ide 區分 既然Base SDK的版本大於等於Deployment Target的版本,那麼就要當心了,由於「只要用到的類、方法,在當前的Base SDK版本里面存在,就能夠編譯經過!可是一旦運行APP的手機的系統版本低於這些類、方法的最低版本要求,APP就會Crash!」函數 因此並非說,能編譯經過的,就必定能運行成功!還要在運行時檢查!簡單來講,就是以下圖:post 宏只在編譯時生效! 宏定義只是純粹的文本替換,只在編譯時起做用。以下代碼:ui
1 2 3 |
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 NSLog(@"Tutuge"); #endif |
被宏定義包起來的代碼是否會執行,在編譯時就決定好了,不管你是用什麼系統運行,宏定義再也沒有什麼卵用=。=spa 編譯時檢查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的用戶,升個級唄=。= 參考
|