Android——Sophix熱修復接入

前言

最近年底項目也沒事做了,琢磨着研究一下熱修復方案。市面上出現很多熱修復方案,大致分爲幾種,一種是dex插樁的、一種Instant run、還有一種是通過底層做修改。本文主要研究阿里雲最新的熱修復方案Sophix,Sophix較其他的優劣勢如下,對比發現Sophix熱修復最大的特點就是即使生效(已有方法修改)和冷啓動修復(新增方法和資源修改),並且Sophix已經做到兼容Android P,目前市場上比較成熟的主要是微信Tinker和阿里的Sophix,另一篇Android——Tinker熱修復接入

創建項目

1.1 註冊登錄

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

1.2 開通產品

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

1.3 創建熱修復項目

點擊下圖的加號即可創建項目,創建總分爲三步,具體如下圖:


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

"config": {
    "emas.appKey":"27478432",
    "emas.appSecret":"5e2f53a5aa6b8a48e74ac7146688b65c",
    "emas.packageName":"com.example.testsophix",
    "hotfix.idSecret":"2547832-1",
    "hotfix.rsaSecret":"//////",
    "httpdns.accountId":"102949",
    "httpdns.secretKey":"826b1d8ad5119dd34f1a855eedd3cea0"

說明:

  • 應用名稱可以隨意填寫,建議填寫項目中的appname,如果以後項目多避免混亂
  • 包名一定要填寫清單列表中包名
  • 建議json配置不要放在項目中,如果忘記下載json配置也沒關係,創建之後如下圖所示點擊下載配置即可獲取配置文件


項目接入SDK

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

2.1 Project的build.gradle配置

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

repositories {
       maven {
           url "http://maven.aliyun.com/nexus/content/repositories/releases"
       }
    }
2.2 module級別build.gradle配置

module下build.gradle文件添加版本依賴:

 compile 'com.aliyun.ams:alicloud-android-hotfix:3.2.7'
2.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"/>

說明:

  • 當使用調試工具(後面會提)調試補丁的時候,如果補丁放在本地,需要加讀權限,並且Android 6.0以上的讀寫權限動態申請
  • 聯網權限是用於拉取阿里服務器補丁
2.4 配置SDK參數

配置參數有兩種方式,一種是清單列表中,第二種是在項目的Application中通過參數的形式,推薦使用第二種(相對較爲安全)

2.4.1 AndroidManifest文件配置SDK參數

其中三個參數對應上文提到的json文件裏面的三個參數

 <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密鑰" />
2.4.2 代碼中配置SDK參數
SophixManager.getInstance()
                .setContext(this)
                .setAppVersion(appVersion)
                .setSecretMetaData(null, null, null)//這裏填寫參數
                .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();
  • Sophix初始化的時候.setSecretMetaData(null, null, null)設置三個參數,順序依次跟AndroidManifest一致。
2.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.testsophix.MyRealApplication {
        public <init>();
    }

說明:如果項目混淆開啓的情況下,在app/build/outputs/mapping/release路徑下會生成mapping.txt,建議開啓混淆(保證一些參數被混淆)。

2.6 SDK初始化

此穩健方式接入,使用了最新版本的Sophix,最新版本的Sophix採用了代理Application方式接入SDK,使自身的Application和Sophix初始化解耦合。初始化在繼承SophixApplication的SophixStubApplication(名稱可以自定義)中實現,其中自身的Application不需要做任何修改操作。

2.6.1 Application中初始化Sophix
public class SophixStubApplication extends SophixApplication {

    private final String TAG = "SophixStubApplication";

    private final String hotfixId = "25478432-1";
    private final String hotfixappKey = "5e2f53a5aa6b8a48e74ac7146688b65c";

    /**
     * 此處SophixEntry應指定真正的Application,
     * 並且保證RealApplicationStub類名不被混淆。
     *
     * @keep 註解已經做好了,無需在混淆文件做處理
     */
    @Keep
    @SophixEntry(MyRealApplication.class)
    static class RealApplicationStub {
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //如果需要使用MultiDex,需要在此處調用。
        // MultiDex.install(this);
        initSophix();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //聯網下載新的插件
        SophixManager.getInstance().queryAndLoadNewPatch();
    }

    /**
     * initialize最好放在attachBaseContext最前面
     */
    private void initSophix() {
        SophixManager.getInstance().setContext(this)
                .setAppVersion(BuildConfig.VERSION_NAME)
                .setAesKey(null)
                .setSecretMetaData(hotfixId,hotfixappKey,getString(R.string.hotfixrsaSecret))
                .setEnableDebug(true)
                .setPatchLoadStatusStub(new PatchLoadStatusListener() {
                    @Override
                    public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {

                        Log.e(TAG, "修復模式:" + mode);
                        Log.e(TAG, "修復回調code:" + code);
                        Log.e(TAG, "修復信息:" + info);
                        Log.e(TAG, "修復版本:" + handlePatchVersion);

                        // 補丁加載回調通知
                        if (code == PatchStatus.CODE_LOAD_SUCCESS) {
                            // 表明補丁加載成功
                            Log.e(TAG, "表明補丁加載成功");
                        } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
                            // 表明新補丁生效需要重啓. 開發者可提示用戶或者強制重啓;
                            // 建議: 用戶可以監聽進入後臺事件, 然後應用自殺
                            Log.e(TAG, "表明新補丁生效需要重啓. 開發者可提示用戶或者強制重啓");
                        } else if (code == PatchStatus.CODE_LOAD_FAIL) {
                            // 內部引擎異常, 推薦此時清空本地補丁, 防止失敗補丁重複加載
                            // SophixManager.getInstance().cleanPatches();
                            Log.e(TAG, "內部引擎異常, 推薦此時清空本地補丁, 防止失敗補丁重複加載");
                        } else {
                            // 其它錯誤信息, 查看PatchStatus類說明
                            Log.e(TAG, "其它錯誤信息, 查看PatchStatus類說明");
                        }
                    }
                }).initialize();
    }
}
2.6.2 修改AndroidManifest裏面的Application名稱
 <application
            android:name="com.my.pkg.SophixStubApplication"
            ... ...>
            ... ...

說明:

  • 需要把AndroidManifest裏面的Application名稱改爲這個新增的SophixStubApplication類(一定要修改,否則無法初始化Sophix)
  • SophixStubApplication必須要繼承SophixApplication
  • @SophixEntry(MyRealApplication.class)處的MyRealApplication一定要填寫自己本身的Application
  • 這裏的@Keep是android.support包中的類,目的是爲了防止這個內部靜態類的類名被混淆,因爲sophix內部會反射獲取這個類的,如果項目中沒有依賴android.support的話,就需要在progurad裏面手動指定RealApplicationStub不被混淆(就是自己的Application)。(詳情參見文檔)例子如下:
-keepclassmembers class com.my.pkg.MyRealApplication {
        public <init>();
    }
    # 如果不使用android.support.annotation.Keep則需加上此行
    # -keep class com.my.pkg.SophixStubApplication$RealApplicationStub
2.7 接口說明
2.7.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.7.2 PatchLoadStatusListener接口

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

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

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

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

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

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

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

2.7.4 killProcessSafely方法

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

2.7.5 cleanPatches()方法

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

補丁管理

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

3.1 創建補丁
3.1.1 下載補丁和調試工具

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

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

3.1.2 補丁工具使用

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



說明:

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

進入阿里雲控制檯,點擊右側的移動熱修復,進入項目補丁管理



如上圖在管理控制檯,找到質量管理下面的移動熱修復進入到補丁管理頁面

3.2.1 添加補丁版本

此處添加的版本儘量與項目中的versionname保持一致


3.2.2 上傳補丁

上傳補丁裏面描述,儘量要描述當前補丁修復那些內容,方便以後版本的回滾


3.2.3 發佈調試補丁

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


調試補丁

4.1 調試工具使用

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

4.2 加載補丁

掃描補丁之後查看調試工具日誌輸出,查看補丁情況,如果之前打補丁的時候高級設置裏面設置強制冷啓動,一定要先殺死當前測試熱修復應用的進程,然後再次打開查看打補丁之後的情況。下面列舉日誌code碼經常出現的幾種情況更多code值請查看接口文檔(超鏈接):

4.3 輸出補丁日誌
    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;//加載階段, 補丁格式非法
4.4 補丁說明

說明一:patch是針對客戶端具體某個版本的,patch和具體版本綁定

  • 應用當前版本號是1.1.0, 那麼只能在後臺查詢到1.1.0版本對應發佈的補丁, 而查詢不到之前1.0.0舊版本發佈的補丁.

說明二:針對某個具體版本發佈的新補丁, 必須包含所有的bugfix, 而不能依賴補丁遞增修復的方式, 因爲應用僅可能加載一個補丁

  • 針對1.0.0版本在後臺發佈了一個補丁版本號爲1的補丁修復了bug1, 然後發現此時針對這個版本補丁1修復的不完全, 代碼還有bug2, 在後臺重新發佈一個補丁版本號爲2的補丁, 那麼此時補丁2就必須同時包含bug1和bug2的修復才行, 而不是隻包含bug2的修復(bug1就沒被修復了)

發佈補丁

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

注意事項

具體接入細節請參考官方文檔:
非穩健方式接入
穩健方式接入

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