1、Android 9.0應用遷移
1.1 概述
在最開始針對速貸進行Android9.0版本的適配時,我使用的是真機是vivo X21A,將targetSdkVersion升到28,運行發現網絡請求全報400 Bad Request
,查閱了網上針對android9.0網絡請求問題的解決方案以及其他的一些遷移到Android9.0需注意的點(下文會講到)
,做了些適配和調整,發現仍有問題,通過抓包也並沒有發現問題的原因所在。隨後試了下模擬器以及借來的google pixel(9.0)真機運行了下,能正常使用,並沒有發現什麼問題,於是猜想是機子本身系統的問題。
- 11.01更新:莫名又正常運行了。
1.2 Android9的遷移和適配
在模擬器上的適配我這邊做的不多,官網有比較詳細的介紹: 將應用遷移到 Android 9
1.2.1 針對Android 9設備上運行的所有應用都有影響的關鍵變化有(列舉了常用的,具體看官方文檔):
- * 對非 SDK 接口的限制:現已禁止訪問特定的非 SDK 接口,無論是直接訪問,還是通過 JNI 或反射進行間接訪問。嘗試訪問受限制的接口時,會生成 NoSuchFieldException 和 NoSuchMethodException 之類的錯誤。
- 移除加密提供程序:從 Android 9 開始,Crypto JCA 提供程序已被移除。調用 SecureRandom.getInstance("SHA1PRNG", "Crypto") 將會引發 NoSuchProviderException。
- 更嚴格的
UTF-8
解碼器:在 Android 9 中,針對 Java 語言的 UTF-8 解碼器比以往更嚴格,並且遵循 Unicode 標準。 - 強制執行
FLAG_ACTIVITY_NEW_TASK
:在 Android 9 中,您不能從非 Activity 環境中啓動 Activity,除非您傳遞 Intent 標誌FLAG_ACTIVITY_NEW_TASK
。 如果您嘗試在不傳遞此標誌的情況下啓動 Activity,則該 Activity 不會啓動,系統會在日誌中輸出一則消息。
1.2.2 targetSdkVersion 設置爲 28 時影響應用的關鍵變化(列舉了常用的,具體看官方文檔):
- * 默認情況下啓用網絡傳輸層安全協議 (TLS):如果應用以 Android 9 或更高版本爲目標平臺,則默認情況下 isCleartextTrafficPermitted() 函數返回 false。 如果您的應用需要爲特定域名啓用明文,您必須在應用的網絡安全性配置中針對這些域名將 cleartextTrafficPermitted 顯式設置爲 true。
- * 前臺服務:針對 Android 9 或更高版本並使用前臺服務的應用必須請求 FOREGROUND_SERVICE 權限。 這是普通權限,因此,系統會自動爲請求權限的應用授予此權限。
- 按進程分設基於網絡的數據目錄:爲改善 Android 9 中的應用穩定性和數據完整性,應用無法再讓多個進程共用同一 WebView 數據目錄。 此類數據目錄一般存儲 Cookie、HTTP 緩存以及其他與網絡瀏覽有關的持久性和臨時性存儲。
- 構建序列號棄用:在 Android 9 中,Build.SERIAL 始終設置爲 "UNKNOWN" 以保護用戶的隱私。 如果您的應用需要訪問設備的硬件序列號,您應改爲請求 READ_PHONE_STATE 權限,然後調用 getSerial()。
其中星號*
標註的是我在項目中添加的修改部分。
1.3、適配詳解(重要部分)
1.3.1、non-sdk 接口限制
non-sdk即非 SDK 接口,它們是不屬於官方 Android SDK 的 Java 字段和函數,它們屬於實現詳情,不提倡被調用或者被禁止調用的,需要通過反射等其他手段來實現;而SDK接口是官方提供的,公開的標準接口,可以被我們調用。
我們可以通過查看日誌消息來得知調用的非SDK
接口屬於灰名單還是黑名單:
也可以使用命令掃描整個app裏面存在的非 SDK 接口:
1appcompat.sh --dex-file=apk路徑
如:
/Users/Clem/常用工具/runtime-master-appcompat/veridex-mac/appcompat.sh --dex-file=/Users/Clem/常用工具/runtime-master-appcompat/app-jianrongsudai-debug.apk
結果如圖:
- 白名單:即SDK
- 淺灰名單(72個):仍可以訪問的非 SDK 函數/字段
- 深灰名單(7個):對於目標 SDK 低於 API 級別 28 的應用,允許使用深灰名單接口; 對於目標 SDK 爲 API 28 或更高級別的應用:行爲與黑名單相同
- 黑名單(0個):受限,無論目標 SDK 如何,平臺將表現爲似乎接口並不存在
列入淺灰名單的非 SDK 接口包含可以在 Android 9 中繼續工作的函數和字段,但不能保證在未來版本的平臺中能夠繼續訪問,主要需要關注深灰名單和黑名單,需要找到可以替代的SDK接口進行適配。網上有人發現了繞過API檢查的方法,也有專門的庫允許在Android P上使用反射而沒有任何限制,如FreeReflection:
1//允許在Android P上使用反射而不受任何限制 2implementation 'me.weishu:free_reflection:1.2.0' 3 4//在App.java中加入即可: 5Reflection.unseal(this);
1.3.2、http網絡請求的問題
在我們項目中,使用OKHttp請求會出現如下異常:
1java.net.UnknownServiceException: CLEARTEXT communication ** not permitted by network security policy
也就是說,當API級別爲28及以上時,應用使用的如果是非加密的明文流量的http網絡請求,則會導致該應用無法進行網絡請求,https則不會受影響,網上提供了三種方案:
- APP改用https請求
- targetSdkVersion 降到27及以下
- 在 res 下新增一個 xml 目錄,然後創建一個名爲:network_security_config.xml 文件(名字可自定),大概意思就是允許開啓http請求,內容如下:
1<?xml version="1.0" encoding="utf-8"?> 2<network-security-config> 3 <base-config cleartextTrafficPermitted="true" /> 4</network-security-config>
然後在APP的AndroidManifest.xml文件下的application標籤增加以下屬性即可完成:
1<application 2... 3 android:networkSecurityConfig="@xml/network_security_config" 4... 5/>
1.4、後續問題
在完成上述的適配修改後,交給測試測了以後發現瞭如下問題:
- 1、綁定公積金,選擇城市時,頁面會崩潰
- 2、環境切換功能,點擊測試入口時,頁面會崩潰
- 3、需支持支付寶h5支付
- 4、偶現人臉識別不成功,反覆進入活體認證頁面
針對問題一和二:我測試了下Android9.0的模擬器以及google pixel(9.0)真機,發現並沒有這些問題,而在vivo X21A真機上面,當頁面崩潰時也沒有任何明確的錯誤日誌,只看到如圖的信息:
通過打斷點調試發現在vivo X21A真機上,無法使用Spinner和RadioButton控件(目前看到的就這兩個),一旦使用就會造成崩潰,這個比較頭疼,猜測是系統問題。
- 11.07更新:使用新機子華爲 Mate 20發現並沒有這個問題。
針對問題三:測試了一下只有targetSdkVersion
設置爲 28時纔會出現該問題,與設備的系統版本無關,上螞蟻金服平臺查了下最新文檔發現,App支付功能近期有了更新和升級,具體來說就是:打包方式更換爲 AAR,替代之前的 JAR 打包,SDK 支付接口部分不變(親測有效)。下載官方demo可以發現附帶的更新日誌文檔中也有記錄這些,如圖:
針對問題四,由於是偶現,且也沒有任何明確的報錯日誌,需要多個真機進行多次測試。
2、新特性介紹(功能及API)
2.1、顯示屏缺口支持:layoutInDisplayCutoutMode
Android P的真機設備或模擬器上都可以模擬屏幕缺口,提供了三種樣式。
- 11.07更新:使用新機子華爲 Mate 20發現開發者選項中沒有提供模擬屏幕缺口,且該機也自帶有凹口位置,狀態欄也一直處於劉海區域。下面提到的模式針對該機效果都不變。
API 28也提供了新的類: DisplayCutout
類,該類主要用於獲取凹口位置和安全區域的位置等。
主要接口如下:
方法 | 接口說明 |
---|---|
getBoundingRects() | 返回Rects的列表,每個Rects都是顯示屏上非功能區域的邊界矩形 |
getSafeInsetLeft () | 返回安全區域距離屏幕左邊的距離,單位是px。 |
getSafeInsetRight () | 返回安全區域距離屏幕右邊的距離,單位是px。 |
getSafeInsetTop () | 返回安全區域距離屏幕頂部的距離,單位是px。 |
getSafeInsetBottom() | 返回安全區域距離屏幕底部的距離,單位是px。 |
此外,API 28中還提供了新的佈局參數屬性 layoutInDisplayCutoutMode
,包含了三種不同模式:
模式 | 模式說明 |
---|---|
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | 只有當DisplayCutout完全包含在系統欄中時,才允許窗口延伸到DisplayCutout區域。 否則,窗口布局不與DisplayCutout區域重疊。 |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER | 該窗口決不允許與DisplayCutout區域重疊。 |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES | 該窗口始終允許延伸到屏幕短邊上的DisplayCutout區域。 |
注:在Android P之前,各大手機廠商針對劉海適配方案都不太一樣。
2.2、適用於可繪製對象和位圖: ImageDecoder
可以將PNG, JPEG, WEBP, GIF, or HEIF 格式的圖片的轉換成Drawable 或者Bitmap 對象的類,可不再使用BitmapFactory
和 BitmapFactory.Options
API。
1ImageDecoder.OnHeaderDecodedListener listener = new ImageDecoder.OnHeaderDecodedListener() { 2 @Override 3 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, ImageDecoder.Source source) { 4 //將解碼的圖像縮放到精確尺寸 5 decoder.setTargetSampleSize(2); 6 } 7 }; 8 ImageDecoder.Source source = ImageDecoder.createSource(getResources(), R.mipmap.ic_launcher); 9 //轉換成Drawable對象 10 Drawable drawable = ImageDecoder.decodeDrawable(source, listener); 11 //轉換成Bitmap對象 12 //Bitmap bitmap = ImageDecoder.decodeBitmap(source);
2.3、動畫:AnimatedImageDrawable
Android 9 引入了 AnimatedImageDrawable 類,用於繪製和顯示 GIF 和 WebP 動畫圖像。 AnimatedImageDrawable 的工作方式與 AnimatedVectorDrawable 的相似之處在於,都是渲染線程驅動 AnimatedImageDrawable 的動畫。 渲染線程還使用工作線程進行解碼,因此,解碼不會干擾渲染線程的其他操作。 這種實現機制允許您的應用在顯示動畫圖像時,無需管理其更新,也不會干擾應用界面線程上的其他事件。
1private void decodeImage() throws IOException { 2 Drawable decodedAnimation = ImageDecoder.decodeDrawable( 3 ImageDecoder.createSource(getResources(), R.drawable.my_drawable)); 4 5 if (decodedAnimation instanceof AnimatedImageDrawable) { 6 // 如果屬於動畫,則優先開啓動畫,展示第一幀 7 ((AnimatedImageDrawable) decodedAnimation).start(); 8 } 9}
2.4、Magnifier(放大鏡)
Android P引入了Magnifier來提升用戶選擇文字的體驗,它通過放大鏡將文字放大從而來幫助用戶準確定位想要選擇的文字:
1@RequiresApi(api = 28) 2 @Override 3 public boolean onTouch(View v, MotionEvent event) { 4 Magnifier magnifier = new Magnifier(v); 5 switch (event.getActionMasked()) { 6 case MotionEvent.ACTION_DOWN: 7 magnifier.show(event.getX(), event.getY()); 8 break; 9 case MotionEvent.ACTION_MOVE: 10 magnifier.show(event.getX(), event.getY()); 11 break; 12 case MotionEvent.ACTION_UP: 13 magnifier.dismiss(); 14 break; 15 default: 16 break; 17 } 18 return true; 19 }