更新

2015-11-16

感谢微博好友@zyyy_000的评论,补充了为什么要在+ (void)load方法里面做Method Swizzling

前言

最近,在做项目时,因为某种原因,突然要“适配”iOS6(也是醉了。。。),保证极少数的iOS6用户可以“用上”新的版本。哪怕界面上有瑕疵,只要功能正常就行。于是就只好花几天时间对iOS6进行紧急适配(心中一万头驼羊奔跑而过。。。)

本文总结了一些常规的,和“非常规”的iOS项目向老版本兼容的办法,结合了宏定义CategoryRuntime,大家看着消遣一下就好哈~

重点概念

首先强调一些概念。

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版本,运行时检查系统版本

这个是最基本的适配手段。

用到的宏如下:

  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,则能否适配就看你的实现方式了。 不要想了。以新系统为主,兼容旧系统为辅。

iOS如何限制使用SDK的版本? 解决iOS项目的版本兼容问题的更多相关文章

  1. 解决低版本Xcode不支持高版本iOS真机调试的问题

    1.现象截图 Could not locate device support files. This iPhone 6s is running iOS 11.1 (15B93), which may ...

  2. iOS 9的新的改变 iOS SDK Release Notes for iOS 9 说了些改变

    iOS 9的新的改变 iOS SDK Release Notes for iOS 9 说了些改变   看了下还算能理解!!!有兴趣可以看看哈!!!不喜勿喷!!后面的对于废除的方法什么有用感觉!!!   ...

  3. iOS SDK Release Notes for iOS 9 iOS9 SDK 版本更新说明

    Important: This is a preliminary document for an API or technology in development. Apple is supplyin ...

  4. [iOS 开发] Xcode常见报错及解决办法

    报错一: 在iOS7的真机运行时,弹出错误:App installation failed. There was an internal API error. 如图 解决办法: 在Xcode -> ...

  5. 解决IOS safari在input focus弹出输入法时不支持position fixed的问题

    该文章为转载 我们在做移动web应用的时候,常常习惯于使用position:fixed把一个input框作为提问或者搜索框固定在页面底部.但在IOS的safari和webview中,对position ...

  6. iOS 中系统与 SDK 版本检测

    一.编译时检测 1. 判断 SDK 是否是某个版本或更高版本 ifdef __IPHONE_11_0 2.判断当前需要支持的最低版本 __IPHONE_OS_VERSION_MIN_REQUIRED ...

  7. Android SDK Manager 下载SDK失败的解决办法

    摘要:本文记录了无法使用Android SDK  Manager下载SDK开发包的解决办法. 最近需要进行android应用程序的开发工作,在android官网下载了adt-bundle-linux- ...

  8. SDK接入(3)之iOS内支付(In-App Purchase)接入

    SDK接入(3)之iOS内支付(In-App Purchase)接入 继整理了Android平台的SDK接入过程.再来分享下iOS平台的内支付(In-App Purchase)接入,作为笔者在游戏开发 ...

  9. 移动端上传照片 预览+Draw on Canvas's Demo(解决 iOS 等设备照片旋转 90 度的 bug)

    背景: 本人的一个移动端H5项目,需求如下: 需求一:手机相册选取或拍摄照片后在页面上预览 需求二:然后绘制在canvas画布上 这里,我们先看一个demo(http://jsfiddle.net/q ...

随机推荐

  1. Windows7使用无线网卡建立WiFi热点

    在Windows7下设置热点需要用到命令netsh wlan,具体的设置步骤如下: 1.配置热点 以管理员身份打开命令行模式,输入命令 netsh wlan set hostednetwork mod ...

  2. 感觉Google要搞事情

  3. Elasticsearch之中文分词器

    前提 什么是倒排索引? Elasticsearch之分词器的作用 Elasticsearch之分词器的工作流程 Elasticsearch之停用词 Elasticsearch的中文分词器 1.单字分词 ...

  4. Flume在企业大数据仓库架构中位置及功能

    Flume在企业大数据仓库架构中位置及功能 hadoop 数据仓库 flume 数据仓库架构 1.如下图所示,外部数据中,关系型数据库导入到HDFS用sqoop,由Nginx产生的文件实时监控用Flu ...

  5. 【cs231n】神经网络学习笔记3

    + mu) * v # 位置更新变了形式 对于NAG(Nesterov's Accelerated Momentum)的来源和数学公式推导,我们推荐以下的拓展阅读: Yoshua Bengio的Adv ...

  6. VirtualBox 安装增强功能失败 解决方法

    参考:virtualbox安装增强功能时[未能加载虚拟光盘] 在安装VirtualBox并配置虚拟机之后,想要安装增强功能:设备 => 安装增强功能. 出现未能加载虚拟光盘问题. 解决方法: 进 ...

  7. 读懂 ECMAScript 规格

    概述 规格文件是计算机语言的官方标准,详细描述语法规则和实现方法. 一般来说,没有必要阅读规格,除非你要写编译器.因为规格写得非常抽象和精炼,又缺乏实例,不容易理解,而且对于解决实际的应用问题,帮助不 ...

  8. 【Python】简单实现爬取小说《天龙八部》,并在页面本地访问

    背景 很多人说学习爬虫是提升自己的一个非常好的方法,所以有了第一次使用爬虫,水平有限,依葫芦画瓢,主要作为学习的记录. 思路 使用python的requests模块获取页面信息 通过re模块(正则表达 ...

  9. 在 Ubuntu 里如何下载、安装和配置 Plank Dock

    一个众所周知的事实就是,Linux 是一个用户可以高度自定义的系统,有很多选项可以选择 —— 作为操作系统,有各种各样的发行版,而对于单个发行版来说,又有很多桌面环境可以选择.与其他操作系统的用户一样 ...

  10. WPF TextBox 获得焦点后,文本框中的文字全选中

    textbox.GotFocus 事件处理 Textbox.SelectAll() 是不行的, 这样处理会发生的情况是: 1) textbox1 当前没有焦点, 内容为 someText. 2) 鼠标 ...