1. 程式人生 > >Android修煉之Pie 適配的搬運工

Android修煉之Pie 適配的搬運工

自嘲時刻

Android P正式版(以下稱為Pie)已經正式上線了,各大廠商已經開始了系統升級工作,咱做上層開發的也得跟上節奏。當然了,新版本所有的行為更改內容都可以在官網上找到,對於其中如何繞開非SDK介面限制的問題,也有各路大神給出瞭解決方案。所以,我只能當搬運工了(偷笑ing)。下面我會結合現有的開發經驗,聊聊Pie中對我們開發影響較大的一些更新,重頭戲還是適配齊劉海非SDK介面限制

行為變更

這裡只會列出部分行為變更(僅代表個人見解),感興趣的同學直接去官網檢視。

通知渠道設定更新

更新內容

  • 遮蔽渠道組:現在,使用者可以針對某個應用在通知設定中遮蔽整個渠道組。 您可以使用 isBlocked() 函式確定何時遮蔽一個渠道組,從而不會向該組中的渠道傳送任何通知。
  • 全新的廣播 Intent 型別:現在,當通知渠道和渠道組的遮蔽狀態發生變更時,Android 系統將傳送廣播 Intent。 擁有已遮蔽的渠道或渠道組的應用可以偵聽這些 Intent 並做出相應的迴應。

影響:APP的推送功能可能需要適配,Pie中可以知道渠道組的狀態了。

ImageDecoder & AnimatedImageDrawable

更新內容:

  • ImageDecoder 類,可提供現代化的影象解碼方法。 使用該類取代 BitmapFactory 和 BitmapFactory.Options API。ImageDecoder 讓您可通過位元組緩衝區、檔案或 URI 來建立 Drawable 或 Bitmap。
  • AnimatedImageDrawable 類,用於繪製和顯示 GIF 和 WebP 動畫影象。 AnimatedImageDrawable 的工作方式與 AnimatedVectorDrawable 的相似之處在於,都是渲染執行緒驅動 AnimatedImageDrawable 的動畫。

影響:對APP級別的影響應該較小,畢竟大部分都是使用第三方庫來實現圖片資源的載入。可能對SDK影響較大,SDK裡基本都是自己寫Image壓縮解碼邏輯,所以可以考慮使用。

統一生物識別身份驗證對話方塊

更新內容: 在 Android 9 中,系統代表您的應用提供生物識別身份驗證對話方塊。 該功能可建立標準化的對話方塊外觀、風格和位置,讓使用者更加確信,他們在使用可信的生物識別憑據檢查程式進行身份驗證。

影響:接入了生物識別功能的(如:指紋)APP就有統一的提示對話方塊了,挺好的。就是不知道標準的風格是否適合各種應用。

螢幕旋轉

更新內容:

  • 為避免無意的旋轉,我們新增了一種模式,哪怕裝置位置發生變化,也會固定在當前螢幕方向上。 必要時使用者可以通過按系統欄上的一個按鈕手動觸發旋轉。
  • 從 Android 9 開始,對縱向旋轉模式做出了重大變更。縱向模式已重新命名為旋轉鎖定,它會在自動螢幕旋轉關閉時啟用。當裝置處於旋轉鎖定模式時,使用者可將其螢幕鎖定到頂層可見 Activity 所支援的任何旋轉。 Activity 不應假定它將始終以縱向呈現。

影響:使用者可以在關閉自動旋轉的情況下,手動旋轉螢幕;能否旋轉取決於頂層可見Activity所支援的旋轉設定。

文字

更新內容:

  • 文字預先計算:PrecomputedText 類使您能提前計算和快取所需資訊,改善了文字渲染效能。
  • 放大器:Magnifier 類是一種可提供放大器API的微件,可在所有應用中實現一致的放大器功能體驗。
  • Smart Linkify:Android 9增強了TextClassifier類,該類可利用機器學習在選定文字中識別一些實體並建議採取相應的操作。
  • 文字佈局:藉助幾種便捷函式和屬性,可以更輕鬆地實現介面設計。

影響:這個無疑強化了TextView的能力,應該比現在設定TextVie的樣式方便。

電源管理

更新內容:

  • 新增了一個應用待機群組的概念,系統將根據使用者的使用模式限制應用對 CPU 或電池等裝置資源的訪問。 五個群組如下:
  1. 活躍 如果使用者當前正在使用應用,應用將被歸到“活躍”群組中。 如果應用處於“活躍”群組,系統不會對應用的作業、報警或 FCM 訊息施加任何限制。
  2. 工作集 如果應用經常執行,但當前未處於活躍狀態,它將被歸到“工作集”群組中。 如果應用處於“工作集”群組,系統會對它執行作業和觸發報警的能力施加輕度限制。
  3. 常用 如果應用會定期使用,但不是每天都必須使用,它將被歸到“常用”群組中。 如果應用處於“常用”群組,系統將對它執行作業和觸發報警的能力施加較強的限制,也會對高優先順序 FCM 訊息的數量設定限制。
  4. 極少使用 如果應用不經常使用,那麼它屬於“極少使用”群組。 如果應用處於“極少使用”群組,系統將對它執行作業、觸發警報和接收高優先順序FCM訊息的能力施加嚴格限制。系統還會限制應用連線到網路的能力。
  5. 從未使用 安裝但是從未執行過的應用會被歸到“從未使用”群組中。 系統會對這些應用施加極強的限制。

影響:無疑,Google在幫Android機省電、省資源,算是一個比較好的更新。但是,我相信廠商會對分組策略稍作修改(或者白名單啥的),以方便自身應用可以隨時喚起。總的來說,應該對應用的Push類功能影響較大。

許可權變更

更新內容:

  • 構建序列號棄用:在 Android 9 中,Build.SERIAL 始終設定為 “UNKNOWN” 以保護使用者的隱私。 如果您的應用需要訪問裝置的硬體序列號,您應改為請求 READ_PHONE_STATE 許可權,然後呼叫 getSerial()。
  • 前臺服務:針對 Android 9 或更高版本並使用前臺服務的應用必須請求 FOREGROUND_SERVICE 許可權。
  • 後臺對感測器的訪問受限:Android 9限制後臺應用訪問使用者輸入和感測器資料的能力。
  • 限制訪問通話記錄:Android 9 引入 CALL_LOG 許可權組並將 READ_CALL_LOG、WRITE_CALL_LOG 和 PROCESS_OUTGOING_CALLS 許可權移入該組。
  • 限制訪問電話號碼:在未首先獲得 READ_CALL_LOG 許可權的情況下,除了應用的用例需要的其他許可權之外,運行於 Android 9 上的應用無法讀取電話號碼或手機狀態。
  • 限制訪問 Wi-Fi 位置和連線資訊:在 Android 9 中,應用進行 Wi-Fi 掃描的許可權要求比之前的版本更嚴格。具體限制見官網。

影響:許可權變更對收集手機應用資訊和監聽應用WiFi狀態(主要是位置資訊)都有很大影響,特別是對這些資訊有依賴的業務。

Apache Http被棄用

更新內容: 從 Android 9 開始,預設情況下Apache網路庫已從 bootclasspath 中移除且不可用於應用。

影響:終於要徹底拋棄Apache庫了,但業務硬要用的話,還是可以自己匯入jar包或者在AndroidManifest.xml中使用uses-library標籤。

適配齊劉海

有Pixel的同學可以把系統升級到Pie檢視效果,也可以下載相應映象用模擬器測試(最好使用AS3.1以上)。Pie提供了三種劉海樣式:

  1. 邊角劉海(我相信絕對沒廠商用…)

邊角劉海

  1. 正常劉海(被iPhone X帶出來的風格…)

正常劉海

  1. 劉海+下巴(為啥要有下巴…)

劉海

下巴

在Pie中,官方提供了DisplayCutout類來獲取劉海塊相關資料;通過getDisplayCutout()函式可以知道劉海是否存在;通過窗口布局屬性layoutInDisplayCutoutMode可以讓應用為裝置螢幕缺口周圍的內容進行佈局。直接上程式碼吧:

    @TargetApi(28)
    public void showDisplayCutout(View view) {
        // 注意:getRootWindowInsets()只有在檢視載入完成後,才會返回,否則返回null
        DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
        if (cutout == null) { // 沒有劉海的時候拿到的DisplayCutout為null
            Log.d(TAG, "showDisplayCutout: 普通屏,沒有劉海");
            return;
        }
        List<Rect> rects = cutout.getBoundingRects(); // 獲取可能的凹凸塊
        if (rects.size() == 1) {
            Log.d(TAG, "showDisplayCutout: 有劉海");
            Rect rect = rects.get(0);
            Log.d(TAG, "showDisplayCutout: 劉海區域:" + rect);
        } else {
            Log.d(TAG, "showDisplayCutout: 有劉海和下巴");
            for (Rect rect : rects) {
                Log.d(TAG, "showDisplayCutout: 區域:" + rect);
            }
        }

        // 獲取安全區域的資料,單位px
        int safeInsetLeft = cutout.getSafeInsetLeft();
        int safeInsetTop = cutout.getSafeInsetTop();
        int safeInsetRight = cutout.getSafeInsetRight();
        int safeInsetBottom = cutout.getSafeInsetBottom();
        Log.d(TAG, "showDisplayCutout: 安全區域距離螢幕左邊:" + safeInsetLeft);
        Log.d(TAG, "showDisplayCutout: 安全區域距離螢幕頂部:" + safeInsetTop);
        Log.d(TAG, "showDisplayCutout: 安全區域距離螢幕右邊:" + safeInsetRight);
        Log.d(TAG, "showDisplayCutout: 安全區域距離螢幕底部:" + safeInsetBottom);

        int statusBarHeight = getStatusBarHeight();
        Log.d(TAG, "showDisplayCutout: 狀態列高度:" + statusBarHeight);
    }

    private int getStatusBarHeight() {
        int result = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

注意

  • getRootWindowInsets()只有在View載入完後才會返回值,否則返回null。所以,如果你想在生命週期(onCreate等)裡直接呼叫的話,可能會出現空指標異常,可以使用post runnable的方式處理。
  • 劉海的高度不一定和狀態列的高度一樣,應該是小於登入狀態列高度。

設定視窗屬性:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        // 設定全屏
        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);

        //沉浸式狀態列
//        getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

        WindowManager.LayoutParams lp = getWindow().getAttributes();
        // 只有當DisplayCutout完全包含在系統欄中時,才允許視窗延伸到DisplayCutout區域。 否則,窗口布局不與DisplayCutout區域重疊。
        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
        // 該視窗決不允許與DisplayCutout區域重疊。
//        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
        // 該視窗始終允許延伸到螢幕短邊上的DisplayCutout區域。
//        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
        getWindow().setAttributes(lp);
    }

部分效果圖:

  1. 普通頁面帶狀態列 頁面帶狀態列

  2. 全屏頁面預設情況 全屏頁面預設情況

  3. 全屏頁面強制使用劉海區域 全屏強制使用劉海區域

  4. 沉浸式 沉浸式頁面

適配方案

  • 對於有狀態列的頁面,不會受到劉海的影響,內容會展示在狀態列下方。
  • 對於全屏頁面,預設情況下系統會把展示內容下移到劉海之下,會產生一個黑條,此時需要注意頁面底部的內容,要避免因為下移導致的顯示不全問題;如果修改了視窗配置為LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,則會強制使用劉海區域,這時頁面要規避劉海塊的那一點區域。
  • 對於沉浸式頁面,是受到影響最大的,預設情況下就會使用劉海區域,所以頁面內容要進行區域規避。

非SDK介面限制

Android 9(API 級別 28)引入了針對非 SDK 介面的使用限制,無論是直接使用還是通過反射或 JNI 間接使用。無論應用是引用非 SDK 介面還是嘗試使用反射或 JNI 獲取其控制代碼,均適用這些限制。 如果我們使用非SDK介面,會出現幾種情況:

  1. logcat日誌 類似 Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI) 格式;
  2. 彈警告Toast
  3. 引發系統錯誤

非SDK介面都記錄在了不同的名單下:

  1. 白名單:SDK
  2. 淺灰名單:仍可以訪問的非 SDK 函式/欄位。
  3. 深灰名單: 對於目標 SDK 低於 API 級別 28 的應用,允許使用深灰名單介面。 對於目標 SDK 為 API 28 或更高級別的應用:行為與黑名單相同。
  4. 黑名單:受限,無論目標 SDK 如何,平臺將表現為似乎介面並不存在。

限制原理分析

  1. 編譯過程中有一個hiddenapi過程,會根據名單檔案,對dex中所有函式和欄位進行重新標記;
  2. runtime的時候,Art虛擬機器會根據函式和欄位的標記,並結合呼叫者的身份返回特定值;這些值就決定了呼叫者是否能成功呼叫介面。

階段一

階段二

如何繞過

還是大佬們的文章:

其中替換classloader的方式目前是最可靠的。

總結

參考資料:

  1. 其他資料已經在文中列出,不再重複;