Android Sophix熱修復集成

!!!! 阿里雲熱修復官網地址

移動熱修復(Mobile Hotfix)是阿里雲提供的全平臺App熱修復服務方案。產品基於阿里巴巴首創hotpatch技術,提供最細粒度熱修復能力,讓您無需等待實時修復應用線上問題。

主要解決問題

(1)產品已有功能,出現問題,無需發版,即可發補丁,實時修復。

(2)存量功能微調

HotFix總體來說最大的優勢在於:

  • 補丁即時生效,不需要應用重啓;
  • 補丁包同樣採用差量技術,生成的PATCH體積小;
  • 對應用無侵入,幾乎無性能損耗;
  • 兩行代碼,傻瓜式接入。

使用限制

  • (1)支持加固,但是如果app中用到了加固,切記需要在加固前打補丁包。因爲加固做了很多特殊處理,所以接入之後,儘量對加固的包,進行全面的測試。

  • (2)目前阿里雲的聚安全加固進行了兼容支持,經測試,愛加密、梆梆、360、樂固、娜迦加固下全版本均測試正常。

  • (3)混淆和加固:混淆和加固是不一樣的,加固可能包含了混淆,並且做了很多特殊處理。注:如出現未加固時修復正常,加固後修復時找不到方法或類的情況,查看未加固的包是否混淆,可先對項目進行常規混淆,混淆後的包能正常修復後再進行加固測試。

  • (4)當前僅支持Android平臺

  • (5)當前熱修復,支持存量功能錯誤問題的修復,或者存量功能類的調整,不支持增量新增功能。

  • (6)注意:平臺計費是基於一個計費接口(queryAndLoadNewPatch),不是發了補丁纔會有計費。您可以根據自身需要,針對該計費接口,加上開關。

  • (7)注意:平臺計費,接口查詢,是爲了防止惡意查詢,導致服務成本極速上升。給定了比較高的額度,是日均查詢次數:20次。每個賬號下,平均到每臺設備,一天免費查補丁詢20次。【這裏是平均數】,超出部分是:2元/萬次請求。

  • (8)支持的資源:res資源 和 asset資源。

  • (9)Sophix 默認支持補丁大小30M以內

創建項目

1.1 註冊登

阿里雲官網->登陸賬號->產品->企業應用->移動雲下面的熱修復,切換到當前頁面先去註冊登錄,登錄之後,如果是之前沒用過,會顯示立即開通,之前已經開通的話會直接顯示管理控制檯如下圖所示:

1.2 開通產品並創建熱修復項目

如果是第一次使用阿里雲產品會先開通使用產品,開通之後如下圖所示,點擊加號開通產品,產品裏面包含一系列,相當於一個產品集合,暫時只使用熱修復

第二步會產生一個json文件,是當前項目所有阿里雲產品的配置,如果只做熱修復,只需要關心熱修復的三個主要參數,如下圖所示的參數(appKey、hotfix.idSecret、hotfix.rsaSecret)。下面是配置信息,如果按照正常接入步驟可以按照文檔接入,本文主要採用兼容Android 9.0穩健方式接入,下面的json配置文件儘量不要按照普通接入方式複製在項目下。

json文件中的一部分,包括項目要用到的appKey、appSecret、rsaSecret
{
  "config": {
	"emas.appKey":"29486055",
	"emas.appSecret":"394a882eef83f5d3af3a14a1572cde86",
	"emas.packageName":"com.example.alihot",
	"hotfix.idSecret":"29486055-1",
	"hotfix.rsaSecret":"MIIEvQIBADANBgkqhkiG9w0BqT9ALSe+vg=",
	"httpdns.accountId":"193836",
	"httpdns.secretKey":"420909cd9d988e938dfc8fadd146fe37",
	"appmonitor.tlog.rsaSecret":"MIGfMA0GnROqZIrLhdbIsvLEgmzB"
}

項目接入SDK

本文主要接入Sophix穩健方式的,兼容Android9.0,因爲Android9.0對於部分熱修復方式做簡單修改,如果普通接入會出現奔潰或者修復不成功問題,下面是具體接入Android studio(3.0)方式,更多方式參考文檔

1、Project項目下的build.gradle文件,添加maven倉庫地址,添加如下配置:

repositories {
       maven {
           url "http://maven.aliyun.com/nexus/content/repositories/releases"
       }
    }

2、module級別build.gradle配置

implementation 'com.aliyun.ams:alicloud-android-hotfix:3.2.14'

注意,若SDK集成過程中出現UTDID衝突(一般是之前集成過支付寶),請參考:阿里雲-移動雲產品SDK UTDID衝突解決方案

如若倉庫訪問失敗, 那麼用本地依賴的方式進行依賴, SDK下載見“1.5 客戶端本地SDK及DEMO下載”節。

注意:使用android studio打包生成apk時,要關閉instant run。

3、權限配置

<! -- 網絡權限 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <! -- 外部存儲讀權限,調試工具加載本地補丁需要 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

READ_EXTERNAL_STORAGE權限屬於Dangerous Permissions,僅調試工具獲取外部補丁需要,不影響線上發佈的補丁加載,調試時請自行做好android6.0以上的運行時權限獲取。

4、AndroidManifest文件配置SDK參數

 <meta-data
    android:name="com.taobao.android.hotfix.IDSECRET"
    android:value="App ID" />
    <meta-data
    android:name="com.taobao.android.hotfix.APPSECRET"
    android:value="App Secret" />
    <meta-data
    android:name="com.taobao.android.hotfix.RSASECRET"
    android:value="RSA密鑰" />

注:App ID/App Secret將被用於計量計費,請妥善保管注意安全。

5 、混淆配置

#基線包使用,生成mapping.txt
-printmapping mapping.txt
#生成的mapping.txt在app/build/outputs/mapping/release路徑下,移動到/app路徑下
#修復後的項目使用,保證混淆結果一致
#-applymapping mapping.txt
#hotfix
-keep class com.taobao.sophix.**{*;}
-keep class com.ta.utdid2.device.**{*;}
-dontwarn com.alibaba.sdk.android.utils.**
#防止inline
-dontoptimize
#看情況是否要混淆
-keepclassmembers class com.example.hotfixtext.MyApplication {
        public <init>();
    }

SDK接口使用說明

1、接入範例

initialize的調用應該儘可能的早,必須在Application.attachBaseContext()的最開始(在super.attachBaseContext之後,如果有Multidex,也需要在Multidex.install之後)進行SDK初始化操作,初始化之前不能用到其他自定義類,否則極有可能導致崩潰。而查詢服務器是否有可用補丁的操作可以在後面的任意地方。不建議在Application.onCreate()中初始化,因爲如果帶有ContentProvider,就會使得Sophix初始化時機太遲從而引發問題。

SDK穩健接入

Sophix最新版本引入了新的初始化方式。

原來的初始化方式仍然可以使用。只是新方式可以提供更全面的功能修復支持,將會帶來以下優點:初始化與應用原先業務代碼完全隔離,使得原先真正的Application可以修復,並且減少了補丁預加載時間等等。另外,新方式能夠更完美地兼容Android 8.0以後版本。

具體而言,是需要用戶自行加入以下這個類:

    package com.my.pkg;
    import android.app.Application;
    import android.content.Context;
    import android.support.annotation.Keep;
    import android.util.Log;
    import com.taobao.sophix.PatchStatus;
    import com.taobao.sophix.SophixApplication;
    import com.taobao.sophix.SophixEntry;
    import com.taobao.sophix.SophixManager;
    import com.taobao.sophix.listener.PatchLoadStatusListener;
    import com.my.pkg.MyRealApplication;
    /**
     * Sophix入口類,專門用於初始化Sophix,不應包含任何業務邏輯。
     * 此類必須繼承自SophixApplication,onCreate方法不需要實現。
     * 此類不應與項目中的其他類有任何互相調用的邏輯,必須完全做到隔離。
     * AndroidManifest中設置application爲此類,而SophixEntry中設爲原先Application類。
     * 注意原先Application裏不需要再重複初始化Sophix,並且需要避免混淆原先Application類。
     * 如有其它自定義改造,請諮詢官方後妥善處理。
     */
    public class SophixStubApplication extends SophixApplication {
        private final String TAG = "SophixStubApplication";
        // 此處SophixEntry應指定真正的Application,並且保證RealApplicationStub類名不被混淆。
        @Keep
        @SophixEntry(MyRealApplication.class)  //MyRealApplication-->修改成自己的全局類
        static class RealApplicationStub {}
        @Override
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(base);
    //         如果需要使用MultiDex,需要在此處調用。
    //         MultiDex.install(this);
            initSophix();
        }
        private void initSophix() {
            String appVersion = "0.0.0";
            try {
                appVersion = this.getPackageManager()
                                 .getPackageInfo(this.getPackageName(), 0)
                                 .versionName;
            } catch (Exception e) {
            }
            final SophixManager instance = SophixManager.getInstance();
            instance.setContext(this)
                    .setAppVersion(appVersion)
                    .setSecretMetaData(key, Secret, rsaSecret)//填寫申請的key
                    .setEnableDebug(true)
                    .setEnableFullLog()
                    .setPatchLoadStatusStub(new PatchLoadStatusListener() {
                        @Override
                        public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
                            if (code == PatchStatus.CODE_LOAD_SUCCESS) {
                                Log.i(TAG, "sophix load patch success!");
                            } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
                                // 如果需要在後臺重啓,建議此處用SharePreference保存狀態。
                                Log.i(TAG, "sophix preload patch success. restart app to make effect.");
                            }
                        }
                    }).initialize();
        }
    }
    // queryAndLoadNewPatch不可放在attachBaseContext 中,否則無網絡權限,建議放在後面任意時刻,如onCreate中
    SophixManager.getInstance().queryAndLoadNewPatch();
        @Keep
        @SophixEntry(MyRealApplication.class)
        static class RealApplicationStub {}

SophixEntry應指定項目中原先真正的Application(原項目裏application的android::name指定的),這裏用MyRealApplication指代。並且保證RealApplicationStub類名不被混淆。而SophixStubApplication的類名和包名可以自行取名。

這裏的Keep是android.support包中的類,目的是爲了防止這個內部靜態類的類名被混淆,因爲sophix內部會反射獲取這個類的SophixEntry。如果項目中沒有依賴android.support的話,就需要在progurad裏面手動指定RealApplicationStub不被混淆,詳見下文。

然後,在proguard文件裏面需要加上下面內容:

    -keepclassmembers class com.example.test.MyApplication {
        public <init>();
    }


    # 如果不使用android.support.annotation.Keep則需加上此行
    # -keep class com.my.pkg.SophixStubApplication$RealApplicationStub

目的是防止真正Application的構造方法被proguard混淆。

最後,需要把AndroidManifest裏面的application改爲這個新增的SophixStubApplication類:

        <application
            android:name="com.my.pkg.SophixStubApplication"
            ... ...>
            ... ...

這樣便完成了新方式的初始化接入改造。

總結一下,過程一共有四個步驟:

  1. 把此SophixStubApplication入口類添加進項目中,所有Sophix相關初始化放在此類中。並且不應包含開發者的任何業務邏輯代碼。 若使用了MultiDex,也應在SophixStubApplication的initSophix之前添加,並且需要記得在原來的Application裏面去除MultiDex,避免重複調用導致問題。
  2. 把RealApplicationStub的SophixEntry註解的內容改爲自己原先真正的MyRealApplication類。
  3. 混淆文件中確保某些內容不被混淆。
  4. AndroidManifest裏面的application改爲新增的SophixStubApplication入口類。

2、接口說明

1 initialize方法

  • initialize(): <必選>
    該方法主要做些必要的初始化工作以及如果本地有補丁的話會加載補丁, 但不會自動請求補丁。因此需要自行調用queryAndLoadNewPatch方法拉取補丁。這個方法調用需要儘可能的早, 必須在Application的attachBaseContext方法的最前面調用(在super.attachBaseContext之後,如果有Multidex,也需要在Multidex.install之後), initialize()方法調用之前你需要先調用如下幾個方法進行一些必要的參數設置, 方法調用說明如下:
  • setContext(application): <必選> 傳入入口Application即可
  • setAppVersion(appVersion): <必選> 應用的版本號
    說明:appVersion在產生補丁的兩包必須相同,否則無法加載補丁
    appVersion默認取的module的build.gradle裏面的versionName,當然可以自定義,推薦使用versionName,並且最好與熱修復控制檯的版本保持一致,防止版本多了混亂。
  • setSecretMetaData(idSecret, appSecret, rsaSecret): <可選,推薦使用> 三個Secret分別對應AndroidManifest裏面的三個,可以不在AndroidManifest設置而是用此函數來設置Secret。放到代碼裏面進行設置可以自定義混淆代碼,更加安全,此函數的設置會覆蓋AndroidManifest裏面的設置,如果對應的值設爲null,默認會在使用AndroidManifest裏面的。
  • setAesKey(aesKey): <可選> 用戶自定義aes祕鑰, 會對補丁包採用對稱加密。這個參數值必須是16位數字或字母的組合,是和補丁工具設置裏面AES Key保持完全一致, 補丁才能正確被解密進而加載。此時平臺無感知這個祕鑰, 所以不用擔心阿里雲移動平臺會利用你們的補丁做一些非法的事情。
  • setPatchLoadStatusStub(new PatchLoadStatusListener()): <可選,推薦> 設置patch加載狀態監聽器, 該方法參數需要實現PatchLoadStatusListener接口, 接口參數說明見2.7.2說明

2 PatchLoadStatusListener接口

該接口需要自行實現並傳入initialize方法中, 補丁加載狀態會回調給該接口, 參數說明如下:

  • mode: 無實際意義, 爲了兼容老版本, 默認始終爲0
  • code: 補丁加載狀態碼(code=1代表加載成功), 更多文檔(超鏈接)PatchStatus類說明
  • info: 補丁加載詳細說明
  • handlePatchVersion: 當前處理的補丁版本號, 0:無 -1:本地補丁 其它:後臺補丁

3 queryAndLoadNewPatch方法

該方法主要用於查詢服務器是否有新的可用補丁. SDK內部限制連續兩次queryAndLoadNewPatch()方法調用不能短於3s, 否則的話就會報code:19的錯誤碼. 如果查詢到可用的話, 首先下載補丁到本地。

  • 應用原本沒有補丁, 那麼如果當前應用的補丁是熱補丁, 那麼會立刻加載(不管是冷補丁還是熱補丁). 如果當前應用的補丁是冷補丁, 那麼需要重啓生效.

  • 應用已經存在一個補丁, 請求發現有新補丁後,本次不受影響。並且在下次啓動時補丁文件刪除, 下載並預加載新補丁。在下下次啓動時應用新補丁。

  • 補丁在後臺發佈之後, 並不會主動下行推送到客戶端, 需要手動調用queryAndLoadNewPatch方法查詢後臺補丁是否可用.
    只會下載補丁版本號比當前應用存在的補丁版本號高的補丁, 比如當前應用已經下載了補丁版本號爲5的補丁, 那麼只有後臺發佈的補丁版本號>5纔會重新下載.

同時1.4.0以上版本服務後臺上線了“一鍵清除”補丁的功能, 所以如果後臺點擊了“一鍵清除”那麼這個方法將會返回code:18的狀態碼. 此時本地補丁將會被強制清除, 同時不清除本地補丁版本號

4 killProcessSafely方法

可以在PatchLoadStatusListener監聽到CODE_LOAD_RELAUNCH後在合適的時機,調用此方法殺死進程。注意,不可以直接Process.killProcess(Process.myPid())來殺進程,這樣會擾亂Sophix的內部狀態。因此如果需要殺死進程,建議使用這個方法,它在內部做一些適當處理後才殺死本進程。

5 cleanPatches()方法

清空本地補丁,並且不再拉取被清空的版本的補丁。正常情況下不需要開發者自己調用,因爲Sophix內部會判斷對補丁引發崩潰的情況進行自動清空。

常見狀態碼說明如下: 一個補丁的加載一般分爲三個階段: 查詢/預加載/加載

        //兼容老版本的code說明
        int CODE_LOAD_SUCCESS = 1;//加載階段, 成功
        int CODE_ERR_INBLACKLIST = 4;//加載階段, 失敗設備不支持
        int CODE_REQ_NOUPDATE = 6;//查詢階段, 沒有發佈新補丁
        int CODE_REQ_NOTNEWEST = 7;//查詢階段, 補丁不是最新的 
        int CODE_DOWNLOAD_SUCCESS = 9;//查詢階段, 補丁下載成功
        int CODE_DOWNLOAD_BROKEN = 10;//查詢階段, 補丁文件損壞下載失敗
        int CODE_UNZIP_FAIL = 11;//查詢階段, 補丁解密失敗
        int CODE_LOAD_RELAUNCH = 12;//預加載階段, 需要重啓
        int CODE_REQ_APPIDERR = 15;//查詢階段, appid異常
        int CODE_REQ_SIGNERR = 16;//查詢階段, 簽名異常
        int CODE_REQ_UNAVAIABLE = 17;//查詢階段, 系統無效
        int CODE_REQ_SYSTEMERR = 22;//查詢階段, 系統異常
        int CODE_REQ_CLEARPATCH = 18;//查詢階段, 一鍵清除補丁
        int CODE_PATCH_INVAILD = 20;//加載階段, 補丁格式非法
        //查詢階段的code說明
        int CODE_QUERY_UNDEFINED = 31;//未定義異常
        int CODE_QUERY_CONNECT = 32;//連接異常
        int CODE_QUERY_STREAM = 33;//流異常
        int CODE_QUERY_EMPTY = 34;//請求空異常
        int CODE_QUERY_BROKEN = 35;//請求完整性校驗失敗異常
        int CODE_QUERY_PARSE = 36;//請求解析異常
        int CODE_QUERY_LACK = 37;//請求缺少必要參數異常
        //預加載階段的code說明
        int CODE_PRELOAD_SUCCESS = 100;//預加載成功
        int CODE_PRELOAD_UNDEFINED = 101;//未定義異常
        int CODE_PRELOAD_HANDLE_DEX = 102;//dex加載異常
        int CODE_PRELOAD_NOT_ZIP_FORMAT = 103;//基線dex非zip格式異常
        int CODE_PRELOAD_REMOVE_BASEDEX = 105;//基線dex處理異常
        //加載階段的code說明 分三部分dex加載, resource加載, lib加載
        //dex加載
        int CODE_LOAD_UNDEFINED = 71;//未定義異常
        int CODE_LOAD_AES_DECRYPT = 72;//aes對稱解密異常
        int CODE_LOAD_MFITEM = 73;//補丁SOPHIX.MF文件解析異常
        int CODE_LOAD_COPY_FILE = 74;//補丁拷貝異常
        int CODE_LOAD_SIGNATURE = 75;//補丁簽名校驗異常
        int CODE_LOAD_SOPHIX_VERSION = 76;//補丁和補丁工具版本不一致異常
        int CODE_LOAD_NOT_ZIP_FORMAT = 77;//補丁zip解析異常
        int CODE_LOAD_DELETE_OPT = 80;//刪除無效odex文件異常
        int CODE_LOAD_HANDLE_DEX = 81;//加載dex異常
        // 反射調用異常
        int CODE_LOAD_FIND_CLASS = 82;
        int CODE_LOAD_FIND_CONSTRUCTOR = 83;
        int CODE_LOAD_FIND_METHOD = 84;
        int CODE_LOAD_FIND_FIELD = 85;
        int CODE_LOAD_ILLEGAL_ACCESS = 86;
        //resource加載
        public static final int CODE_LOAD_RES_ADDASSERTPATH = 123;//新增資源補丁包異常
        //lib加載
        int CODE_LOAD_LIB_UNDEFINED = 131;//未定義異常
        int CODE_LOAD_LIB_CPUABIS = 132;//獲取primaryCpuAbis異常
        int CODE_LOAD_LIB_JSON = 133;//json格式異常
        int CODE_LOAD_LIB_LOST = 134;//lib庫不完整異常
        int CODE_LOAD_LIB_UNZIP = 135;//解壓異常
        int CODE_LOAD_LIB_INJECT = 136;//注入異常

補丁管理

簡單說一下Sophix修復的過程,首先需要兩個apk包,一個是線上或者測試bug包,另一個是修復好問題的apk包,通過使用補丁工具,兩個不同apk包會產生一個補丁,補丁上傳阿里服務器,掃碼下載補丁或者手動存放到本地,千萬不要一開始就直接發佈補丁,首先要用調試工具調試一下,查看是否完全符合自己的要求,當補丁符合要求再去發佈,具體發佈詳情見下面的發佈。並且要保證兩個包是同一個版本,一個版本同時只能存在一個補丁,所以如果當前最近的補丁不是第一個補丁,那麼最新的補丁一定是結合修復了之前所有補丁之後通過補丁工具生成的補丁。

3、 創建補丁

1 下載補丁和調試工具

patch補丁包生成需要使用到打補丁工具SophixPatchTool, 如還未下載打包工具,請前往下載Android打包工具。

該工具提供了Windows和macOS和Linux版本,Windows下運行SophixPatchTool.exe,macOS下運行SophixPatchTool.app,Linux下(Ubuntu 16.04 64bit最佳)運行SophixPatchTool。並且需要安裝Java環境且在JDK7或以上才能正常使用。
說明:一些注意事項在補丁工具中會介紹(兩張圖片介紹)

2 補丁工具使用

解壓補丁工具,運行SophixPatchTool.exe,如下圖:

說明:

  • 簽名是必須要填寫的,高級設置裏面一些參數,具體情況而定
  • 產生的補丁一定不要修改名稱(補丁名稱:sophix-patch.jar)
  • 注意,補丁包的名字必須是 sophix-patch.jar ,不能修改!

3上傳補丁

點擊發布之後,需要我們進行調試發佈補丁,接下來進入調試、發佈補丁階段

 

使用說明:
1、使用工具調試之前一定要先安裝有bug的版本
2 、接下來連接應用
3、加載補丁有兩種方式,一種是掃碼二維碼的方式,第二種是加載補丁放在本地,直接加載(本地加載需要讀權限,涉及到Android6.0動態權限的設置),推薦使用掃描方式加載補丁。
4、通過下方的日誌獲取當前補丁的加載過程

掃描補丁之後查看調試工具日誌輸出,查看補丁情況,如果之前打補丁的時候高級設置裏面設置強制冷啓動,一定要先殺死當前測試熱修復應用的進程,然後再次打開查看打補丁之後的情況。

灰度發佈:可指定修復補丁的手機數量
全量發佈:用於生產環境。經本地測試,灰度發佈測試沒問題後,就可以全量發佈了

發佈完畢,殺掉進程(而不是返回)纔會生效。

發佈補丁

發佈前請嚴格按照:掃碼內測 => 灰度發佈 => 全量發佈的流程進行,以保證補丁包能夠正常在所有Android版本的機型上生效。爲了保險起見,理論上應該對每個版本的android手機都測一遍是否生效會比較好。不過,其實只需測試通過以下具有代表性的Android版本就基本沒什麼大問題了:4.0、4.4、5.1、7.0

官網demo地址:https://github.com/aliyun/alicloud-android-demo/tree/master/hotfix_android_demo

官方接入:移動熱修復-快速開始

Android熱修復常見問題:   常見問題

對騰訊 Tinker 感興趣的也可以看看另一篇文章bugly熱修復集成與踩坑總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章