Android 各版本兼容性適配詳解
Android 6
本文根據(jù)我個人的開發(fā)經(jīng)驗,總結(jié)了從 Android 6 - Android 13 重要的行為變更。當(dāng)然,這不是 Android 所有的行為變更,這里只是列舉了我覺得比較有影響的,比較常見的一些場景的開發(fā)適配。那么,下面讓我們一起來瞧瞧,有哪些行為變更是需要我們特別注意的。
在 Android 6 版本開始引進(jìn)運(yùn)行時權(quán)限機(jī)制,Android 將所有的權(quán)限歸為兩類,一類是普通權(quán)限,一類是危險權(quán)限。普通權(quán)限一般不會威脅到用戶的安全和隱私,對于這部分權(quán)限,系統(tǒng)自動對軟件進(jìn)行授權(quán),不需要詢問用戶。而危險權(quán)限是可能對用戶的安全和隱私造成影響的權(quán)限,如獲取設(shè)備地理位置、獲取設(shè)備聯(lián)系人信息等,這些就需要明確通知用戶,并由用戶手動進(jìn)行授權(quán)才可以進(jìn)行相應(yīng)操作。
危險權(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(存儲) | READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE |
運(yùn)行時權(quán)限動態(tài)申請,這里推薦郭神的開源庫 - PermissionX,使用簡單方便,這里不再贅述。
Android 7
Android 7 禁止向你的應(yīng)用外公開 file://URI, 如果在 Android 7 及以上系統(tǒng)傳遞 file:// URI 就會觸發(fā) FileUriExposedException,不適配的話在 Android 7 及以上系統(tǒng)就會出現(xiàn)應(yīng)用崩潰的現(xiàn)象。如果要在應(yīng)用間共享文件,可以發(fā)送 content://URI 類型的 URI,并授予 URI 臨時訪問權(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)部存儲,對應(yīng) filesDir,路徑:/data/data/package_name/files--> <files-path name="files_path" path="." /> <!-- 內(nèi)部存儲,對應(yīng) cacheDir,路徑:/data/data/package_name/cache--> <cache-path name="cache_path" path="." /> <!--外部存儲,對應(yīng) getExternalFilesDir,路徑:/storage/sdcard/Android/data/package_name/files--> <external-files-path name="external_files_path" path="." /> <!--外部存儲,對應(yīng) externalCacheDir,路徑:/storage/sdcard/Android/data/package_name/cache--> <external-cache-path name="external_cache_path" path="." /> </paths>
在 AndroidManifest 中注冊 FileProvider
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.example.myapplication.fileProvider" android:exported="false" android:grantUriPermissions="true"> <!--exported 要為 false,否則會報安全異常,grantUriPermissions 為 true,表示授予 URI 臨時訪問權(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) { // 通過 FileProvider 創(chuàng)建一個 content 類型的 Uri val pictureUri = FileProvider.getUriForFile( this, "com.example.myapplication.fileProvider", pictureFile ) intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri) // 授予目錄臨時共享權(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 開始,Google 規(guī)定所有的通知必須分配一個渠道,每一個渠道,你都可以設(shè)置渠道中所有通知的行為。用戶界面將通知渠道稱之為通知類別,用戶可以隨意修改這些設(shè)置來決定通知的行為。
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 開始,不允許后臺應(yīng)用啟動后臺服務(wù),需要使用 startForegroundService 指定為前臺服務(wù),否則系統(tǒng)會停止 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 開始,限制了 HTTP 網(wǎng)絡(luò)請求,如果繼續(xù)使用 HTTP 請求,會在日志做出警告,不過只是無法正常發(fā)出請求,不會導(dǎo)致應(yīng)用崩潰。如果我們需要使用 HTTP 請求的話,需要在 AndroidManifest 中添加如下配置:
<application ... android:usesCleartextTraffic="true"> ... </application>
除了這個方法,我們也可以指定域名。在 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 之前的版本,我們在做文件的操作時都會申請存儲空間的讀寫權(quán)限,但是這些權(quán)限可能被濫用,造成手機(jī)的存儲空間中充斥著大量不明作用的文件,并且應(yīng)用卸載后也沒刪除掉。為了解決這個問題,Android 10 開始引入了分區(qū)存儲的概念。
分區(qū)存儲就是對外部存儲進(jìn)行了重新設(shè)計,簡單來說,對于外部共享文件,需要通過 MediaStrore API 和 Storage Access Framework 來訪問,對于外部私有文件,無法讀寫自己應(yīng)用以外創(chuàng)建的其他文件。
Android 中存儲可以分為兩大類:專屬存儲和共享存儲。
- 專屬存儲:每個應(yīng)用在都擁有自己的專屬目錄,其它應(yīng)用看不到。它包括 APP 自身的內(nèi)部存儲和外部存儲,這倆無需存儲權(quán)限便可訪問。
- 共享存儲:共享存儲空間存放的是圖片,視頻和音頻等文件,這些資源是公共的,所有 App 都能訪問它們。
舉個例子,如果想拿到共享存儲里的圖片路徑,該怎么做呢?
首先需要申請權(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() } }
通過 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() }
媒體文件可以通過 MediaStore 和 SAF 兩種方式訪問,但是非媒體文件只能通過 SAF 訪問,通過 SAF,用戶可以通過一個簡單的標(biāo)準(zhǔn)界面,以統(tǒng)一的方式瀏覽訪問文件。
這里以選擇 sdcard 目錄下的一個文本文件,對它進(jìn)行讀寫操作為例。
private lateinit var startActivity: ActivityResultLauncher<Intent>
在 Activity 中注冊結(jié)果返回,這里需要注意的是,別等到 Activity 的生命周期執(zhí)行到 onResume 了才注冊,會報錯的,建議最好在 onCreate 中進(jìn)行注冊,在這里拿到選擇的文件的 uri
startActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.data != null && it.resultCode == Activity.RESULT_OK) { readFileContent(it.data!!.data) } }
寫入內(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() } }
打開文件選擇器
private fun openSAF() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) //指定選擇文本類型的文件 intent.type = "text/plain" startActivity.launch(intent) }
選擇器的用戶界面是這樣的,那個 NewTextFile.txt 就是我們操作的文本文件。
由此可見,SAF 提供了文件選擇器,調(diào)用者只需指定要讀寫的文件類型,比如文本類型,圖片類型,視頻類型等,選擇器就會過濾出相應(yīng)文件以供選擇,使用簡單。
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 中添加對應(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)限的時候會有紅線提示,建議使用上面的這種方式。加入該權(quán)限的 APP,應(yīng)用市場能不能過審,就很難說了。
Android 11 還增加了單次授權(quán),就是請求與位置信息,麥克風(fēng)或攝像頭相關(guān)的權(quán)限時,系統(tǒng)會自動提供一個單次授權(quán)的選項,只供這一次權(quán)限獲取,選擇它的話,用戶下次再次打開 APP 的時候,系統(tǒng)會再次提示用戶請求權(quán)限,所以,需要我們每次使用的時候去判斷一下權(quán)限,沒有就去申請。
Android 12
Android 12 增加了系統(tǒng)默認(rèn)的 APP 啟動頁,這個啟動頁會使用 APP 定義的主題生成,這對我們的應(yīng)用影響還是比較大的,通常我們會用一個 Activity 作為啟動頁來顯示一些廣告推廣啥的,但是在 Android 12 上不適配的話,那用戶將會看到兩個閃屏。怎么去適配呢? Google 告訴我們,你可以選擇不管或者去掉 SplashActivity 并使用設(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> <!--啟動畫面圖標(biāo)動畫的時長,Google 建議不超過 1000 毫秒--> <item name="windowSplashScreenAnimationDuration">200</item> <!--必填項,SplashView 移除后使用此主題恢復(fù) Activity 樣式--> <item name="postSplashScreenTheme">@style/Theme.MyApplication</item> </style>
設(shè)置主題
<application ... android:theme="@style/Theme.App.Splash"> ... </application>
在啟動 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)行為。在以前的版本中,返回鍵會執(zhí)行 finish Activity,而從 Android 12 開始會將任務(wù)棧切換到后臺,也就是說在根 Activity 點擊返回鍵時,生命周期只會執(zhí)行到 onStop,不執(zhí)行 onDestroy,所以,用戶返回應(yīng)用時將執(zhí)行溫啟動。
Android 13
從 Android 13 開始,用戶可以通過抽屜式通知欄完成工作流,以停止具有持續(xù)前臺服務(wù)的應(yīng)用,如下圖所示,此功能稱為前臺服務(wù) (FGS) 任務(wù)管理器,應(yīng)用必須能夠處理這種由用戶發(fā)起的停止操作。
此外,Android 13 引入了運(yùn)行時通知權(quán)限:POST_NOTIFICATIONS, 如果拒絕這個權(quán)限的話,應(yīng)用將無法發(fā)送通知,此更改有助于用戶只關(guān)注自己認(rèn)為重要的通知,但是與媒體會話以及自行管理通話的應(yīng)用相關(guān)的通知不受此行為變更的影響。
以上就是Android 各版本兼容性適配詳解的詳細(xì)內(nèi)容,更多關(guān)于Android 版本兼容性適配的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android Button點擊事件的四種實現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Android Button點擊事件的四種實現(xiàn)方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07Android下拉刷新控件SwipeRefreshLayout源碼解析
這篇文章主要為大家詳細(xì)解析Android下拉刷新控件SwipeRefreshLayout源碼,感興趣的小伙伴們可以參考一下2016-07-07Android開發(fā)之圖形圖像與動畫(四)AnimationListener簡介
就像Button控件有監(jiān)聽器一樣,動畫效果也有監(jiān)聽器,只需要實現(xiàn)AnimationListener就可以實現(xiàn)對動畫效果的監(jiān)聽,感興趣的朋友可以了解下啊,希望本文對你有所幫助2013-01-01Android開發(fā)之使用SQLite存儲數(shù)據(jù)的方法分析
這篇文章主要介紹了Android開發(fā)之使用SQLite存儲數(shù)據(jù)的方法,結(jié)合實例形式分析了Android使用SQLite數(shù)據(jù)庫實現(xiàn)針對數(shù)據(jù)的增刪改查操作相關(guān)技巧,需要的朋友可以參考下2017-07-07老生常談ProgressBar、ProgessDialog的用法
下面小編就為大家?guī)硪黄仙U凱rogressBar、ProgessDialog的用法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07WindowManagerService服務(wù)是如何以堆棧的形式來組織窗口
我們知道,在Android系統(tǒng)中,Activity是以堆棧的形式組織在ActivityManagerService服務(wù)中的;在本文中,我們就詳細(xì)分析WindowManagerService服務(wù)是如何以堆棧的形式來組織窗口的2013-01-01