前言
众所周知,Android 适用于众多类型的设备,从手机到平板电脑和电视都能搭载使用。为了能在所有这些设备上顺利运行,Android 系统在应用到设备上时,必不可少的需要处理与 Android 应用的兼容性问题。这里就牵扯出两个概念:设备兼容性与应用兼容性。
- 设备兼容性:设备能够正常运行我们编写的 Android 应用。
- 应用兼容性:针对市面上千奇百怪的 Android 设备,应用是否兼容每一种可能的设备配置。
对于Android 应用开发者来说 ,我们无需担心设备是否兼容 Android,而是更加关注于我们开发的应用能够在尽可能多的 Android 设备上正常运行,即,上面所说的应用兼容性。
而一个应用的兼容性所覆盖的内容较多,从设备功能到平台版本再到屏幕配置,以及针对不同的国家或语言做出的修改,每一部分能涉及相当多的内容。
这篇文章就先介绍最常见的版本兼容性,顺便帮助大家理解 Android 开发中常见的几个版本相关的属性:minSdkVersion
、targetSdkVersion
、maxSdkVersion
、compileSdkVersion
。
自己设计版本兼容
在说明 Android 的应用兼容性之前,我们先做这么一个假设:如果我们自己是 Android API 的开发者,为了让更多的 Android 应用能够跑在我们的系统上,我们应该处理版本兼容问题
一、版本号如何确立
这里我们简单地把 Android 框架 API 想象为一个给其他开发者使用的库。如果我们开发了一个库,让别的开发者拿去用,那么第一个问题就来了,那就是版本号的问题。在几乎所有情况下,我们每发一个版本,都会用一个依次增长的整数来表明这个库是什么版本。这里我们也从1开始来发一个版本:
// 版本号 1 的 平台 API
public boolean doSomething() { /*do something ... */ return true;}
public void print(){
System.out.println("hello version 1");
}
好了,那么这就是我们发的版本号为1的库了。里面包含了两个方法,一个返回库的版本号,一个仅仅做了打印的操作。
二、升级能直接修改代码吗
随着时间的推移,我们现在需要升级一下这个库。之前已经确定了版本号为一个不断增加的整数,之前发的版本号为1,那我们现在需要更新版本号为2的库了。
那么问题就来了,我们现在发现之前库中print()
这个方法不太好,或者说打印的字符串不太对,那我们要怎么修改。这就牵扯出以下三个问题了:
- 能不能删除之前版本中的方法
- 能不能直接修改之前版本中的方法实现
- 能不能直接修改方法名
很遗憾的说,上面的三个问题的答案都是否定的。如果直接删除print()
方法,那么外部使用之前版本的应用一旦升级,就会因为找不到方法而崩溃;同理,修改方法名也是一样的;至于直接修改方法中的实现呢,那造成的后果可能会更加严重。这里如果我们更改了打印的字符串,外部引用会明显的发现这里不对,跟上个版本不一样;如果你说一个字符串还好,那我可以举一个极端的例子,原本这个方法是打开摄像头的,但是下一个版本你改成闪光灯了,这样外部引用此库的应用升级库版本之后是完全无法工作的。
这里就需要确认一个升级库的约定:向后兼容,进行新增更改!
虽然老的方法不能删除,那么我们可以增加新的方法啊,并且为了标识老的方法不再被维护,可以添加@Deprecated
注解。于是下面就是我们的版本号为2的库:
//版本号2 的 平台 API
public boolean doSomething() { /*do something ... */ return true;}
@Deprecated
public void print(){
System.out.println("hello version 1");
}
public void printNew() {
System.out.println("hello verson 2");
}
这里在这个版本号为2的库中,增加了一个printNew()
方法,并保留了版本1中的所有内容,只不过将print()
方法标记为废弃了。
在开发一个依赖库的时候,我们需要只要考虑到这两方面的问题就基本可以解决版本兼容问题。但 Android API 版本并不是一个依赖库,它还需要被安装在各个设备上,因此我们还需要往下讨论几个问题。
三、应用需要告知什么信息
在继续讨论之前,我们先把依赖库的设想放下,而是将我们开发的东西想象为 Android API,它需要被安装在各种设备之上。由于我们刚发了两个版本,那么现在市面上就会有版本号为1和版本号为2的设备,而未来会有更多的版本号。
现在有这么一个应用,它是依赖版本号为 2 的 API 版本开发的,而且在这个应用中使用到了 printNew()
方法。那这问题就来了,这个应用能安装在市面上所有的设备上么?显然不能:假如它被安装在一个 API 版本号为 1 设备上,而这个设备上是没有printNew()
方法的,这样的话,应用就会因为找不到方法而崩溃。
因此,一个应用,在被安装到设备上时,必须能够告知设备一些信息。在这里,必须告知设备的信息就是应用在开发时是基于哪一个 API 版本进行开发的。但是在第二条里,我们确定了平台 API 的开发必须是新增更改,这就意味着一个应用如果是基于某一个平台版本开发的,那么在这个平台版本后续的版本上也能够完全支持这个应用。在 Android 开发里就是:Android 应用一般向前兼容新版本的 Android 平台,这个我们后面再说。
于是乎我们需要知道并不是这个应用是基于哪个 API 版本进行开发的,而是它最低能跑在哪个 API 版本之上。在这里,由于它使用了在API版本号为1的平台中没有的printNew()
方法,因此这个应用只能指定为 2 了。而且由于保证了上面的平台 API 升级的约定,它既然能在版本号 2 上跑,那么它也就能够在 3、4、5... 以及后续的所有平台版本上跑了。
因为这是应用需要告知我们的信息,所以它需要在应用开发时指定,这里我们先命名它为 minSdkVersion
,对于 Android 应用,我们就在 AndroidManifest.xml
清单文件中指定:
//应用提供的信息
四、提供一个信息就足够了么
现在应用只告诉了平台它能支持的最低版本,那现在我们就需要想一想了,仅仅告知这个信息足够么?
在回答此问题之前,我们来升级下前面写的平台 API。之前的版本为 2,且添加了printNew()
方法,并打印了一个字符串。大家知道System.out.println()
这个是在Java 中常用的方法,但是在 Android 中,我们常用android.util.Log
工具类来打印某些文本。
但是由于之前我们确定了升级API的原则为新增更改,那么就意味着直接修改代码是绝对不行的,否则应用在新的平台上的行为会改变,这可不是我们想看到的。对于这个问题,我们应该怎么办呢?
既然不能直接修改,那么原来代码显然是要保留的,针对老平台编译的使用之前的打印方式,针对新平台那么我们就采用新的打印方式好了。那答案就出来了,我们可以在运行时判断。那么,版本号为 3 的平台 API 就出来了:
//版本号3 的 平台 API
public boolean doSomething() { /*do something ... */ return true;}
@Deprecated
public void print(){
System.out.println("hello version 1");
}
public void printNew() {
if(应用使用的API版本 <= 2) {
System.out.println("hello verson 2");
} else {
Log.d("tag", "hello version > 2");
}
}
通过这个代码我们就知道了,应用仅仅告诉我们它支持的最小 API 是不够的,我们还需要知道应用是基于哪个平台版本开发和测试的,在这里,如果应用是使用 2 版本,那么就用System.out.println
,如果用的是之后的版本开发的,那我们就用android.util.Log
来打印。这样就可以保证应用跑在任何设备上都是其想要的行为了。
于是,我们需要再定义一个应用针对哪个版本开发和测试的的属性,这里我们将其命名为targetSdkVersion
。这样,最终应用的清单文件为:
五、版本兼容设计完成
这样看来好像没有其他问题了。那么现在总结一下,我们自己的平台API版本控制有这么四点需要注意的:
- 版本号的确立(从1开始增加的整数);
- 版本升级的原则,与所有早期版本保持兼容;
- 应用需要告知支持的最小平台版本号;
- 应用需要告知针对哪个版本进行开发和测试;
如果我们自己构建 API,大概就是这些问题了。
那么接下来,我们就来看看 Android 官方是如何处理这些问题的。
Android 的版本兼容
依照我们前面设计的四个问题,我们来依照顺序来看 Android 官方是怎么处理的。
Android API 级别
API 级别是对 Android 平台版本提供的框架 API 修订版进行唯一标识的整数值。
Android 平台提供的框架 API 使用称为“API 级别”的整数标识符指定。每个 Android 平台版本恰好支持一个 API 级别,但隐含对所有早期 API 级别(低至 API 级别 1)的支持。Android 平台初始版本提供的是 API 级别 1,后续版本的 API 级别则依次增加。
可见,Android 官方的版本号设计也是与我们所设计的版本号类似,都是从 1 开始的整数,并依次增加。官方还给出了Android 平台版本所支持的 API 级别,这里就不贴了,想看的话可以点文末的链接或者去 Android 的官方网站看看。
Android API 级别的兼容性
Android 平台的每个后续版本均可包括其提供的 Android 应用框架 API 的更新。
框架 API 更新的设计用途是使新 API 与早期版本的 API 保持兼容。换言之,大多数 API 更改都是新增更改,并且会引入新功能或替代功能。在 API 的某些部分得到升级时,系统会弃用经替换的旧版部分,但不会将其移除,以便其仍可供现有应用使用。在极少数情况下,系统可能会修改或移除 API 的某些部分,但通常只有在为确保 API 稳健性以及应用或系统安全性时,才需要进行此类更改。所有其他来自早期修订版的 API 部分都将继续保留,不做任何修改。
这里能看出 Android 的版本升级与我们设计的一样,首先就是要保证与早期版本的 API 兼容。在继续讨论应用的兼容性前我们先聊两个概念:
应用向前兼容性
Android 应用一般向前兼容新版本的 Android 平台。
由于几乎所有对框架 API 的更改都是新增更改,所以使用 API 任何给定版本(其 API 级别所指定版本)开发的 Android 应用均向前兼容更新版本的 Android 平台以及更高 API 级别。应用应能在所有后期版本的 Android 平台上运行,除非在个别情况下,系统后来因某种原因将应用使用的某个 API 部分移除。
应用向后兼容性
Android 应用未必向后兼容比其编译时所用目标版本更旧的 Android 平台版本。
每个新版本的 Android 平台都可能包含新的框架 API,例如能够让应用使用新的平台功能或替换现有 API 部分的 API。在新平台上运行时,应用可以使用这些新 API;且如上所述,在更新版本的平台(API 级别所指定的平台)上运行时,应用也可使用这些新 API。反之,由于早期版本的平台未包含新 API,因此使用新 API 的应用无法在这些平台上运行。
作为应用开发者,通过上面的描述咱们可以简单理解为:一个应用如果能在当前的API级别上跑,那么就可以在以后的API上,但未必能在早期的API上跑。于是乎,为了让平台知道这个应用能不能再自己的这个版本上跑,应用就需要提供一些信息。这就是我们提出的第三和第四个问题了。
Android 应用选择平台版本和 API 级别
首先,我们上面分析过了,应用必须向外面告知minSdkVersion
和targetSdkVersion
。在Android 上,是这么描述这个两个属性的,以及maxSdkVersion
这个属性:
android:minSdkVersion
指定能够运行应用的最低 API 级别。默认值为“1”。
应用在 android:minSdkVersion 中声明 API 级别的主要原因是,告知 Android 系统,其正使用在指定 API 级别引入的 API。如果由于某种原因将应用安装在 API 级别较低的平台上,则它会在运行时试图访问不存在的 API 时发生崩溃。如果应用所需的最低 API 级别高于目标设备上平台版本的 API 级别,则系统不允许安装该应用,以防出现这种结果。
例如,android.appwidget 软件包是随 API 级别 3 引入的。如果应用使用该 API,则必须使用“3”一值声明 android:minSdkVersion 属性。随后,应用便可安装在 Android 1.5(API 级别 3)和 Android 1.6(API 级别 4)等平台上,但不能安装在 Android 1.1(API 级别 2)和 Android 1.0(API 级别 1)平台上。
android:targetSdkVersion
指定运行应用的目标 API 级别。在某些情况下,此属性允许应用使用在目标 API 级别中定义的清单元素或行为,而非仅限于使用针对最低 API 级别定义的元素或行为。
targetSdkVersion 属性不会阻止您的应用安装在高于指定值的平台版本上,但它很重要,因为它向系统指示您的应用是否应继承较新版本中的行为更改。如果您不将 targetSdkVersion 更新到最新版本,则系统会认为您的应用在最新版本上运行时需要一些向后兼容性行为。例如,在 Android 4.4 中的行为更改中,使用 AlarmManager API 创建的闹钟现在默认不精确,因此系统可以批量处理应用闹钟并节省系统电量,但如果您的目标 API 级别低于“19”,则系统会为您的应用保留之前的 API 行为。
这里通过一张图是最能说明这个属性是怎么用的了:
这是
AlarmManager
的构造和cancel()
方法。首先在构造方法中获取到应用指定的targetSdkVersion
并存放在mTargetSdkVersion
中。在cancel()
方法里,Android 会判断应用针对哪个 API 级别开发和测试的。可以看到应用针对新的API级别和老的级别,反应到平台上,其行为是不一样的。
我们在看 Android 的源码时,会经常发现这样的代码,使用方法也类似,从这些代码中就能够看出targetSdkVersion
的作用了。
android:maxSdkVersion
指定能够运行应用的最高 API 级别。其值必须大于或等于系统的 API 级别整数。如果未声明,则系统假定应用没有最高 API 级别。
不建议声明该属性。首先,您没有必要设置该属性,并将其作为阻止您的应用部署至新版本 Android 平台的一种手段。从设计上讲,新版本平台完全向后兼容。只要您的应用仅使用标准 API 并遵循部署最佳实践,其应能够在新版本平台上正常工作。其次,请注意在某些情况下,声明该属性可能会导致您的应用在系统更新至更高 API 级别后从用户设备中移除。大多数可能安装您应用的设备都会定期收到 OTA 系统更新,因此您应在设置该属性前考虑这些更新对应用的影响。
总结一下就是不要声明该属性,甚至你可以忘掉这个属性的存在。
compileSdkVersion
至于这个声明,其实不用太在意,它只是我们在查看源代码和编译时才发挥作用的,它与应用兼容性关系不大。它指定了 Gradle 用哪个版本的 API 级别来编译你的应用,这样你在代码里就能够使用这个 API 级别提供的方法和功能。
一般来说我会把这个属性设置为与targetSdkVersion
相同,这样在点击查看某个源码时,查看的就是要针对的 API 级别对应的源代码。不过只要compileSdkVersion
不低于targetSdkVersion
就行了,否则 Android Studio 会有这样的警告:
另外,如果你需要查看某个版本的 Android 源码,那你也可以更改这个值。例如,你更改
compileSdkVersion
为28,那从代码点进去查看到的Android源码就是来源于28的;更改为30,那点进去查看的就是30的源码。
现在我们掌握了这个几个属性的作用了吧。作为开发者,理解这几个属性并选取对应的 API 级别是比较重要的。下面就来一下总结。
总结
这篇文章里,我们先自己设想了一下如果自己设计 Android 的版本兼容会是怎么样,并设计解决了发现的4个问题。然后再进入到 Android 官方的设计思维中,并看到 Google 的大佬们是怎么解决这些问题的。并顺便理解了应用版本声明的几个属性 minSdkVersion
、targetSdkVersion
、maxSdkVersion
、compileSdkVersion
。
关于 Android 的版本兼容,我想,基本理解到这里也就可以了,至少作为应用开发者,我们知道了怎么选minSdkVersion
、targetSdkVersion
版本号以及它们背后的意义了。
Android 兼容性-平台版本.
Android API 级别 use-sdk.