前言
眾所周知,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
清單文件中指定:
//應用提供的信息
<uses-sdk android:minSdkVersion="2" />
四、提供一個信息就足夠了么
現在應用只告訴了平臺它能支持的最低版本,那現在我們就需要想一想了,僅僅告知這個信息足夠么?
在回答此問題之前,我們來升級下前面寫的平臺 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
。這樣,最終應用的清單文件為:
<uses-sdk android:minSdkVersion="2"
android:targetSdkVersion="3" />
五、版本兼容設計完成
這樣看來好像沒有其他問題了。那么現在總結一下,我們自己的平臺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
版本號以及它們背后的意義了。