Android 各版本兼容性適配詳解
Android 6
本文根據(jù)我個(gè)人的開(kāi)發(fā)經(jīng)驗(yàn),總結(jié)了從 Android 6 - Android 13 重要的行為變更。當(dāng)然,這不是 Android 所有的行為變更,這里只是列舉了我覺(jué)得比較有影響的,比較常見(jiàn)的一些場(chǎng)景的開(kāi)發(fā)適配。那么,下面讓我們一起來(lái)瞧瞧,有哪些行為變更是需要我們特別注意的。
在 Android 6 版本開(kāi)始引進(jìn)運(yùn)行時(shí)權(quán)限機(jī)制,Android 將所有的權(quán)限歸為兩類,一類是普通權(quán)限,一類是危險(xiǎn)權(quán)限。普通權(quán)限一般不會(huì)威脅到用戶的安全和隱私,對(duì)于這部分權(quán)限,系統(tǒng)自動(dòng)對(duì)軟件進(jìn)行授權(quán),不需要詢問(wèn)用戶。而危險(xiǎn)權(quán)限是可能對(duì)用戶的安全和隱私造成影響的權(quán)限,如獲取設(shè)備地理位置、獲取設(shè)備聯(lián)系人信息等,這些就需要明確通知用戶,并由用戶手動(dòng)進(jìn)行授權(quán)才可以進(jìn)行相應(yīng)操作。
危險(xiǎn)權(quán)限如下所示:
| 權(quán)限組名 | 權(quán)限名 |
|---|---|
| CALENDAR(日歷) | READ_CALENDAR,WRITE_CALENDAR |
| CAMERA(攝像頭) | CAMERA |
| CONTACTS(聯(lián)系人) | READ_CONTACTS,WRITE_CONTACTS,GET_ACCOUNTS |
| LOCATION(定位) | ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION |
| MICROPHONE(麥克風(fēng)) | RECORD_AUDIO |
| PHONE(手機(jī)) | READ_PHONE_STATE,CALL_PHONE,READ_CALL_LOG,WRITE_CALL_LOG,ADD_VOICEMAIL,USE_SIP,PROCESS_OUTGOING_CALLS |
| SENSOR(傳感器) | BODY_SENSORS |
| SMS(短信) | SEND_SMS,RECEIVE_SMS,READ_SMS,RECEIVE_WAP_PUSH,RECEIVE_MMS |
| STORAGE(存儲(chǔ)) | READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE |
運(yùn)行時(shí)權(quán)限動(dòng)態(tài)申請(qǐng),這里推薦郭神的開(kāi)源庫(kù) - PermissionX,使用簡(jiǎn)單方便,這里不再贅述。
Android 7
Android 7 禁止向你的應(yīng)用外公開(kāi) file://URI, 如果在 Android 7 及以上系統(tǒng)傳遞 file:// URI 就會(huì)觸發(fā) FileUriExposedException,不適配的話在 Android 7 及以上系統(tǒng)就會(huì)出現(xiàn)應(yīng)用崩潰的現(xiàn)象。如果要在應(yīng)用間共享文件,可以發(fā)送 content://URI 類型的 URI,并授予 URI 臨時(shí)訪問(wèn)權(quán)限,這就需要用到 FileProvider 類。
我們以調(diào)用系統(tǒng)相機(jī)拍照為例,在 res 下創(chuàng)建 xml 目錄,在此目錄下創(chuàng)建 file_paths.xml 文件。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!-- 內(nèi)部存儲(chǔ),對(duì)應(yīng) filesDir,路徑:/data/data/package_name/files-->
<files-path
name="files_path"
path="." />
<!-- 內(nèi)部存儲(chǔ),對(duì)應(yīng) cacheDir,路徑:/data/data/package_name/cache-->
<cache-path
name="cache_path"
path="." />
<!--外部存儲(chǔ),對(duì)應(yīng) getExternalFilesDir,路徑:/storage/sdcard/Android/data/package_name/files-->
<external-files-path
name="external_files_path"
path="." />
<!--外部存儲(chǔ),對(duì)應(yīng) externalCacheDir,路徑:/storage/sdcard/Android/data/package_name/cache-->
<external-cache-path
name="external_cache_path"
path="." />
</paths>
在 AndroidManifest 中注冊(cè) FileProvider
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.myapplication.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<!--exported 要為 false,否則會(huì)報(bào)安全異常,grantUriPermissions 為 true,表示授予 URI 臨時(shí)訪問(wèn)權(quán)限-->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
適配
val pictureFile =
File(getExternalFilesDir(null), "${System.currentTimeMillis()}.jpg")
if (!pictureFile.exists()) {
pictureFile.createNewFile()
}
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 通過(guò) FileProvider 創(chuàng)建一個(gè) content 類型的 Uri
val pictureUri =
FileProvider.getUriForFile(
this,
"com.example.myapplication.fileProvider",
pictureFile
)
intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri)
// 授予目錄臨時(shí)共享權(quán)限
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
startActivity(intent)
} else {
val pictureUri = Uri.fromFile(pictureFile)
intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri)
startActivity(intent)
}
Android 8
從 Android 8 開(kāi)始,Google 規(guī)定所有的通知必須分配一個(gè)渠道,每一個(gè)渠道,你都可以設(shè)置渠道中所有通知的行為。用戶界面將通知渠道稱之為通知類別,用戶可以隨意修改這些設(shè)置來(lái)決定通知的行為。
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
"Channel_ID",
"Channel_Name",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(notificationChannel)
val notification =
NotificationCompat.Builder(this, "Channel_ID")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("title").setContentText("content").build()
notificationManager.notify(1, notification)
} else {
val notification =
NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("title").setContentText("content").build()
notificationManager.notify(1, notification)
}
從 Android 8 開(kāi)始,不允許后臺(tái)應(yīng)用啟動(dòng)后臺(tái)服務(wù),需要使用 startForegroundService 指定為前臺(tái)服務(wù),否則系統(tǒng)會(huì)停止 Service 并拋出異常。
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
val intent = Intent(this, MyService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
class MyService : Service() {
override fun onBind(intent: Intent): IBinder? = null
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
"Channel_ID", "Channel_Name", NotificationManager.IMPORTANCE_DEFAULT
)
manager.createNotificationChannel(channel)
val notification = Notification.Builder(this, "Channel_ID").build()
startForeground(1, notification)
}
}
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true)
}
}
}
Android 9
從 Android 9 開(kāi)始,限制了 HTTP 網(wǎng)絡(luò)請(qǐng)求,如果繼續(xù)使用 HTTP 請(qǐng)求,會(huì)在日志做出警告,不過(guò)只是無(wú)法正常發(fā)出請(qǐng)求,不會(huì)導(dǎo)致應(yīng)用崩潰。如果我們需要使用 HTTP 請(qǐng)求的話,需要在 AndroidManifest 中添加如下配置:
<application
...
android:usesCleartextTraffic="true">
...
</application>
除了這個(gè)方法,我們也可以指定域名。在 res 的 xml 目錄下新建文件 network_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">www.wanandroid.com</domain>
</domain-config>
</network-security-config>
然后在 AndroidManifest 中配置即可
<application
...
android:networkSecurityConfig="@xml/network_config">
...
</application>
Android 10
在 Android 10 之前的版本,我們?cè)谧鑫募牟僮鲿r(shí)都會(huì)申請(qǐng)存儲(chǔ)空間的讀寫(xiě)權(quán)限,但是這些權(quán)限可能被濫用,造成手機(jī)的存儲(chǔ)空間中充斥著大量不明作用的文件,并且應(yīng)用卸載后也沒(méi)刪除掉。為了解決這個(gè)問(wèn)題,Android 10 開(kāi)始引入了分區(qū)存儲(chǔ)的概念。
分區(qū)存儲(chǔ)就是對(duì)外部存儲(chǔ)進(jìn)行了重新設(shè)計(jì),簡(jiǎn)單來(lái)說(shuō),對(duì)于外部共享文件,需要通過(guò) MediaStrore API 和 Storage Access Framework 來(lái)訪問(wèn),對(duì)于外部私有文件,無(wú)法讀寫(xiě)自己應(yīng)用以外創(chuàng)建的其他文件。
Android 中存儲(chǔ)可以分為兩大類:專屬存儲(chǔ)和共享存儲(chǔ)。
- 專屬存儲(chǔ):每個(gè)應(yīng)用在都擁有自己的專屬目錄,其它應(yīng)用看不到。它包括 APP 自身的內(nèi)部存儲(chǔ)和外部存儲(chǔ),這倆無(wú)需存儲(chǔ)權(quán)限便可訪問(wèn)。
- 共享存儲(chǔ):共享存儲(chǔ)空間存放的是圖片,視頻和音頻等文件,這些資源是公共的,所有 App 都能訪問(wèn)它們。
舉個(gè)例子,如果想拿到共享存儲(chǔ)里的圖片路徑,該怎么做呢?
首先需要申請(qǐng)權(quán)限,這里直接使用 PermissionX 。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
PermissionX.init(this)
.permissions(Manifest.permission.READ_EXTERNAL_STORAGE).request { allGranted, _, _ ->
if (allGranted) {
Log.i(tag, "All permissions have been agreed")
} else {
Toast.makeText(this, "Please agree to the permission", Toast.LENGTH_SHORT)
.show()
}
}
通過(guò) MediaStrore 查詢
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.let {
val indexPhotoPath = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
while (it.moveToNext()) {
Log.i(tag, "picture path: ${it.getString(indexPhotoPath)}")
}
it.close()
}
媒體文件可以通過(guò) MediaStore 和 SAF 兩種方式訪問(wèn),但是非媒體文件只能通過(guò) SAF 訪問(wèn),通過(guò) SAF,用戶可以通過(guò)一個(gè)簡(jiǎn)單的標(biāo)準(zhǔn)界面,以統(tǒng)一的方式瀏覽訪問(wèn)文件。
這里以選擇 sdcard 目錄下的一個(gè)文本文件,對(duì)它進(jìn)行讀寫(xiě)操作為例。
private lateinit var startActivity: ActivityResultLauncher<Intent>
在 Activity 中注冊(cè)結(jié)果返回,這里需要注意的是,別等到 Activity 的生命周期執(zhí)行到 onResume 了才注冊(cè),會(huì)報(bào)錯(cuò)的,建議最好在 onCreate 中進(jìn)行注冊(cè),在這里拿到選擇的文件的 uri
startActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.data != null && it.resultCode == Activity.RESULT_OK) {
readFileContent(it.data!!.data)
}
}
寫(xiě)入內(nèi)容
private fun writeForUri(uri: Uri?) {
if (uri == null) return
try {
val outputStream = contentResolver.openOutputStream(uri)
val content = "Hello Android"
outputStream?.write(content.toByteArray())
outputStream?.flush()
outputStream?.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
讀取內(nèi)容
private fun readFileContent(uri: Uri?) {
if (uri == null) return
try {
val inputStream = contentResolver.openInputStream(uri) ?: return
val readContent = ByteArray(1024)
var len: Int
do {
len = inputStream.read(readContent)
if (len != -1) { //打印出文件內(nèi)容
Log.d(tag, "File Content: ${String(readContent).substring(0, len)}")
}
} while (len != -1)
inputStream.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
打開(kāi)文件選擇器
private fun openSAF() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
//指定選擇文本類型的文件
intent.type = "text/plain"
startActivity.launch(intent)
}
選擇器的用戶界面是這樣的,那個(gè) NewTextFile.txt 就是我們操作的文本文件。

由此可見(jiàn),SAF 提供了文件選擇器,調(diào)用者只需指定要讀寫(xiě)的文件類型,比如文本類型,圖片類型,視頻類型等,選擇器就會(huì)過(guò)濾出相應(yīng)文件以供選擇,使用簡(jiǎn)單。
Android 11
在 Android 11 中,不能直接獲取其他應(yīng)用的信息了,比如,查詢應(yīng)用信息的代碼如下:
val appList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
for (app in appList) {
Log.i(tag, "packageName: ${app.packageName}")
}
這段代碼只能查詢到自己應(yīng)用和系統(tǒng)應(yīng)用的信息,如果想要查詢其他應(yīng)用的信息,需要在 AndroidManifest 中添加對(duì)應(yīng)的包名配置。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
...
<queries>
<package android:name="com.example.composeapp" />
</queries>
...
</manifest>
如果你就是要獲取所有應(yīng)用的信息,怎么辦呢?Android 11 也提供了 QUERY_ALL_PACKAGES 權(quán)限,在 AndroidManifest 中加入即可。但是,加入該權(quán)限的時(shí)候會(huì)有紅線提示,建議使用上面的這種方式。加入該權(quán)限的 APP,應(yīng)用市場(chǎng)能不能過(guò)審,就很難說(shuō)了。
Android 11 還增加了單次授權(quán),就是請(qǐng)求與位置信息,麥克風(fēng)或攝像頭相關(guān)的權(quán)限時(shí),系統(tǒng)會(huì)自動(dòng)提供一個(gè)單次授權(quán)的選項(xiàng),只供這一次權(quán)限獲取,選擇它的話,用戶下次再次打開(kāi) APP 的時(shí)候,系統(tǒng)會(huì)再次提示用戶請(qǐng)求權(quán)限,所以,需要我們每次使用的時(shí)候去判斷一下權(quán)限,沒(méi)有就去申請(qǐng)。

Android 12
Android 12 增加了系統(tǒng)默認(rèn)的 APP 啟動(dòng)頁(yè),這個(gè)啟動(dòng)頁(yè)會(huì)使用 APP 定義的主題生成,這對(duì)我們的應(yīng)用影響還是比較大的,通常我們會(huì)用一個(gè) Activity 作為啟動(dòng)頁(yè)來(lái)顯示一些廣告推廣啥的,但是在 Android 12 上不適配的話,那用戶將會(huì)看到兩個(gè)閃屏。怎么去適配呢? Google 告訴我們,你可以選擇不管或者去掉 SplashActivity 并使用設(shè)置主題的方式來(lái)兼容,下面來(lái)看看設(shè)置主題的方式如何去實(shí)現(xiàn)?
implementation 'androidx.core:core-splashscreen:1.0.0'
<style name="Theme.App.Splash" parent="Theme.SplashScreen">
<!--特定的單色填充背景-->
<item name="windowSplashScreenBackground">@color/white</item>
<!--起始窗口中心的圖標(biāo)-->
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<!--啟動(dòng)畫(huà)面圖標(biāo)動(dòng)畫(huà)的時(shí)長(zhǎng),Google 建議不超過(guò) 1000 毫秒-->
<item name="windowSplashScreenAnimationDuration">200</item>
<!--必填項(xiàng),SplashView 移除后使用此主題恢復(fù) Activity 樣式-->
<item name="postSplashScreenTheme">@style/Theme.MyApplication</item>
</style>
設(shè)置主題
<application
...
android:theme="@style/Theme.App.Splash">
...
</application>
在啟動(dòng) Activity 中調(diào)用 installSplashScreen,注意要在 super.onCreate 之前添加。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
另外,Android 12 修改了根 Activity 在返回鍵的默認(rèn)行為。在以前的版本中,返回鍵會(huì)執(zhí)行 finish Activity,而從 Android 12 開(kāi)始會(huì)將任務(wù)棧切換到后臺(tái),也就是說(shuō)在根 Activity 點(diǎn)擊返回鍵時(shí),生命周期只會(huì)執(zhí)行到 onStop,不執(zhí)行 onDestroy,所以,用戶返回應(yīng)用時(shí)將執(zhí)行溫啟動(dòng)。
Android 13
從 Android 13 開(kāi)始,用戶可以通過(guò)抽屜式通知欄完成工作流,以停止具有持續(xù)前臺(tái)服務(wù)的應(yīng)用,如下圖所示,此功能稱為前臺(tái)服務(wù) (FGS) 任務(wù)管理器,應(yīng)用必須能夠處理這種由用戶發(fā)起的停止操作。

此外,Android 13 引入了運(yùn)行時(shí)通知權(quán)限:POST_NOTIFICATIONS, 如果拒絕這個(gè)權(quán)限的話,應(yīng)用將無(wú)法發(fā)送通知,此更改有助于用戶只關(guān)注自己認(rèn)為重要的通知,但是與媒體會(huì)話以及自行管理通話的應(yīng)用相關(guān)的通知不受此行為變更的影響。
以上就是Android 各版本兼容性適配詳解的詳細(xì)內(nèi)容,更多關(guān)于Android 版本兼容性適配的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中Fragment的基本用法示例總結(jié)
Fragment是activity的界面中的一部分或一種行為,下面這篇文章主要給大家介紹了關(guān)于Android中Fragment的基本用法的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2018-05-05
Android 判斷SIM卡屬于哪個(gè)移動(dòng)運(yùn)營(yíng)商詳解及實(shí)例
這篇文章主要介紹了Android 判斷SIM卡屬于哪個(gè)移動(dòng)運(yùn)營(yíng)商詳解相關(guān)資料,并附實(shí)例代碼,具有一定參考價(jià)值,需要的朋友可以參考下2016-11-11
詳解Android壁紙服務(wù)的啟動(dòng)過(guò)程
你有設(shè)置過(guò)手機(jī)的壁紙嗎,你知道壁紙是什么樣的程序它是怎么在后臺(tái)運(yùn)行的嗎?這篇文章主要介紹了詳解Android系統(tǒng)壁紙服務(wù)的啟動(dòng)過(guò)程2021-08-08
Android?RecyclerView曝光采集的實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Android?RecyclerView曝光采集的實(shí)現(xiàn)方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
Android編程實(shí)現(xiàn)攝像頭臨摹效果的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)攝像頭臨摹效果的方法,涉及Android權(quán)限控制、布局及攝像頭功能調(diào)用等相關(guān)操作技巧,需要的朋友可以參考下2017-09-09
Android開(kāi)發(fā)解決字符對(duì)齊問(wèn)題方法
這篇文章主要為大家介紹了Android開(kāi)發(fā)解決字符對(duì)齊問(wèn)題方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
Android自定義雙向進(jìn)度條的實(shí)現(xiàn)代碼
本篇文章主要介紹了Android自定義雙向進(jìn)度條的實(shí)現(xiàn)代碼,非常具有實(shí)用的價(jià)值,有興趣的同學(xué)一起來(lái)了解一下2017-09-09

