教你如何在SDK開發使用美團Robust進行熱更新

OuBowu發表於2017-11-07

RobustForSdk

演示如何在SDK開發中使用美團的Robust進行程式碼的熱更新

一開始要做SDK的熱更新,我的內心是拒絕的-_-。看了大名鼎鼎的Tinker、Sophix、Robust之後,基於SDK的應用場景和時效性,我選擇了Robust,以下介紹SDK接入Robust的整個流程~

接入流程

1.Robust無法直接應用在SDK專案的解決方式

首先參考Robust接入指南完成配置,這裡不再贅述

裡面寫到Module是需要應用程式的形式

    apply plugin: 'com.android.application'
    //製作補丁時將這個開啟,auto-patch-plugin緊跟著com.android.application
    //apply plugin: 'auto-patch-plugin'
    apply plugin: 'robust'複製程式碼

很明顯SDK的開發是apply plugin: 'com.android.library',如果編譯的話會報類似如下錯誤

Failed to notify project evaluation listener.
Transforms with scopes '[SUB_PROJECTS, SUB_PROJECTS_LOCAL_DEPS, EXTERNAL_LIBRARIES]' cannot be applied to library projects.
Could not find property 'applicationVariants' on com.android.build.gradle.LibraryExtension_Decorated@6a5c2d2d.

因此我們需要思考如何能打出Robust處理過的jar包呢?

我們不妨將SDK的Module配置成application形式先,然後打個develop渠道的apk包,可以看到\sdk\build\outputs\apk\develop\release目錄下生成了apk包,

QQ截圖20171026151701.png
QQ截圖20171026151701.png

既然如此,那麼apk在打包過程中編譯生產的資原始檔、程式碼檔案應該會生成在\sdk\build的某個目錄下,因此我們通過*.jar搜尋找到了apk對應的程式碼jar檔案,gradle-3.0.0 & robust-0.4.71對應的路徑為
\sdk\build\intermediates\transforms\proguard\develop\release\0.jar;gradle-2.3.3 & robust-0.4.7對應的路徑為 \sdk\build\intermediates\transforms\proguard\develop\release\jars\3\1f\main.jar,用檢視工具可知Robust已經在編譯過程中插入熱修復需要的程式碼

QQ截圖20171026152420.png
QQ截圖20171026152420.png

然後其它資原始檔,清單檔案,參考標準的aar解壓後的結構可以在\sdk\build下能找到其餘對應的路徑,這裡就不再贅述,參考 sdk 目錄下build.gradle 的打包jar包task即可

QQ截圖20171026153115.png
QQ截圖20171026153115.png

2.jar包的處理以及aar的打包

從上面的分析我們得知了jar包和各種資源的路徑,因此我們可以介入gradle打包apk的過程,將打包apk過程中生成的各項檔案進行處理,然後合併成aar包,輸出指定的目錄。下面就以多渠道的打包處理,講解一下如何處理

在gradle.properties配置兩個變數,用於方便切換SDK的打包模式,宿主Module在開發依賴的時候可以根據變數採取不同依賴方式

# Application模式,Robust需要是Application才能插入程式碼和打補丁
isAppModule=true
# Application模式下開啟這個就可以打補丁
isPatchModule=false複製程式碼

apply plugin的配置

// apply plugin表示該專案會使用指定的外掛,sdk對應的是com.android.library
if (isAppModule.toBoolean()) {
    # Application模式,使用robust
    apply plugin: 'com.android.application'
    if (isPatchModule.toBoolean()) {
        //製作補丁時將這個開啟,auto-patch-plugin緊跟著com.android.application
        apply plugin: 'auto-patch-plugin'
    }
    apply plugin: 'robust'
} else {
    apply plugin: 'com.android.library'
}複製程式碼

配置兩個渠道

    // 配置渠道
    productFlavors {
        // 測試渠道
        develop {
            dimension "test"
        }
        // 預設
        normal {
            dimension "test"
        }
    }複製程式碼

配置清單檔案指定的路徑,因為application和library的清單檔案是有明顯區別的

    sourceSets {
        main {
            // 指定jni的檔案源為資料夾libs
            jniLibs.srcDirs = ['libs']

            // Application和Library清單檔案處理方式不同
            if (isAppModule.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/release/AndroidManifest.xml'
            }

        }
    }複製程式碼

依賴的配置

dependencies {

    // 避免宿主與我們sdk造成第三方jar包衝突,本地依賴第三方jar包不打入sdk模組的jar包
    compileOnly fileTree(dir: 'libs', include: ['*.jar'])

    androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    testImplementation 'junit:junit:4.12'

    // 製作補丁的時候robust打入jar包裡面,不用宿主再去compile,這樣sdk的熱修復對於宿主無感知
    implementation 'com.meituan.robust:robust:0.4.71'

    // 遠端依賴aar包的話不支援provided或者compileOnly形式,由此需要在自定義task打jar包的時候過濾掉
    implementation 'com.orhanobut:logger:2.1.1'

    // 與依賴logger同理
    implementation 'cn.bmob.android:bmob-sdk:3.5.8'

    // secret.jar由secret模組輸出的jar包,直接打入sdk模組的jar包,使用api而不是implementation是因為在app模組有直接使用secret裡面的方法,因此暴露此依賴
    api files('libs/secret.jar')

}複製程式碼

自定義task進行打包的處理,介入專案打release版本apk包的過程

// 專案打release版本apk包的話,必然會呼叫到assemble(渠道)Release的命令,於是我們可以用正則匹配來匹配所有渠道的打Release包過程
Pattern p = Pattern.compile("^assemble(.*)Release\$")

// 在task新增到列表的時候,進行打包task的匹配
tasks.whenTaskAdded { task ->
    if (!isAppModule.toBoolean()) {
        // 不是Application模式不處理
        return
    }
    // 在任務執行的時候,匹配執行assemble(渠道)Release的打APK任務
    Matcher m = p.matcher(task.name)
    if (m.find()) {
        // 取出渠道
        String flavor = m.group(1)
        if (flavor.length() > 1) {
            // 渠道命名的修正,首字母小寫,例如develop渠道對應命令為assembleDevelopRelease
            flavor = flavor.substring(0, 1).toLowerCase() + flavor.substring(1)
        }

        // 打release包task完成之後進行資源的整合以及jar包去指定class檔案,並且生成aar包
        task.doLast {

            delete {
                // 刪除上次生成的檔案目錄,目錄為 \sdk\robustjar\(渠道)\release
                delete projectDir.toString() + File.separator + 'robustjar' + File.separator + flavor + File.separator + "release"
            }

            // 打包所需資源所在的父目錄, \sdk\build\intermediates
            String intermediatesPath = buildDir.toString() + File.separator + "intermediates"

            // gradle-3.0.0 & robust-0.4.71對應的路徑為 \sdk\build\intermediates\transforms\proguard\(渠道)\release\0.jar
            String robustJarPath = intermediatesPath + File.separator + "transforms" + File.separator + "proguard" + File.separator + flavor + File.separator + "release" + File.separator + "0.jar"

            // gradle-2.3.3 & robust-0.4.7對應的路徑為 \sdk\build\intermediates\transforms\proguard\(渠道)\release\jars\3\1f\main.jar
            // String robustJarPath = intermediatesPath + File.separator + "transforms" + File.separator + "proguard" + File.separator + flavor + File.separator + "release" + File.separator + "jars" + File.separator + "3" + File.separator + "1f" + File.separator + "main.jar"

            // 資原始檔的路徑,\sdk\build\intermediates\assets\(渠道)\release
            String assetsPath = intermediatesPath + File.separator + "assets" + File.separator + flavor + File.separator + "release"

            // 依賴本地jar包路徑,\sdk\build\intermediates\jniLibs\(渠道)\release
            String libsPath = intermediatesPath + File.separator + "jniLibs" + File.separator + flavor + File.separator + "release"

            // res資原始檔的路徑,\sdk\build\intermediates\res\merged\(渠道)\release,經測試發現此目錄下生成的.9圖片會失效,因此棄置,換另外方式處理
            // String resPath = intermediatesPath + File.separator + "res" + File.separator + "merged" + File.separator + flavor + File.separator + "release"

            // 由於上述問題,直接用專案的res路徑 \sdk\src\main\res ,因此第三方依賴的資原始檔無法整合,但是我是基於生成只包含自身程式碼的jar包和資源,其餘依賴宿主另外再依賴的方案,所以可以這樣處理
            String resPath = projectDir.toString() + File.separator + "src" + File.separator + "main" + File.separator + "res"

            // 資源id路徑,\sdk\build\intermediates\symbols\(渠道)\release
            String resIdPath = intermediatesPath + File.separator + "symbols" + File.separator + flavor + File.separator + "release"

            // 清單檔案路徑,\sdk\build\intermediates\manifests\full\(渠道)\release,由於是生成的application的清單檔案,因此下面還會做刪除元件宣告的處理
            String manifestPath = intermediatesPath + File.separator + "manifests" + File.separator + "full" + File.separator + flavor + File.separator + "release"

            // 整合上述檔案後的目標路徑,\sdk\robustjar\(渠道)\release\origin
            String destination = projectDir.toString() + File.separator + /*'outputs' + File.separator +*/ 'robustjar' + File.separator + flavor + File.separator + 'release' + File.separator + 'origin'

            // 貌似aidl的資料夾沒啥用,打包會根據例如G:\\sms-hotfix\\SmsParsingForRcs-Library\\library\\src\\main\\aidl\\com\\cmic\\IMyAidlInterface.aidl的定義程式碼生成com.cmic.IMyAidlInterface到jar包裡面,因此aidl僅僅是空資料夾
            // String aidlPath = buildDir.toString() + File.separator + "generated" + File.separator + "source" + File.separator + "aidl" + File.separator + flavor + File.separator + "release"

            File file = file(robustJarPath)
            if (file.exists()) {
                println '渠道是:' + flavor + ';開始複製robust插樁jar包'
                copy {

                    // 拷貝到assets目錄
                    from(assetsPath) {
                        into 'assets'
                    }

                    //  第三方本地jar包不處理,提供宿主整合的時候另外提供
                    //  from(libsPath) {
                    //      into 'libs'
                    //      include '**/*.jar'
                    //      exclude {
                    //          // println it.path+";"+it.isDirectory()
                    //          it.isDirectory()
                    //      }
                    //  }

                    // .so檔案拷貝到jni目錄
                    from(libsPath) {
                        into 'jni'
                        include '**/*/*.so'
                    }

                    // 資原始檔拷貝到res目錄
                    from(resPath) {
                        // 排除MainActivity載入的佈局檔案,因為輸出的是jar包,加MainActivity僅僅是為了能讓打apk包任務執行
                        exclude '/layout/activity_main.xml'
                        exclude {
                            // 排除空資料夾
                            it.isDirectory() && it.getFile().listFiles().length == 0
                        }
                        into 'res'
                    }

                    // aidl的資料夾沒啥用,不處理
                    // from(aidlPath) {
                    //     into 'aidl'
                    // }

                    // 拷貝此目錄下資源id檔案 R.txt
                    from resIdPath

                    // 拷貝到目錄 \sdk\robustjar\(渠道)\release\origin
                    into destination

                }

                copy {
                    // 複製供宿主的混淆規則,這裡我在android{ defaultConfig { consumerProguardFiles 'lib-proguard-rules.pro' }},配置了一個混淆規則
                    def files = android.defaultConfig.consumerProguardFiles
                    if (files != null && files.size() > 0) {
                        def file1 = files.get(0);
                        //  println '混淆檔案路徑:'+file1.path
                        from file1.path
                        into destination
                        // 複製混淆規則並且重新命名
                        rename(file1.name, 'proguard.txt')
                    }
                }

                // 補丁生成需要的mapping.txt和methodsMap.robust檔案
                copy {
                    // 混淆mapping檔案的路徑,\sdk\build\outputs\mapping\(渠道)\release\mapping.txt
                    from(buildDir.toString() + File.separator + 'outputs' + File.separator + 'mapping' + File.separator + flavor + File.separator + 'release') {
                        include 'mapping.txt'
                    }
                    // 拷貝到目錄 \sdk\robustjar\(渠道)\release
                    into projectDir.toString() + File.separator + 'robustjar' + File.separator + flavor + File.separator + 'release'
                }

                copy {
                    // robust生成的methodsMap檔案路徑,\sdk\build\outputs\robust\methodsMap.robust
                    from(buildDir.toString() + File.separator + 'outputs' + File.separator + 'robust') {
                        include 'methodsMap.robust'
                    }
                    // 拷貝到目錄 \sdk\robustjar\(渠道)\release
                    into projectDir.toString() + File.separator + 'robustjar' + File.separator + flavor + File.separator + 'release'
                }

                // 若不存在aidl目錄,建立aidl空目錄
                createDir(destination + File.separator + "aidl")
                // 同上
                createDir(destination + File.separator + "assets")
                // 同上
                createDir(destination + File.separator + "jni")
                // 同上
                createDir(destination + File.separator + "libs")
                // 同上
                createDir(destination + File.separator + "res")

                // 將清單檔案application節點的內容和activity節點的內容替換,例如下面
                <application
                    android:allowBackup="true"
                    tools:replace="android:label"
                    android:label="sdk"
                    android:supportsRtl="true"
                    android:icon="@android:drawable/ic_dialog_info"
                    android:theme="@android:style/Theme.Black"
                    >
                    <activity android:name=".MainActivity">
                        <intent-filter>
                            <action android:name="android.intent.action.MAIN"/>
                            <category android:name="android.intent.category.LAUNCHER"/>
                        </intent-filter>
                    </activity>
                </application>
                轉換成
                <application
                    android:allowBackup="true"
                    tools:replace="android:label"
                    android:label="sdk"
                    android:supportsRtl="true">
                </application>

                def oldStr = ["<application[\\s\\S]*?>", "<activity[\\s\\S]*?</activity>"]
                def newStr = ["<application\n" + "        android:allowBackup=\"true\"\n" + "        android:supportsRtl=\"true\">", ""]
                // 處理 \sdk\build\intermediates\manifests\full\(渠道)\release\AndroidManifest.xml
                String strBuffer = fileReader(manifestPath + File.separator + "AndroidManifest.xml", oldStr, newStr)
                // 輸出至 \sdk\robustjar\(渠道)\release\origin\AndroidManifest.xml
                fileWrite(destination + File.separator + "AndroidManifest.xml", strBuffer)

                println '輸出robust插樁jar包成功!'

                // 執行打jar包的task,這裡會做原jar包的過濾處理,只保留我們需要的程式碼
                tasks.findByName('jar_' + flavor).execute()

                // 執行打aar包的task,其實就是將目錄\sdk\robustjar\develop\release\origin壓縮成aar字尾的壓縮包
                tasks.findByName('aar_' + flavor).execute()

            }
        }
    }
}複製程式碼

上述task需要執行的另外兩個task

// 根據渠道生成打jar包和aar包的對應的task
for (String flavor : android.productFlavors.names) {

    // 遍歷所有渠道,生成對應渠道打jar包的task,名字為jar_(渠道)
    tasks.create(name: 'jar_' + flavor, type: Jar) {

        println "當前渠道是:" + flavor

        // jar包命名為classes.jar
        baseName 'classes'

        String intermediatesPath = buildDir.toString() + File.separator + "intermediates"

        // gradle-2.3.3 & robust-0.4.7對應的路徑為 \sdk\build\intermediates\transforms\proguard\(渠道)\release\jars\3\1f\main.jar
        // String robustJarPath = intermediatesPath + File.separator + "transforms" + File.separator + "proguard" + File.separator + flavor + File.separator + "release" + File.separator + "jars" + File.separator + "3" + File.separator + "1f" + File.separator + "main.jar"

         // gradle-3.0.0 & robust-0.4.71對應的路徑為 \sdk\build\intermediates\transforms\proguard\(渠道)\release\0.jar
        String robustJarPath = intermediatesPath + File.separator + "transforms" + File.separator + "proguard" + File.separator + flavor + File.separator + "release" + File.separator + "0.jar"

        def zipFile = new File(robustJarPath)
        // 將jar包解壓
        FileTree jarTree = zipTree(zipFile)

        from jarTree

        // jar包輸出路徑為 \sdk\robustjar\(渠道)\release\origin
        File destDir = file(projectDir.toString() + File.separator + 'robustjar' + File.separator + flavor + File.separator + 'release' + File.separator + 'origin')
        // 設定輸出路徑
        setDestinationDir destDir

        include {
            // 只打包我們需要的類
            it.path.startsWith('com/oubowu/sdk') || it.path.startsWith('com/meituan/robust') || it.path.startsWith('com/oubowu/secret')
        }

        exclude {
            // println "執行排除:" + it.path
            // 排除R相關class檔案,排除MainActivity.class檔案
            it.path.startsWith('com/oubowu/sdk/R$') || it.path.startsWith('com/oubowu/sdk/R.class') || it.path.startsWith('com/oubowu/sdk/MainActivity.class')
        }

        println '壓縮jar包完畢!!!!!!!!'
    }

    // 遍歷所有渠道,生成對應渠道打aar包的task,名字為aar_(渠道)
    tasks.create(name: 'aar_' + flavor, type: Zip) {
        // aar包輸出路徑為 \sdk\robustjar\(渠道)\release\aar
        File destDir = file(projectDir.toString() + File.separator + 'robustjar' + File.separator + flavor + File.separator + 'release' + File.separator + 'aar')
        // aar包命名為 library-(渠道)-release.aar
        archiveName 'library-' + flavor + '-release.aar'
        // 源路徑為 \sdk\robustjar\(渠道)\release\origin
        from projectDir.toString() + File.separator + 'robustjar' + File.separator + flavor + File.separator + 'release' + File.separator + 'origin'
        // 設定壓縮後輸出的路徑
        destinationDir destDir

        println '壓縮aar包完畢!!!!!!!!'
    }

}複製程式碼

至此我們的自定義打包task就編寫完成了,執行assembleDevelopRelease任務,即可生成我們想要的jar包和aar包了

QQ截圖20171101111237.png
QQ截圖20171101111237.png

牛皮扯完了,給出專案地址RobustForSdk,可以下載下來執行方便理解。

接下來我還會講解我是如何處理補丁下發和安全應用補丁的。敬請期待吧!Peace!!!

3.補丁下發和載入的策略

參考官方示例PatchManipulateImp.java來定製我們的下發載入策略

補丁策略.png
補丁策略.png

如上流程圖所述,首先去本地查詢是否已經下發過補丁,若有優先載入本地補丁,這種場景是針對沒有網路或者請求網路下發補丁失敗導致無法修復的情況;然後去請求網路下發補丁的列表資訊,sdk版本作為補丁的下發依據,然後補丁版本高的包含低版本的修復程式碼,我考慮是方便處理,不用載入多個補丁,對應專案發包的策略是一個大版本作為封版建立版本分支,後續此版本上線遇到的bug都基於這個分支做處理,直到下個版本上線合併修復的程式碼。

具體程式碼實現如下:

        // 建立1個固定執行緒的執行緒池,用於序列進行本地補丁的載入和網路請求補丁然後載入的邏輯
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
        // 讀取本地儲存的上次載入的補丁的名稱
        final String pName = context.getSharedPreferences("com.oubowu.sdk.sp", Context.MODE_PRIVATE).getString("pName", "");
        if (!pName.isEmpty()) {
            // 建立本地補丁載入的執行緒
            PatchExecutor patchExecutor1 = new PatchExecutor(context.getApplicationContext(), new PatchManipulateImp(true, pName),...);
            // 執行本地補丁載入的執行緒
            fixedThreadPool.execute(patchExecutor1);
        }

        // 建立網路請求補丁然後載入的執行緒
        PatchExecutor patchExecutor2 = new PatchExecutor(context.getApplicationContext(), new PatchManipulateImp(false, pName), ...);
        fixedThreadPool.execute(patchExecutor2);複製程式碼

補丁的處理

public class PatchManipulateImp extends PatchManipulate {

    // 是否只做本地補丁的判斷
    private boolean mOnlyLocal = true;
    // 儲存於本地的補丁的名稱
    private String mSavePatchName;

    public PatchManipulateImp(boolean onlyLocal, String savePatchName) {
        mOnlyLocal = onlyLocal;
        mSavePatchName = savePatchName;
    }

    /***
     * connect to the network ,get the latest patches
     * 聯網獲取最新的補丁
     * @param context
     *
     * @return
     */
    @Override
    protected List<Patch> fetchPatchList(final Context context) {

        final List<Patch> patches = new ArrayList<>();

        if (mOnlyLocal) {
            // 只是做本地判斷的話
            if (!mSavePatchName.isEmpty()) {
                // 名稱不為空說明存在,新增本地儲存的補丁資訊,然後返回
                addPatchInfo(context, mSavePatchName, patches);
            }
            return patches;
        }

        // 由於下面做的是非同步的網路請求下發補丁,所以使用CountDownLatch進行同步
        final CountDownLatch mCountDownLatch = new CountDownLatch(1);

        //Bmob初始化
        Bmob.initialize(context.getApplicationContext(), "52e558b89195c84cd761afbeabc3df52");

        BmobQuery<com.oubowu.sdk.Patch> query = new BmobQuery<>();
        // 通過sdkVersion查詢此sdk版本的線上補丁
        query.addWhereEqualTo("sdkVersion", BuildConfig.VERSION_NAME);
        // 根據patchVersion欄位降序顯示資料
        query.order("-patchVersion");
        // query.setLimit(1);
        query.findObjects(new FindListener<com.oubowu.sdk.Patch>() {
            @Override
            public void done(List<com.oubowu.sdk.Patch> list, BmobException e) {
                if (e != null) {
                    // 請求補丁列表資料失敗
                    Logger.e(e.getMessage());
                    mCountDownLatch.countDown();
                } else {
                    if (list != null && list.size() > 0) {
                        // 取最高補丁版本的補丁
                        final com.oubowu.sdk.Patch p = list.get(0);
                        Logger.e(p.toString());
                        final String filename = p.getPatchUrl().getFilename();
                        // 若sp存的補丁名稱跟下發的最高版本的補丁名稱不一樣的話,下載並應用補丁;或者名稱一樣,但是本地沒有此補丁,下載並應用補丁
                        if (!filename.equals(mSavePatchName) || !(new File(context.getFilesDir(), mSavePatchName).exists())) {
                            File saveFile = new File(context.getFilesDir(), filename);
                            if (!saveFile.exists()) {
                                // 本地沒有儲存的話,下載補丁
                                p.getPatchUrl().download(saveFile, new DownloadFileListener() {
                                    @Override
                                    public void done(String s, BmobException e) {
                                        if (e != null) {
                                            mCountDownLatch.countDown();
                                        } else {
                                            Logger.e("下載成功," + s);
                                            context.getSharedPreferences("com.oubowu.sdk.sp", Context.MODE_PRIVATE).edit().putString("pName", filename).apply();
                                            addPatchInfo(context, filename, patches);
                                            mCountDownLatch.countDown();
                                        }
                                    }

                                    @Override
                                    public void onProgress(Integer integer, long l) {
                                    }
                                });
                            } else {
                                // 本地已經儲存了的話,儲存名稱,直接使用
                                context.getSharedPreferences("com.oubowu.sdk.sp", Context.MODE_PRIVATE).edit().putString("pName", filename).apply();
                                addPatchInfo(context, filename, patches);
                                mCountDownLatch.countDown();
                            }
                        }

                    } else {
                        // 此sdk版本沒有補丁
                        mCountDownLatch.countDown();
                    }
                }
            }
        });

        try {
            // 阻塞等待網路請求結束
            mCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return patches;
    }

    /**
     * 新增補丁資訊
     *
     * @param context
     * @param fileName
     * @param patches
     */
    private void addPatchInfo(Context context, String fileName, List<Patch> patches) {
        // 解密下發的已加密補丁並且返回解密後的檔案路徑,使用ndk保證解密安全性
        String dPatchPath = NdkHelper.p(context, fileName, false);
        File dPatchFile = new File(dPatchPath);
        if (dPatchFile.exists()) {
            // 解密檔案存在的話,新增到補丁列表
            Patch patch = new Patch();
            patch.setName(dPatchFile.getName().replace(".jar", ""));
            patch.setLocalPath(dPatchFile.getPath().replace(".jar", ""));
            patch.setPatchesInfoImplClassFullName("com.oubowu.sdk.lib.PatchManipulateImp.PatchesInfoImpl");
            patches.add(patch);
        }
    }

    /**
     * @param context
     * @param patch
     * @return you can verify your patches here
     */
    @Override
    protected boolean verifyPatch(Context context, Patch patch) {
        //do your verification, put the real patch to patch
        //放到app的私有目錄
        patch.setTempPath(context.getCacheDir() + File.separator + "robust" + File.separator + patch.getName());
        //in the sample we just copy the file
        try {
            copy(patch.getLocalPath(), patch.getTempPath());
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("copy source patch to local patch error, no patch execute in path " + patch.getTempPath());
        }

        // 刪除解密的本地補丁
        patch.delete(patch.getLocalPath());

        return true;
    }

    ...

}複製程式碼

4.補丁的生成和使用

在SdkTest.java我寫了一個會丟擲NumberFormatException的方法

    public static void callBugMethod(Context context) {

        String strFromCPlus = NdkHelper.getStrFromCPlus();

        Logger.e(strFromCPlus);

        int i = Integer.parseInt(strFromCPlus);

    }複製程式碼

然後修復callBugMethod方法,新增一個靜態內部類

    @Modify
    public static void callBugMethod(Context context) {

        String strFromCPlus = NdkHelper.getStrFromCPlus();

        Logger.e(strFromCPlus);

        try {
            int i = Integer.parseInt(strFromCPlus);
        } catch (NumberFormatException e) {
            e.printStackTrace();
            Logger.e("我使用Robust熱更新把空指標修復啦!!!");
        }

        MyClass.call();

    }

    @Add
    public static class MyClass {
        public static void call() {
            Logger.e("我使用Robust熱更新新增了一個靜態內部類");
        }
    }複製程式碼

將 \RobustForSdk\gradle.properties isPatchModule設為true,將\sdk\robustjar\develop\release資料夾下的mapping.txt以及methodsMap.robust放到\sdk\robust資料夾

    # Application模式,Robust需要是Application才能插入程式碼和打補丁
    isAppModule=true
    # Application模式下開啟這個就可以打補丁
    isPatchModule=true複製程式碼

執行assembleDevelopRelease,得到以下提示,即說明生成補丁成功

* What went wrong:
Execution failed for task ':sdk:transformClassesWithAutoPatchTransformForDevelopRelease'.
> auto patch end successfully複製程式碼

QQ截圖20171102112715.png
QQ截圖20171102112715.png

補丁為了保證安全性,需要加密後再上傳到Bmob後臺,為了方便操作,我用MFC寫了個Window程式,可以看下 AesWindowsApplication

加解密工具.png
加解密工具.png

上傳到Bmob後臺

Bmob後臺.png
Bmob後臺.png

在app模組使用library-develop-release.aar,在主頁面MainActivity.class進行SDK初始化,SdkTest.init會執行補丁請求和載入的邏輯

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

        mTvHello = (TextView) findViewById(R.id.tv_hello);

        findViewById(R.id.bt_sdk).setOnClickListener(this);

        checkPermissionAndCallSdk();

    }

    private void checkPermissionAndCallSdk() {
        boolean checkPermission = MPermissionUtils.getInstance()
                .checkPermission(this, REQUEST_PERMISSION_SUCCESS, Manifest.permission.READ_PHONE_STATE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.READ_EXTERNAL_STORAGE);
        if (checkPermission) {
            SdkTest.init(this);
        }
    }複製程式碼

第一次啟動APP並且斷網的情況下,在點選事件呼叫SDK有bug的方法

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt_sdk:
                SdkTest.callBugMethod(this);
                mTvHello.setText(NdkHelper.getStrFromCPlus());
                break;
            default:
                break;
        }
    }複製程式碼

Logcat列印以下字串轉換整型丟擲的異常

    FATAL EXCEPTION: main
    Process: com.oubowu.robustforsdk, PID: 18306
    java.lang.NumberFormatException: Invalid int: "Hello from C++"複製程式碼

將網路開啟,第二次啟動APP,網路請求下發正常的話,列印出我們使用的補丁版本是3,對應SDK版本是2.6.0;下載成功後放在/data/data/(包名)/files;並且應用成功了

11-03 09:48:43.893 19711-19711/com.oubowu.robustforsdk E/PRETTY_LOGGER: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
11-03 09:48:43.893 19711-19711/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ Thread: main
11-03 09:48:43.893 19711-19711/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:48:43.893 19711-19711/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ From.Code  (PolicyQuery.java:264)
11-03 09:48:43.893 19711-19711/com.oubowu.robustforsdk E/PRETTY_LOGGER: │    a$1.done  (PatchManipulateImp.java:95)
11-03 09:48:43.893 19711-19711/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:48:43.893 19711-19711/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ Patch{sdkVersion='2.6.0', patchVersion='3', patchUrl=http://bmob-cdn-14435.b0.upaiyun.com/2017/10/16/37e20fe240ae64a2803b3d5ed7047be9.jar} com.oubowu.sdk.Patch@42588d90
11-03 09:48:43.893 19711-19711/com.oubowu.robustforsdk E/PRETTY_LOGGER: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────

11-03 09:51:35.513 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
11-03 09:51:35.513 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ Thread: main
11-03 09:51:35.513 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:51:35.523 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ of.onPostExecute  (BmobFileDownloader.java:2095)
11-03 09:51:35.523 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │    a$1$1.done  (PatchManipulateImp.java:109)
11-03 09:51:35.523 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:51:35.523 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ 下載成功,/data/data/com.oubowu.robustforsdk/files/e-patch-2.6.0-4.jar
11-03 09:51:35.523 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────

11-03 09:51:35.533 20875-20875/com.oubowu.robustforsdk E/secret: /data/data/com.oubowu.robustforsdk/files/e-patch-2.6.0-4.jar
11-03 09:51:35.533 20875-20875/com.oubowu.robustforsdk E/secret: /data/data/com.oubowu.robustforsdk/files/d-e-patch-2.6.0-4.jar
11-03 09:51:35.623 20875-20891/com.oubowu.robustforsdk E/PRETTY_LOGGER: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
11-03 09:51:35.623 20875-20891/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ Thread: pool-1-thread-1
11-03 09:51:35.623 20875-20891/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:51:35.623 20875-20891/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ PatchExecutor.applyPatchList  (PatchExecutor.java:71)
11-03 09:51:35.623 20875-20891/com.oubowu.robustforsdk E/PRETTY_LOGGER: │    SdkTest$3.onPatchApplied  (SdkTest.java:97)
11-03 09:51:35.623 20875-20891/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:51:35.623 20875-20891/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ PatchExecutor
11-03 09:51:35.623 20875-20891/com.oubowu.robustforsdk E/PRETTY_LOGGER: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────複製程式碼

這時候點選點選事件,可以從log看到已經使用了修復的程式碼

11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err: java.lang.NumberFormatException: Invalid int: "Hello from C++"
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at java.lang.Integer.invalidInt(Integer.java:137)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at java.lang.Integer.parse(Integer.java:374)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at java.lang.Integer.parseInt(Integer.java:365)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at java.lang.Integer.parseInt(Integer.java:331)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at com.oubowu.sdk.lib.PatchManipulateImp.SdkTestPatch.callBugMethod(SdkTestPatch.java:163)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at com.oubowu.sdk.lib.PatchManipulateImp.SdkTestPatchControl.accessDispatch(PatchTemplate.java)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at com.meituan.robust.PatchProxy.accessDispatch(PatchProxy.java:61)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at com.oubowu.sdk.SdkTest.callBugMethod(SdkTest.java)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at com.oubowu.robustforsdk.MainActivity.onClick(MainActivity.java:46)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at android.view.View.performClick(View.java:4444)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at android.view.View$PerformClick.run(View.java:18457)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at android.os.Handler.handleCallback(Handler.java:733)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:95)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at android.os.Looper.loop(Looper.java:136)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:5113)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at java.lang.reflect.Method.invokeNative(Native Method)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at java.lang.reflect.Method.invoke(Method.java:515)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:796)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:612)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk W/System.err:     at dalvik.system.NativeStart.main(Native Method)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ Thread: main
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ SdkTestPatchControl.accessDispatch  (PatchTemplate.java:-1)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │    SdkTestPatch.callBugMethod  (SdkTestPatch.java:166)
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ 我使用Robust熱更新把空指標修復啦!!!
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk D/robust: invoke static  method is       No:  23  e
11-03 09:53:46.213 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
11-03 09:53:46.223 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ Thread: main
11-03 09:53:46.223 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:53:46.223 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ SdkTestPatch.callBugMethod  (SdkTestPatch.java:169)
11-03 09:53:46.223 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │    SdkTest$MyClass.call  (SdkTest.java:176)
11-03 09:53:46.223 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
11-03 09:53:46.223 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: │ 我使用Robust熱更新新增了一個靜態內部類
11-03 09:53:46.223 20875-20875/com.oubowu.robustforsdk E/PRETTY_LOGGER: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────複製程式碼

以上就是我實踐SDK熱修復的思路和方法,希望能給讀者帶來一些用處。覺得不錯的話可以給專案RobustForSdk一個star,你的肯定是對我最大的鼓勵哦!

相關文章