iOS如何限制使用SDK的版本? 解決iOS項目的版本兼容問題

 

更新

2015-11-16

感謝微博好友@zyyy_000的評論,補充了爲何要在+ (void)load方法裏面作Method Swizzlinghtml

前言


最近,在作項目時,由於某種緣由,忽然要「適配」iOS6(也是醉了。。。),保證極少數的iOS6用戶能夠「用上」新的版本。哪怕界面上有瑕疵,只要功能正常就行。因而就只好花幾天時間對iOS6進行緊急適配(心中一萬頭駝羊奔跑而過。。。)ios

本文總結了一些常規的,和「很是規」的iOS項目向老版本兼容的辦法,結合了宏定義CategoryRuntime,你們看着消遣一下就好哈~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版本,運行時檢查系統版本

這個是最基本的適配手段。

用到的宏以下:

  1. __IPHONE_OS_VERSION_MAX_ALLOWED: 值等於Base SDK,即用於檢查SDK版本的。
  2. __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的用戶,升個級唄=。=

參考

不能「強制用戶」。即便能,也不要這樣作。蘋果很是鼓勵開發者儘快適配新的系統,並拋棄老的系統。卻是能夠用舊版本的 SDK 編譯打包,若是你一直不升級 Xcode 的話。 可能會有問題,取決於你用的 API 和類。若是你用的 API 或類標明是NS_ENUM_AVAILABLE_IOS(8_0),那麼在 7.0、7.1 系統上就會crash。爲了同時適配這兩個系統,你能夠判斷一下系統版本,或者用respondsToSelector:@selector(……) 判斷應該使用新 or 老 API。 若是不加 LaunchScreen,會進入兼容模式,直接拉伸。效果確定是不完美的,就是字號、圖片全都拉大了,但也湊合能看。最好專門作適配。若是加了 LaunchScreen,則可否適配就看你的實現方式了。 不要想了。以新系統爲主,兼容舊系統爲輔。

相關文章
相關標籤/搜索