前言
眾所周知,Android 適用于眾多類(lèi)型的設(shè)備,從手機(jī)到平板電腦和電視都能搭載使用。為了能在所有這些設(shè)備上順利運(yùn)行,Android 系統(tǒng)在應(yīng)用到設(shè)備上時(shí),必不可少的需要處理與 Android 應(yīng)用的兼容性問(wèn)題。這里就牽扯出兩個(gè)概念:設(shè)備兼容性與應(yīng)用兼容性。
- 設(shè)備兼容性:設(shè)備能夠正常運(yùn)行我們編寫(xiě)的 Android 應(yīng)用。
- 應(yīng)用兼容性:針對(duì)市面上千奇百怪的 Android 設(shè)備,應(yīng)用是否兼容每一種可能的設(shè)備配置。
對(duì)于Android 應(yīng)用開(kāi)發(fā)者來(lái)說(shuō) ,我們無(wú)需擔(dān)心設(shè)備是否兼容 Android,而是更加關(guān)注于我們開(kāi)發(fā)的應(yīng)用能夠在盡可能多的 Android 設(shè)備上正常運(yùn)行,即,上面所說(shuō)的應(yīng)用兼容性。
而一個(gè)應(yīng)用的兼容性所覆蓋的內(nèi)容較多,從設(shè)備功能到平臺(tái)版本再到屏幕配置,以及針對(duì)不同的國(guó)家或語(yǔ)言做出的修改,每一部分能涉及相當(dāng)多的內(nèi)容。
這篇文章就先介紹最常見(jiàn)的版本兼容性,順便幫助大家理解 Android 開(kāi)發(fā)中常見(jiàn)的幾個(gè)版本相關(guān)的屬性:minSdkVersion、targetSdkVersion、maxSdkVersion、compileSdkVersion。
自己設(shè)計(jì)版本兼容
在說(shuō)明 Android 的應(yīng)用兼容性之前,我們先做這么一個(gè)假設(shè):如果我們自己是 Android API 的開(kāi)發(fā)者,為了讓更多的 Android 應(yīng)用能夠跑在我們的系統(tǒng)上,我們應(yīng)該處理版本兼容問(wèn)題
一、版本號(hào)如何確立
這里我們簡(jiǎn)單地把 Android 框架 API 想象為一個(gè)給其他開(kāi)發(fā)者使用的庫(kù)。如果我們開(kāi)發(fā)了一個(gè)庫(kù),讓別的開(kāi)發(fā)者拿去用,那么第一個(gè)問(wèn)題就來(lái)了,那就是版本號(hào)的問(wèn)題。在幾乎所有情況下,我們每發(fā)一個(gè)版本,都會(huì)用一個(gè)依次增長(zhǎng)的整數(shù)來(lái)表明這個(gè)庫(kù)是什么版本。這里我們也從1開(kāi)始來(lái)發(fā)一個(gè)版本:
// 版本號(hào) 1 的 平臺(tái) API
public boolean doSomething() { /*do something ... */ return true;}
public void print(){
System.out.println("hello version 1");
}
好了,那么這就是我們發(fā)的版本號(hào)為1的庫(kù)了。里面包含了兩個(gè)方法,一個(gè)返回庫(kù)的版本號(hào),一個(gè)僅僅做了打印的操作。
二、升級(jí)能直接修改代碼嗎
隨著時(shí)間的推移,我們現(xiàn)在需要升級(jí)一下這個(gè)庫(kù)。之前已經(jīng)確定了版本號(hào)為一個(gè)不斷增加的整數(shù),之前發(fā)的版本號(hào)為1,那我們現(xiàn)在需要更新版本號(hào)為2的庫(kù)了。
那么問(wèn)題就來(lái)了,我們現(xiàn)在發(fā)現(xiàn)之前庫(kù)中print()這個(gè)方法不太好,或者說(shuō)打印的字符串不太對(duì),那我們要怎么修改。這就牽扯出以下三個(gè)問(wèn)題了:
- 能不能刪除之前版本中的方法
- 能不能直接修改之前版本中的方法實(shí)現(xiàn)
- 能不能直接修改方法名
很遺憾的說(shuō),上面的三個(gè)問(wèn)題的答案都是否定的。如果直接刪除print()方法,那么外部使用之前版本的應(yīng)用一旦升級(jí),就會(huì)因?yàn)檎也坏椒椒ǘ罎ⅲ煌?,修改方法名也是一樣的;至于直接修改方法中的?shí)現(xiàn)呢,那造成的后果可能會(huì)更加嚴(yán)重。這里如果我們更改了打印的字符串,外部引用會(huì)明顯的發(fā)現(xiàn)這里不對(duì),跟上個(gè)版本不一樣;如果你說(shuō)一個(gè)字符串還好,那我可以舉一個(gè)極端的例子,原本這個(gè)方法是打開(kāi)攝像頭的,但是下一個(gè)版本你改成閃光燈了,這樣外部引用此庫(kù)的應(yīng)用升級(jí)庫(kù)版本之后是完全無(wú)法工作的。
這里就需要確認(rèn)一個(gè)升級(jí)庫(kù)的約定:向后兼容,進(jìn)行新增更改!
雖然老的方法不能刪除,那么我們可以增加新的方法啊,并且為了標(biāo)識(shí)老的方法不再被維護(hù),可以添加@Deprecated注解。于是下面就是我們的版本號(hào)為2的庫(kù):
//版本號(hào)2 的 平臺(tái) 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");
}
這里在這個(gè)版本號(hào)為2的庫(kù)中,增加了一個(gè)printNew()方法,并保留了版本1中的所有內(nèi)容,只不過(guò)將print()方法標(biāo)記為廢棄了。
在開(kāi)發(fā)一個(gè)依賴庫(kù)的時(shí)候,我們需要只要考慮到這兩方面的問(wèn)題就基本可以解決版本兼容問(wèn)題。但 Android API 版本并不是一個(gè)依賴庫(kù),它還需要被安裝在各個(gè)設(shè)備上,因此我們還需要往下討論幾個(gè)問(wèn)題。
三、應(yīng)用需要告知什么信息
在繼續(xù)討論之前,我們先把依賴庫(kù)的設(shè)想放下,而是將我們開(kāi)發(fā)的東西想象為 Android API,它需要被安裝在各種設(shè)備之上。由于我們剛發(fā)了兩個(gè)版本,那么現(xiàn)在市面上就會(huì)有版本號(hào)為1和版本號(hào)為2的設(shè)備,而未來(lái)會(huì)有更多的版本號(hào)。
現(xiàn)在有這么一個(gè)應(yīng)用,它是依賴版本號(hào)為 2 的 API 版本開(kāi)發(fā)的,而且在這個(gè)應(yīng)用中使用到了 printNew()方法。那這問(wèn)題就來(lái)了,這個(gè)應(yīng)用能安裝在市面上所有的設(shè)備上么?顯然不能:假如它被安裝在一個(gè) API 版本號(hào)為 1 設(shè)備上,而這個(gè)設(shè)備上是沒(méi)有printNew()方法的,這樣的話,應(yīng)用就會(huì)因?yàn)檎也坏椒椒ǘ罎ⅰ?/p>
因此,一個(gè)應(yīng)用,在被安裝到設(shè)備上時(shí),必須能夠告知設(shè)備一些信息。在這里,必須告知設(shè)備的信息就是應(yīng)用在開(kāi)發(fā)時(shí)是基于哪一個(gè) API 版本進(jìn)行開(kāi)發(fā)的。但是在第二條里,我們確定了平臺(tái) API 的開(kāi)發(fā)必須是新增更改,這就意味著一個(gè)應(yīng)用如果是基于某一個(gè)平臺(tái)版本開(kāi)發(fā)的,那么在這個(gè)平臺(tái)版本后續(xù)的版本上也能夠完全支持這個(gè)應(yīng)用。在 Android 開(kāi)發(fā)里就是:Android 應(yīng)用一般向前兼容新版本的 Android 平臺(tái),這個(gè)我們后面再說(shuō)。
于是乎我們需要知道并不是這個(gè)應(yīng)用是基于哪個(gè) API 版本進(jìn)行開(kāi)發(fā)的,而是它最低能跑在哪個(gè) API 版本之上。在這里,由于它使用了在API版本號(hào)為1的平臺(tái)中沒(méi)有的printNew()方法,因此這個(gè)應(yīng)用只能指定為 2 了。而且由于保證了上面的平臺(tái) API 升級(jí)的約定,它既然能在版本號(hào) 2 上跑,那么它也就能夠在 3、4、5... 以及后續(xù)的所有平臺(tái)版本上跑了。
因?yàn)檫@是應(yīng)用需要告知我們的信息,所以它需要在應(yīng)用開(kāi)發(fā)時(shí)指定,這里我們先命名它為 minSdkVersion,對(duì)于 Android 應(yīng)用,我們就在 AndroidManifest.xml清單文件中指定:
//應(yīng)用提供的信息
<uses-sdk android:minSdkVersion="2" />
四、提供一個(gè)信息就足夠了么
現(xiàn)在應(yīng)用只告訴了平臺(tái)它能支持的最低版本,那現(xiàn)在我們就需要想一想了,僅僅告知這個(gè)信息足夠么?
在回答此問(wèn)題之前,我們來(lái)升級(jí)下前面寫(xiě)的平臺(tái) API。之前的版本為 2,且添加了printNew()方法,并打印了一個(gè)字符串。大家知道System.out.println()這個(gè)是在Java 中常用的方法,但是在 Android 中,我們常用android.util.Log工具類(lèi)來(lái)打印某些文本。
但是由于之前我們確定了升級(jí)API的原則為新增更改,那么就意味著直接修改代碼是絕對(duì)不行的,否則應(yīng)用在新的平臺(tái)上的行為會(huì)改變,這可不是我們想看到的。對(duì)于這個(gè)問(wèn)題,我們應(yīng)該怎么辦呢?
既然不能直接修改,那么原來(lái)代碼顯然是要保留的,針對(duì)老平臺(tái)編譯的使用之前的打印方式,針對(duì)新平臺(tái)那么我們就采用新的打印方式好了。那答案就出來(lái)了,我們可以在運(yùn)行時(shí)判斷。那么,版本號(hào)為 3 的平臺(tái) API 就出來(lái)了:
//版本號(hào)3 的 平臺(tái) API
public boolean doSomething() { /*do something ... */ return true;}
@Deprecated
public void print(){
System.out.println("hello version 1");
}
public void printNew() {
if(應(yīng)用使用的API版本 <= 2) {
System.out.println("hello verson 2");
} else {
Log.d("tag", "hello version > 2");
}
}
通過(guò)這個(gè)代碼我們就知道了,應(yīng)用僅僅告訴我們它支持的最小 API 是不夠的,我們還需要知道應(yīng)用是基于哪個(gè)平臺(tái)版本開(kāi)發(fā)和測(cè)試的,在這里,如果應(yīng)用是使用 2 版本,那么就用System.out.println,如果用的是之后的版本開(kāi)發(fā)的,那我們就用android.util.Log來(lái)打印。這樣就可以保證應(yīng)用跑在任何設(shè)備上都是其想要的行為了。
于是,我們需要再定義一個(gè)應(yīng)用針對(duì)哪個(gè)版本開(kāi)發(fā)和測(cè)試的的屬性,這里我們將其命名為targetSdkVersion。這樣,最終應(yīng)用的清單文件為:
<uses-sdk android:minSdkVersion="2"
android:targetSdkVersion="3" />
五、版本兼容設(shè)計(jì)完成
這樣看來(lái)好像沒(méi)有其他問(wèn)題了。那么現(xiàn)在總結(jié)一下,我們自己的平臺(tái)API版本控制有這么四點(diǎn)需要注意的:
- 版本號(hào)的確立(從1開(kāi)始增加的整數(shù));
- 版本升級(jí)的原則,與所有早期版本保持兼容;
- 應(yīng)用需要告知支持的最小平臺(tái)版本號(hào);
- 應(yīng)用需要告知針對(duì)哪個(gè)版本進(jìn)行開(kāi)發(fā)和測(cè)試;
如果我們自己構(gòu)建 API,大概就是這些問(wèn)題了。
那么接下來(lái),我們就來(lái)看看 Android 官方是如何處理這些問(wèn)題的。
Android 的版本兼容
依照我們前面設(shè)計(jì)的四個(gè)問(wèn)題,我們來(lái)依照順序來(lái)看 Android 官方是怎么處理的。
Android API 級(jí)別
API 級(jí)別是對(duì) Android 平臺(tái)版本提供的框架 API 修訂版進(jìn)行唯一標(biāo)識(shí)的整數(shù)值。
Android 平臺(tái)提供的框架 API 使用稱為“API 級(jí)別”的整數(shù)標(biāo)識(shí)符指定。每個(gè) Android 平臺(tái)版本恰好支持一個(gè) API 級(jí)別,但隱含對(duì)所有早期 API 級(jí)別(低至 API 級(jí)別 1)的支持。Android 平臺(tái)初始版本提供的是 API 級(jí)別 1,后續(xù)版本的 API 級(jí)別則依次增加。
可見(jiàn),Android 官方的版本號(hào)設(shè)計(jì)也是與我們所設(shè)計(jì)的版本號(hào)類(lèi)似,都是從 1 開(kāi)始的整數(shù),并依次增加。官方還給出了Android 平臺(tái)版本所支持的 API 級(jí)別,這里就不貼了,想看的話可以點(diǎn)文末的鏈接或者去 Android 的官方網(wǎng)站看看。
Android API 級(jí)別的兼容性
Android 平臺(tái)的每個(gè)后續(xù)版本均可包括其提供的 Android 應(yīng)用框架 API 的更新。
框架 API 更新的設(shè)計(jì)用途是使新 API 與早期版本的 API 保持兼容。換言之,大多數(shù) API 更改都是新增更改,并且會(huì)引入新功能或替代功能。在 API 的某些部分得到升級(jí)時(shí),系統(tǒng)會(huì)棄用經(jīng)替換的舊版部分,但不會(huì)將其移除,以便其仍可供現(xiàn)有應(yīng)用使用。在極少數(shù)情況下,系統(tǒng)可能會(huì)修改或移除 API 的某些部分,但通常只有在為確保 API 穩(wěn)健性以及應(yīng)用或系統(tǒng)安全性時(shí),才需要進(jìn)行此類(lèi)更改。所有其他來(lái)自早期修訂版的 API 部分都將繼續(xù)保留,不做任何修改。
這里能看出 Android 的版本升級(jí)與我們?cè)O(shè)計(jì)的一樣,首先就是要保證與早期版本的 API 兼容。在繼續(xù)討論應(yīng)用的兼容性前我們先聊兩個(gè)概念:
應(yīng)用向前兼容性
Android 應(yīng)用一般向前兼容新版本的 Android 平臺(tái)。
由于幾乎所有對(duì)框架 API 的更改都是新增更改,所以使用 API 任何給定版本(其 API 級(jí)別所指定版本)開(kāi)發(fā)的 Android 應(yīng)用均向前兼容更新版本的 Android 平臺(tái)以及更高 API 級(jí)別。應(yīng)用應(yīng)能在所有后期版本的 Android 平臺(tái)上運(yùn)行,除非在個(gè)別情況下,系統(tǒng)后來(lái)因某種原因?qū)?yīng)用使用的某個(gè) API 部分移除。
應(yīng)用向后兼容性
Android 應(yīng)用未必向后兼容比其編譯時(shí)所用目標(biāo)版本更舊的 Android 平臺(tái)版本。
每個(gè)新版本的 Android 平臺(tái)都可能包含新的框架 API,例如能夠讓?xiě)?yīng)用使用新的平臺(tái)功能或替換現(xiàn)有 API 部分的 API。在新平臺(tái)上運(yùn)行時(shí),應(yīng)用可以使用這些新 API;且如上所述,在更新版本的平臺(tái)(API 級(jí)別所指定的平臺(tái))上運(yùn)行時(shí),應(yīng)用也可使用這些新 API。反之,由于早期版本的平臺(tái)未包含新 API,因此使用新 API 的應(yīng)用無(wú)法在這些平臺(tái)上運(yùn)行。
作為應(yīng)用開(kāi)發(fā)者,通過(guò)上面的描述咱們可以簡(jiǎn)單理解為:一個(gè)應(yīng)用如果能在當(dāng)前的API級(jí)別上跑,那么就可以在以后的API上,但未必能在早期的API上跑。于是乎,為了讓平臺(tái)知道這個(gè)應(yīng)用能不能再自己的這個(gè)版本上跑,應(yīng)用就需要提供一些信息。這就是我們提出的第三和第四個(gè)問(wèn)題了。
Android 應(yīng)用選擇平臺(tái)版本和 API 級(jí)別
首先,我們上面分析過(guò)了,應(yīng)用必須向外面告知minSdkVersion和targetSdkVersion。在Android 上,是這么描述這個(gè)兩個(gè)屬性的,以及maxSdkVersion這個(gè)屬性:
android:minSdkVersion
指定能夠運(yùn)行應(yīng)用的最低 API 級(jí)別。默認(rèn)值為“1”。
應(yīng)用在 android:minSdkVersion 中聲明 API 級(jí)別的主要原因是,告知 Android 系統(tǒng),其正使用在指定 API 級(jí)別引入的 API。如果由于某種原因?qū)?yīng)用安裝在 API 級(jí)別較低的平臺(tái)上,則它會(huì)在運(yùn)行時(shí)試圖訪問(wèn)不存在的 API 時(shí)發(fā)生崩潰。如果應(yīng)用所需的最低 API 級(jí)別高于目標(biāo)設(shè)備上平臺(tái)版本的 API 級(jí)別,則系統(tǒng)不允許安裝該應(yīng)用,以防出現(xiàn)這種結(jié)果。
例如,android.appwidget 軟件包是隨 API 級(jí)別 3 引入的。如果應(yīng)用使用該 API,則必須使用“3”一值聲明 android:minSdkVersion 屬性。隨后,應(yīng)用便可安裝在 Android 1.5(API 級(jí)別 3)和 Android 1.6(API 級(jí)別 4)等平臺(tái)上,但不能安裝在 Android 1.1(API 級(jí)別 2)和 Android 1.0(API 級(jí)別 1)平臺(tái)上。
android:targetSdkVersion
指定運(yùn)行應(yīng)用的目標(biāo) API 級(jí)別。在某些情況下,此屬性允許應(yīng)用使用在目標(biāo) API 級(jí)別中定義的清單元素或行為,而非僅限于使用針對(duì)最低 API 級(jí)別定義的元素或行為。
targetSdkVersion 屬性不會(huì)阻止您的應(yīng)用安裝在高于指定值的平臺(tái)版本上,但它很重要,因?yàn)樗蛳到y(tǒng)指示您的應(yīng)用是否應(yīng)繼承較新版本中的行為更改。如果您不將 targetSdkVersion 更新到最新版本,則系統(tǒng)會(huì)認(rèn)為您的應(yīng)用在最新版本上運(yùn)行時(shí)需要一些向后兼容性行為。例如,在 Android 4.4 中的行為更改中,使用 AlarmManager API 創(chuàng)建的鬧鐘現(xiàn)在默認(rèn)不精確,因此系統(tǒng)可以批量處理應(yīng)用鬧鐘并節(jié)省系統(tǒng)電量,但如果您的目標(biāo) API 級(jí)別低于“19”,則系統(tǒng)會(huì)為您的應(yīng)用保留之前的 API 行為。
這里通過(guò)一張圖是最能說(shuō)明這個(gè)屬性是怎么用的了:
這是
AlarmManager的構(gòu)造和cancel()方法。首先在構(gòu)造方法中獲取到應(yīng)用指定的targetSdkVersion并存放在mTargetSdkVersion中。在cancel()方法里,Android 會(huì)判斷應(yīng)用針對(duì)哪個(gè) API 級(jí)別開(kāi)發(fā)和測(cè)試的??梢钥吹綉?yīng)用針對(duì)新的API級(jí)別和老的級(jí)別,反應(yīng)到平臺(tái)上,其行為是不一樣的。
我們?cè)诳?Android 的源碼時(shí),會(huì)經(jīng)常發(fā)現(xiàn)這樣的代碼,使用方法也類(lèi)似,從這些代碼中就能夠看出targetSdkVersion的作用了。
android:maxSdkVersion
指定能夠運(yùn)行應(yīng)用的最高 API 級(jí)別。其值必須大于或等于系統(tǒng)的 API 級(jí)別整數(shù)。如果未聲明,則系統(tǒng)假定應(yīng)用沒(méi)有最高 API 級(jí)別。
不建議聲明該屬性。首先,您沒(méi)有必要設(shè)置該屬性,并將其作為阻止您的應(yīng)用部署至新版本 Android 平臺(tái)的一種手段。從設(shè)計(jì)上講,新版本平臺(tái)完全向后兼容。只要您的應(yīng)用僅使用標(biāo)準(zhǔn) API 并遵循部署最佳實(shí)踐,其應(yīng)能夠在新版本平臺(tái)上正常工作。其次,請(qǐng)注意在某些情況下,聲明該屬性可能會(huì)導(dǎo)致您的應(yīng)用在系統(tǒng)更新至更高 API 級(jí)別后從用戶設(shè)備中移除。大多數(shù)可能安裝您應(yīng)用的設(shè)備都會(huì)定期收到 OTA 系統(tǒng)更新,因此您應(yīng)在設(shè)置該屬性前考慮這些更新對(duì)應(yīng)用的影響。
總結(jié)一下就是不要聲明該屬性,甚至你可以忘掉這個(gè)屬性的存在。
compileSdkVersion
至于這個(gè)聲明,其實(shí)不用太在意,它只是我們?cè)诓榭丛创a和編譯時(shí)才發(fā)揮作用的,它與應(yīng)用兼容性關(guān)系不大。它指定了 Gradle 用哪個(gè)版本的 API 級(jí)別來(lái)編譯你的應(yīng)用,這樣你在代碼里就能夠使用這個(gè) API 級(jí)別提供的方法和功能。
一般來(lái)說(shuō)我會(huì)把這個(gè)屬性設(shè)置為與targetSdkVersion相同,這樣在點(diǎn)擊查看某個(gè)源碼時(shí),查看的就是要針對(duì)的 API 級(jí)別對(duì)應(yīng)的源代碼。不過(guò)只要compileSdkVersion不低于targetSdkVersion就行了,否則 Android Studio 會(huì)有這樣的警告:
另外,如果你需要查看某個(gè)版本的 Android 源碼,那你也可以更改這個(gè)值。例如,你更改
compileSdkVersion為28,那從代碼點(diǎn)進(jìn)去查看到的Android源碼就是來(lái)源于28的;更改為30,那點(diǎn)進(jìn)去查看的就是30的源碼。
現(xiàn)在我們掌握了這個(gè)幾個(gè)屬性的作用了吧。作為開(kāi)發(fā)者,理解這幾個(gè)屬性并選取對(duì)應(yīng)的 API 級(jí)別是比較重要的。下面就來(lái)一下總結(jié)。
總結(jié)
這篇文章里,我們先自己設(shè)想了一下如果自己設(shè)計(jì) Android 的版本兼容會(huì)是怎么樣,并設(shè)計(jì)解決了發(fā)現(xiàn)的4個(gè)問(wèn)題。然后再進(jìn)入到 Android 官方的設(shè)計(jì)思維中,并看到 Google 的大佬們是怎么解決這些問(wèn)題的。并順便理解了應(yīng)用版本聲明的幾個(gè)屬性 minSdkVersion、targetSdkVersion、maxSdkVersion、compileSdkVersion。
關(guān)于 Android 的版本兼容,我想,基本理解到這里也就可以了,至少作為應(yīng)用開(kāi)發(fā)者,我們知道了怎么選minSdkVersion、targetSdkVersion版本號(hào)以及它們背后的意義了。