前言
Android官方的遷移適配文檔有點(diǎn)混亂,這篇文章旨在給開發(fā)者在適配中對(duì)代碼做快速檢查。適配變化將分為運(yùn)行版本影響和Target版本影響,并提供可能影響的功能以便測(cè)試參考。轉(zhuǎn)載請(qǐng)注明來源「Bug總柴」
Android Q (API level 29)
沙箱機(jī)制(scoped-storage)
在Android Q中變化比較大的是對(duì)外置sdcard的訪問權(quán)限變化,這個(gè)變化將會(huì)影響大部分需要訪問外置存儲(chǔ)的應(yīng)用。
沙箱機(jī)制解讀
- external storage在Android Q開始被設(shè)置成像internal storage那種只能訪問自己包名下的空間,無法直接訪問sdcard其他位置內(nèi)容。就算聲明了READ_EXTERNAL_STORAGE權(quán)限,在應(yīng)用中通過File.listFiles只能看到/storage/emulated/0/Android/data/<package> , /storage/emulated/0/Android/media/<package> , /storage/emulated/0/Android/obb/<package> 三個(gè)文件夾。
- READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE的通用訪問外置sdcard的權(quán)限被拆分為訪問音樂READ_MEDIA_AUDIO、照片READ_MEDIA_IMAGES和視頻READ_MEDIA_VIDEO三種權(quán)限,而訪問應(yīng)用沙箱的內(nèi)容無需額外申請(qǐng)權(quán)限。
沙箱生效時(shí)機(jī)
- 如果target版本小于等于28并且應(yīng)用是安裝在從Android 9升級(jí)到Andoid Q的手機(jī)上,則會(huì)啟用兼容模式,仍然可以隨意訪問external存儲(chǔ)的內(nèi)容。
- 意味著當(dāng)target版本大于28,或者應(yīng)用是在Android Q的手機(jī)上新安裝都會(huì)使沙箱機(jī)制生效。這里需要說明,不管是否target到28以上,只要是在Android Q上新安裝的應(yīng)用都會(huì)使沙箱機(jī)制生效。
- 對(duì)于模擬器里面的Andorid Q Beta 1版本,需要執(zhí)行adb shell sm set-isolated-storage on開啟沙箱機(jī)制
影響范圍
- 各種為了實(shí)現(xiàn)離線使用功能的離線下載文件
- 各種緩存文件(例如信息流緩存、廣告緩存等)
- 需要注意某些三方庫(kù)可能會(huì)使用外置sdcard(例如log或者crash統(tǒng)計(jì)等)
四、處理辦法
- 對(duì)于圖片視頻音樂和下載文件可以通過MediaStore類訪問,或者使用Storage Access Framework
- 對(duì)于之前存儲(chǔ)在外置sdcard的其他數(shù)據(jù),需要遷移存儲(chǔ)到getExternalFilesDir目錄中
- 對(duì)于新增的文件盡量保存在getExternalFilesDir和getExternalCacheDir
Api檢查
Context.getExternalFilesDir(null) -> /storage/emulated/0/Android/data/<package>/files
Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) -> /storage/emulated/0/Android/data/<package>/files/Pictures
Context.externalCacheDir -> /storage/emulated/0/Android/data/<package>/cache
Context.obbDir -> /storage/emulated/0/Android/obb/<package>
Environment.getExternalStorageDirectory() -> /storage/emulated/0 (沙箱機(jī)制下無法訪問)
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) -> /storage/emulated/0/Pictures (沙箱機(jī)制下無法訪問)
Android 9 (API level 28)
非SDK接口使用限制
使用 veridex工具測(cè)試apk是否有調(diào)用非SDK接口
? veridex-mac ./appcompat.sh --dex-file=test.apk
實(shí)例結(jié)果如下:
6889 hidden API(s) used: 6817 linked against, 72 through reflection
0 in blacklistgetConnectionInfo
3 in dark greylist
47 in light greylist
To run an analysis that can give more reflection accesses,
but could include false positives, pass the --imprecise flag.
其中:
| 類型 | 描述 |
|---|---|
| blacklist | 不管是否target到28,都會(huì)報(bào)NoSuchMethodError/NoSuchFieldException
|
| dark greylist | 如果target在28一下沒問題,但是target到28及以上會(huì)報(bào)NoSuchMethodError/NoSuchFieldException
|
| light greylist | 暫時(shí)沒有問題,可以使用 |
處理辦法:
去除blacklist以及dark greylist的非android sdk調(diào)用的反射調(diào)用,有些是android support包內(nèi)部調(diào)用的可以考慮升級(jí)support包版本
隱私&權(quán)限相關(guān)
| 運(yùn)行在9.0受到影響 | 可能受到影響的功能 |
|---|---|
| 不能在后臺(tái)訪問麥克風(fēng)和攝像頭 | 后臺(tái)錄音、后臺(tái)拍照 |
| 加速器陀螺儀等傳感器不能在后臺(tái)持續(xù)獲取數(shù)據(jù) | 步數(shù)計(jì)算 |
| 通過變化模式或者單次模式的傳感器收不到事件 | 顯著運(yùn)動(dòng)檢測(cè)、計(jì)步器、近程傳感器和心率傳感器 |
通話記錄權(quán)限組別由PHONE組調(diào)整到CALL_LOG組 |
需要獲通過記錄權(quán)限的功能 |
通過android.intent.action.PHONE_STATE或TelephonyManager.listen方法獲取手機(jī)號(hào)碼需要申請(qǐng)READ_CALL_LOG 權(quán)限 |
例如來電歸屬地顯示或者來電攔截等需要獲取通話手機(jī)號(hào)的功能 |
wifi掃描頻率限制更為嚴(yán)格,getConnectionInfo WifiManager.getScanResults()以及WifiManager.startScan()需要而外權(quán)限詳見
|
需要wifi掃描匹配等功能 |
WifiManager.getConnectionInfo() 要獲得SSID和BSSID,要求定位權(quán)限并要求設(shè)備打開定位功能,NETWORK_STATE_CHANGED_ACTION不再能獲得SSID和BSSID |
需要獲取wifi信息的功能 |
WifiManager與WifiP2pManager中getScanResults() getConnectionInfo()和discoverServices() addServiceRequest()和NETWORK_STATE_CHANGED_ACTION不再包含用戶定位信息 |
使用wifi定位功能 |
TelephonyManager中getAllCellInfo() listen() getCellLocation() getNeighboringCellInfo()不返回結(jié)果,除非用戶打開了定位功能 |
使用移動(dòng)信號(hào)定位 |
| Target在9.0受到影響 | 可能受到影響的功能 |
|---|---|
啟動(dòng)前臺(tái)服務(wù)要去注冊(cè)android.permission.FOREGROUND_SERVICE權(quán)限 |
前臺(tái)服務(wù)啟動(dòng) |
獲取序列號(hào)不能通過Build.SERIAL,需要注冊(cè)android.permission.READ_PHONE_STATE然后使用Build.getSerial()
|
獲取序列號(hào)相關(guān)功能 |
安全相關(guān)
| 運(yùn)行在9.0受到影響 | 可能受到影響的功能 |
|---|---|
SSLSocket出錯(cuò)不返回NullPointerException,改成返回IOException
|
https網(wǎng)絡(luò)錯(cuò)誤處理 |
加密函數(shù)Cipher.getInstance("AES/CBC/PKCS7PADDING", "BC") Cipher.getInstance("AES/CBC/PKCS7PADDING",Security.getProvider("BC")) SecureRandom.getInstance("SHA1PRNG", "Crypto");移除 |
加密功能 |
| Android secure encrypted files移除 | 移動(dòng)app到sdcard功能 |
| Target在9.0受到影響 | 可能受到影響的功能 |
|---|---|
| DNS客戶端需要根據(jù)系統(tǒng)使用加密DNS查找與系統(tǒng)相同的主機(jī)名,或改由系統(tǒng)解析程序 | DNS自解析功能 |
默認(rèn)要求使用https,如果需要使用http需要設(shè)置cleartextTrafficPermitted="true"詳見
|
所有http網(wǎng)絡(luò)請(qǐng)求 |
| webview的數(shù)據(jù)包括cookies和caches不允許多進(jìn)程共享 | 多進(jìn)程使用webview |
| 不用通過設(shè)置全局Unix權(quán)限共享數(shù)據(jù)文件,不用應(yīng)用的文件共享需要使用ContentProvider | 應(yīng)用間文件共享 |
國(guó)際化相關(guān)
| 運(yùn)行在9.0受到影響 | 可能受到影響的功能 |
|---|---|
java.text.SimpleDateFormat 使用zzzz格式、java.text.DateFormatSymbols.getZoneStrings()格式、NumberFormat.getInstance(ULocale, PLURALCURRENCYSTYLE).parse(String)格式修改 |
時(shí)區(qū)、貨幣顯示相關(guān)功能 |
網(wǎng)絡(luò)相關(guān)
| 運(yùn)行在9.0受到影響 | 可能受到影響的功能 |
|---|---|
| NetworkCapabilities支持返回NET_CAPABILITY_NOT_VPN | vpn設(shè)置功能 |
| Apache HTTP client不能使用system ClassLoader加載,若要使用需要實(shí)現(xiàn)自定義ClassLoader | 使用舊Apache Http client網(wǎng)絡(luò)功能 |
| Target在9.0受到影響 | 可能受到影響的功能 |
|---|---|
NetworkStatsManager能獲取非當(dāng)前正在使用的流量情況 |
網(wǎng)絡(luò)使用統(tǒng)計(jì) |
ConnectivityManager.getMultipathPreference() 可以獲取是否超過了移動(dòng)流量使用限制 |
網(wǎng)絡(luò)使用情況提醒 |
Apache Http背去除,要使用需要加上<uses-library android:name="org.apache.http.legacy" android:required="false"/>或者想apache.http相關(guān)類包通過jar方式引入 |
使用舊Apache Http client網(wǎng)絡(luò)功能 |
界面相關(guān)
| 運(yùn)行在9.0受到影響 | 可能受到影響的功能 |
|---|---|
通過非activity的context啟動(dòng)activity強(qiáng)制要求intent帶上FLAG_ACTIVITY_NEW_TASK
|
后臺(tái)啟動(dòng)頁(yè)面 |
| 屏幕旋轉(zhuǎn)方式由原來的“自動(dòng)旋轉(zhuǎn)”和“縱向”改為“自動(dòng)旋轉(zhuǎn)”和“固定旋轉(zhuǎn)” | 屏幕旋轉(zhuǎn)功能 |
| Target在9.0受到影響 | 可能受到影響的功能 |
|---|---|
| 長(zhǎng)或?qū)挒?的view不再可以獲取焦點(diǎn),新開頁(yè)面不默認(rèn)獲取焦點(diǎn) | 交互過程通過特殊焦點(diǎn)實(shí)現(xiàn)的功能 |
| webview可以支持帶透明度的8位顏色css | webview css 顏色透明度功能 |
| webview中document的root元素滾動(dòng)位置得到支持 | webview 相關(guān) |
| 暫停掛起app的通知會(huì)在app resumed之后重新通知 | 通知相關(guān) |
設(shè)備相關(guān)
| 運(yùn)行在9.0受到影響 | 可能受到影響的功能 |
|---|---|
多攝像頭支持getCameraIdList()前后攝像頭切換需要選擇合適的攝像頭 |
攝像頭相關(guān)功能 |
其他
| 運(yùn)行在9.0受到影響 | 可能受到影響的功能 |
|---|---|
| UTF-8解碼更加嚴(yán)格按照Unicode標(biāo)準(zhǔn)詳見 | UTF-8解碼相關(guān)的功能 |
實(shí)用參考地址
Android 8 (API level 26)
后臺(tái)限制
| 運(yùn)行在8.0受到影響 | 可能受到影響的功能 |
|---|---|
后臺(tái)應(yīng)用通過startService()方法啟動(dòng)服務(wù),包括 IntentService會(huì)受到限制并拋出IllegalStateException異常,需要改成使用 JobScheduler 或者JobIntentService
|
所有啟動(dòng)后臺(tái)服務(wù)的行為,包括但不限于后臺(tái)下載、后臺(tái)數(shù)據(jù)更新、后臺(tái)初始化等等 |
| 前臺(tái)服務(wù)啟動(dòng)不能通過啟動(dòng)后臺(tái)服務(wù)再將其轉(zhuǎn)換為前臺(tái), 需要通過startForegroundService()方法, 并在5s內(nèi)調(diào)用startForeground()方法顯示前臺(tái)通知,否則會(huì)ANR |
所有前臺(tái)服務(wù),包括音樂播放功能、其他有通知的服務(wù) |
| 自定義action廣播以及其他系統(tǒng)非指向性的廣播接收受到限制, 可通過manifests注冊(cè)指向性廣播或者通過 Context.registerReceiver()動(dòng)態(tài)注冊(cè),系統(tǒng)性的廣播事件可考慮通過 JobScheduler配置實(shí)現(xiàn) |
例如軟件安裝后的廣播處理以及網(wǎng)絡(luò)變化通知處理功能 |
| 后臺(tái)應(yīng)用獲取位置受到限制,包括FusedLocationProviderApi、 GnssMeasurement、GnssNavigationMessage、 WifiManager.startScan()、LocationManager,需要使用前臺(tái)服務(wù)保持應(yīng)用前臺(tái)狀態(tài) |
后臺(tái)動(dòng)作檢測(cè)功能、后臺(tái)需要用到地理位置的功能例如后臺(tái)導(dǎo)航之類 |
隱私&權(quán)限相關(guān)
| 運(yùn)行在8.0受到影響 | 可能受到影響的功能 |
|---|---|
| ANDROID_ID從之前的僅與設(shè)備相關(guān),改為與應(yīng)用簽名、設(shè)備、設(shè)備登錄用戶相關(guān)。 | 使用ANDROID_ID的功能 |
獲取系統(tǒng)屬性net.hostname將返回null |
wifi hostname獲取功能 |
| Target在8.0受到影響 | 可能受到影響的功能 |
|---|---|
系統(tǒng)屬性net.dns*不再支持 |
通過系統(tǒng)屬性獲取dns功能 |
需要獲取DNS信息需要ACCESS_NETWORK_STATE權(quán)限,通過NetworkRequest或者NetworkCallback獲取 |
DNS獲取功能 |
獲取序列號(hào)不能通過Build.SERIAL,需要注冊(cè)android.permission.READ_PHONE_STATE然后使用Build.getSerial()
|
獲取序列號(hào)相關(guān)功能 |
| LauncherApps獲取不同用戶的應(yīng)用信息時(shí),會(huì)當(dāng)做沒有任何應(yīng)用安裝,而不是拋出異常 | 桌面啟動(dòng)器相關(guān)功能 |
| 相同權(quán)限組的其他權(quán)限會(huì)在真正需要時(shí)才被自動(dòng)授予,之前是整個(gè)權(quán)限組同時(shí)授予 | 權(quán)限授予相關(guān) |
安全相關(guān)
| 運(yùn)行在8.0受到影響 | 可能受到影響的功能 |
|---|---|
| 不再支持SSLv3 | 使用SSLv3的地方 |
| 當(dāng)HTTPS使用錯(cuò)誤的TLS協(xié)議與服務(wù)交互時(shí),不再使用其他TLS協(xié)議重試 | HTTPS相關(guān) |
| 在bionic之外的系統(tǒng)調(diào)用將被禁止 | bionic系統(tǒng)調(diào)用 |
| WebView被運(yùn)行在多進(jìn)程空間 | WebView間數(shù)據(jù)共享 |
| APKs安裝路徑可能會(huì)被修改 | APKs管理 |
判斷是否能安裝應(yīng)用需使用PackageManager.canRequestPackageInstalls(),INSTALL_NON_MARKET_APPS失效 |
應(yīng)用安裝 |
| 8.0系統(tǒng)默認(rèn)禁止應(yīng)用安裝未知應(yīng)用 | 應(yīng)用安裝功能 |
Thread.UncaughtExceptionHandler 會(huì)記錄在stacktrace中,但不會(huì)殺死應(yīng)用 |
線程異常處理 |
| Target在8.0受到影響 | 可能受到影響的功能 |
|---|---|
registerContentObserver(Uri, boolean, ContentObserver)中的Uri必須使用ContentProvider注冊(cè) |
以Uri來通知變化的功能 |
network_security_config.xml 配置禁止明文傳輸將同樣影響WebView |
Https功能 |
AccountManager不能只通過申明GET_ACCOUNTS來獲取賬號(hào),需要調(diào)用AccountManager.newChooseAccountIntent()讓用戶選擇, 再通過AccountManager.getAccounts()來獲取 |
Account Services相關(guān) |
| native庫(kù)若包含可執(zhí)行文件則不會(huì)加載 | native庫(kù)相關(guān) |
| JNI調(diào)用會(huì)檢查反射的類或方法是否存在,否則會(huì)拋出異常 | JNI調(diào)用 |
DexFile API已經(jīng)過時(shí),建議使用系統(tǒng)默認(rèn)PathClassLoader 或者 BaseDexClassLoader。如果需要用到DexFile,不應(yīng)該進(jìn)行壓縮,否則會(huì)解壓消耗內(nèi)存。 多線程加載相同類由最先加載的類的加載器決定。 |
Dex 加載相關(guān) |
國(guó)際化相關(guān)
| 運(yùn)行在8.0受到影響 | 可能受到影響的功能 |
|---|---|
|
Currency.getDisplayName()、Currency.getSymbol()、 Locale.getDisplayScript() 默認(rèn)調(diào)用Locale.getDefault(Category.DISPLAY) |
國(guó)際化顯示 |
| Currency.getDisplayName(null)將會(huì)拋出異常 | 國(guó)際化單位顯示 |
對(duì)于SimpleDateFormat的時(shí)區(qū)獲取由原來在設(shè)備第一次啟動(dòng)時(shí)候獲取,改為每次實(shí)時(shí)獲取 |
時(shí)區(qū)顯示 |
| 升級(jí)ICU到58版本 | 國(guó)際化單位標(biāo)準(zhǔn) |
網(wǎng)絡(luò)相關(guān)
| 運(yùn)行在8.0受到影響 | 可能受到影響的功能 |
|---|---|
| 無正文的 OPTIONS 請(qǐng)求具有 Content-Length: 0 頭部 | options請(qǐng)求相關(guān) |
| HttpURLConnection會(huì)保證請(qǐng)求最后帶上“/” | HttpURLConnection |
ProxySelector.setDefault()設(shè)置的代理僅處理scheme/host/port,不會(huì)處理請(qǐng)求參數(shù) |
代理設(shè)置相關(guān)功能 |
| 不再支持空lable的URI | 使用URI相關(guān)功能 |
| HttpsURLConnection不會(huì)執(zhí)行不安全的TLS/SSL協(xié)議版本回退 | HttpsURLConnection |
| 隧道Https協(xié)議改變,具體見Networking and HTTP(S) connectivity | 隧道Https |
如果DatagramSocket.connect()返回錯(cuò)誤,DatagramSocket.send()也會(huì)返回錯(cuò)誤 |
socket相關(guān) |
InetAddress.isReachable() 會(huì)在會(huì)退到TCP Echo協(xié)議之前嘗試ICMP協(xié)議,若不可達(dá)會(huì)消耗更多時(shí)間 |
IP地址判斷是否可達(dá)等網(wǎng)絡(luò)功能 |
| 在支持設(shè)備上wifi連接當(dāng)有強(qiáng)度大且已經(jīng)保存的網(wǎng)絡(luò)時(shí)可以自動(dòng)切換 | 需保證網(wǎng)絡(luò)切換不會(huì)影響應(yīng)用功能 |
界面相關(guān)
| 運(yùn)行在8.0受到影響 | 可能受到影響的功能 |
|---|---|
|
TYPE_PHONE、TYPE_PRIORITY_PHONE、 TYPE_SYSTEM_ALERT、TYPE_SYSTEM_OVERLAY、 TYPE_SYSTEM_ERROR這些類型的窗口都會(huì)顯示在TYPE_APPLICATION_OVERLAY之下 |
懸浮球、快速查詞等需要彈窗彈窗的地方 |
| 使用鍵盤導(dǎo)航時(shí),獲取焦點(diǎn)的view將會(huì)加上ripple高亮, 如果不需要這種默認(rèn)的高亮, 需要設(shè)置 android:defaultFocusHighlightEnabled或者 setDefaultFocusHighlightEnabled(false)
|
鍵盤導(dǎo)航 |
| webview中WebSettings.getSaveFormData()返回false, WebSettings.setSaveFormData()沒有任何作用, WebViewDatabase.clearFormData()沒有任何作用, WebViewDatabase.hasFormData()返回false |
網(wǎng)頁(yè)相關(guān) |
| Target在8.0受到影響 | 可能受到影響的功能 |
|---|---|
|
TYPE_PHONE、TYPE_PRIORITY_PHONE、 TYPE_SYSTEM_ALERT、TYPE_SYSTEM_OVERLAY、 TYPE_SYSTEM_ERROR 不能用在alert window上,必須使用 TYPE_APPLICATION_OVERLAY |
懸浮球、快速查詞等需要彈窗彈窗的地方 |
| 可點(diǎn)擊的View默認(rèn)擁有可獲取焦點(diǎn)屬性 | View焦點(diǎn)顯示 |
| Notificaiton通知必須指定Notificaiton Channels,否則不會(huì)顯示通知,詳見notifications | 通知相關(guān) |
設(shè)備相關(guān)
| 運(yùn)行在8.0受到影響 | 可能受到影響的功能 |
|---|---|
| 藍(lán)牙ScanRecord.getBytes()返回長(zhǎng)度不受限制 | 藍(lán)牙相關(guān)功能 |
| Target在8.0受到影響 | 可能受到影響的功能 |
|---|---|
| 音頻獲取焦點(diǎn)時(shí)會(huì)自動(dòng)降低其他音頻音量,現(xiàn)在支持暫停而不是降低音量,詳見automatic ducking | 音頻播放相關(guān)功能 |
| 當(dāng)來電時(shí),自動(dòng)靜音音頻播放 | 音頻播放相關(guān)功能 |
| 需要使用AudioAttributes實(shí)現(xiàn)音頻回放功能,AudioTrack過期 | 音頻回放功能 |
| 音量按鍵事件會(huì)優(yōu)先給前臺(tái)activity,如果前臺(tái)activity不處理會(huì)給最近一次播放音頻的應(yīng)用 | 音量控制 |
其他
| 運(yùn)行在8.0受到影響 | 可能受到影響的功能 |
|---|---|
應(yīng)用快捷方式不能通過com.android.launcher.action.INSTALL_SHORTCUT創(chuàng)建,需要使用ShortcutManager,具體如何創(chuàng)建可以看這篇文章 |
快捷方式創(chuàng)建功能 |
| 無障礙功能中雙擊動(dòng)作轉(zhuǎn)換為點(diǎn)擊動(dòng)作、 能識(shí)別TextView中的ClickableSpan |
無障礙功能 |
findViewById() 返回類型由View改為<T extends View> T
|
覆蓋findViewById() 的地方需要相應(yīng)修改 |
| 從2019年1月7日起,將無法通過 LAST_TIME_CONTACTED /TIMES_CONTACTED /LAST_TIME_USED /TIMES_USED 獲取聯(lián)系人使用情況 |
聯(lián)系人聯(lián)系情況獲取功能 |
|
AbstractCollection.removeAll(java.util.Collection) /AbstractCollection.retainAll(java.util.Collection) 當(dāng)傳入?yún)?shù)為null時(shí)會(huì)報(bào) NullPointerException
|
集合操作 |
| Target在8.0受到影響 | 可能受到影響的功能 |
|---|---|
瀏覽器ua會(huì)包含OPR有可能導(dǎo)致判斷是否Opera瀏覽器失效 |
根據(jù)ua判斷瀏覽器 |
|
Collections.sort()改為在List.sort()基礎(chǔ)上實(shí)現(xiàn),之前是恰好相反。 如果在List.sort()中調(diào)用Collections.sort()會(huì)產(chǎn)生死循環(huán) |
集合排序 |
在遍歷的過程中進(jìn)行排序,現(xiàn)在使用無論使用List.sort()還是Collections.sort()都會(huì)報(bào)錯(cuò) |
集合排序 |