打造萬能更新庫(kù),修改真的只要一點(diǎn)點(diǎn)

轉(zhuǎn)載請(qǐng)標(biāo)明出處
本文出自HCY的微博

一、概述

軟件更新功能可以說是APP的標(biāo)配。以前實(shí)現(xiàn)這個(gè)功能的時(shí)候,自己一行一行代碼重復(fù)擼,浪費(fèi)時(shí)間。所以我決定實(shí)現(xiàn)一個(gè)萬能的可復(fù)用的更新庫(kù)。讓它支持增量更新、全量更新、靜默安裝、普通方式安裝、可以自定義UI。下面就來介紹一下我實(shí)現(xiàn)這個(gè)庫(kù)的主要技術(shù)點(diǎn):增量更新、靜默安裝及如何封裝。

二、軟件增量更新處理流程

(1)服務(wù)端處理流程

1.驗(yàn)證請(qǐng)求的合法性。
2.如果請(qǐng)求不合法(比如請(qǐng)求是模擬的,非客戶端發(fā)出的),則拒絕服務(wù)。
3.如果請(qǐng)求合法,獲取versionCode等信息,根據(jù)versionCode判斷軟件是否更新。
4.如果不需要更新,則返回對(duì)應(yīng)信息。
5.如果需要更新,獲取與versionCode對(duì)應(yīng)的客戶端文件的MD5,判斷該MD5值是否在歷史版本文件的MD5列表中,如果在說明支持增量更新。
6.如果不支持增量更新,則返回完整apk文件的下載鏈接。
7.如果支持增量更新,判斷對(duì)應(yīng)的patch文件是否存在。
8.如果對(duì)應(yīng)的patch文件不存在,調(diào)用腳本程序生成對(duì)應(yīng)的patch文件,并返回該patch文件的下載鏈接。
9.如果對(duì)應(yīng)的patch文件存在,則返回該patch文件的下載鏈接。

(2)客戶端處理流程

1.收集apk的基本信息,向服務(wù)端發(fā)送更新請(qǐng)求。
2.如果沒有更新,則做對(duì)應(yīng)的提示操作。
3.如果有更新,判斷是否是增量更新還是全量更新。
4.如果是全量更新,則下載對(duì)應(yīng)的apk文件,展示相應(yīng)的UI,安裝apk即可。
5.如果是增量更新,則下載對(duì)應(yīng)的patch文件,展示相應(yīng)的UI,然后提取客戶端的apk文件到指定目錄并與patch文件合并成一個(gè)新的apk文件,判斷新合成的apk文件是否與從服務(wù)端獲取的完整的apk文件MD5的值一致,若一致說明合成成功,安裝新合成的apk文件即可,若不一致說明合成失敗,進(jìn)行安裝失敗的提示。

三、增量更新的實(shí)現(xiàn)

通過上面的處理流程分析,我們發(fā)現(xiàn)實(shí)現(xiàn)增量更新的難點(diǎn)主要在于patch文件的生成、新apk文件的合成這兩個(gè)部分。這里借助開源的bsdiff來實(shí)現(xiàn)這兩部分的功能。

(1)下載二進(jìn)制差分、合并工具

增量更新的實(shí)現(xiàn)用到第三方庫(kù)bsdiff,該庫(kù)依賴bzip2。

bsdiff官網(wǎng)截圖

bsdiff目前支持Linux、Windows,同時(shí)也有Python版本的源碼。

(2)服務(wù)端patch文件的生成

服務(wù)端可以根據(jù)需要,選擇對(duì)應(yīng)的版本進(jìn)行patch文件的生成,比如Windows版本的生成方式如下:


Windows 32位版本文件結(jié)構(gòu)

同時(shí)按住Shift+右鍵,選擇“在此處打開命令窗口”,執(zhí)行命令 bsdiff old.apk new.apk patch.patch即可生成patch包,至于腳本怎么執(zhí)行這些命令,請(qǐng)讀者自行發(fā)揮。

(3)客戶端新apk的合成實(shí)現(xiàn)

點(diǎn)擊(1)中圖片所示的"here"鏈接,下載linux版本的源代碼,同時(shí)下載bzip2的源代碼,文件目錄結(jié)構(gòu)如下:


bsdiff及bzip2的代碼目錄結(jié)構(gòu)

接著將bsdiff.c、bspatch.c文件中的main方法改成diff、patch
然后編寫jni代碼,調(diào)用bsdiff和bspatch的diff、patch方法

#include "jni_bsdiff.h"

#ifdef __cplusplus
extern "C" {
#endif

//定義方法宏,用于拼接方法名
#define JNI_METHOD(METHOD_NAME) \
  Java_com_cy_lib_upgrade_bsdiff_BsDiff_##METHOD_NAME

extern int diff(int argc, char *argv[]);
extern int patch(int argc, char *argv[]);

JNIEXPORT jint JNICALL JNI_METHOD(diff)(JNIEnv *env, jobject object,
                                        jstring old_path, jstring new_path, jstring patch_path) {
    int argc = 4;
    char *argv[argc];
    argv[0] = (char *) "bsdiff";
    argv[1] = (char *) (env)->GetStringUTFChars(old_path, 0);
    argv[2] = (char *) (env)->GetStringUTFChars(new_path, 0);
    argv[3] = (char *) (env)->GetStringUTFChars(patch_path, 0);
    bool isCrash = false;
    int ret;
    try {
        ret = diff(argc, argv);
    }
    catch (...) {
        isCrash = true;
    }
    (env)->ReleaseStringUTFChars(old_path, argv[1]);
    (env)->ReleaseStringUTFChars(new_path, argv[2]);
    (env)->ReleaseStringUTFChars(patch_path, argv[3]);
    return isCrash ? -1 : ret;
}

JNIEXPORT jint JNICALL JNI_METHOD(patch)(JNIEnv *env, jobject object,
                                         jstring old_path, jstring new_path, jstring patch_path) {
    int argc = 4;
    char *argv[argc];
    argv[0] = (char *) "bspatch";
    argv[1] = (char *) (env)->GetStringUTFChars(old_path, 0);
    argv[2] = (char *) (env)->GetStringUTFChars(new_path, 0);
    argv[3] = (char *) (env)->GetStringUTFChars(patch_path, 0);
    bool isCrash = false;
    int ret;
    try {
        ret = patch(argc, argv);
    }
    catch (...) {
        isCrash = true;
    }
    (env)->ReleaseStringUTFChars(old_path, argv[1]);
    (env)->ReleaseStringUTFChars(new_path, argv[2]);
    (env)->ReleaseStringUTFChars(patch_path, argv[3]);
    return isCrash ? -1 : ret;
}

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    // Get jclass with env->FindClass.
    // Register methods with env->RegisterNatives.
    return JNI_VERSION_1_6;
}


#ifdef __cplusplus
}
#endif

接下來,在外層的Android.mk文件中編寫makefile腳本(gradle里面編譯jni我不熟,哈哈哈,還是makefile用著習(xí)慣),將bsdiff、bzip2編譯成靜態(tài)庫(kù),同時(shí)引入子目錄的Android.mk文件。

LOCAL_PATH := $(call my-dir)
#定義子目錄下面的makefile文件列表
SUB_MK_FILES := $(call all-subdir-makefiles)

#----------------------------------------------------
#將bzip2編譯成靜態(tài)庫(kù)
BZIP2_PATH :=$(LOCAL_PATH)/bzip2
BZIP2_C_FILE_LIST :=$(wildcard $(BZIP2_PATH)/*.c)
include $(CLEAR_VARS)
LOCAL_MODULE := bzip2
LOCAL_C_INCLUDES := BZIP2_PATH
LOCAL_SRC_FILES :=$(BZIP2_C_FILE_LIST:$(LOCAL_PATH)/%=%)
include $(BUILD_STATIC_LIBRARY)
#----------------------------------------------------

#----------------------------------------------------
#將bsdiff編譯成靜態(tài)庫(kù)
BSDIFF_PATH :=$(LOCAL_PATH)/bsdiff
BSDIFF_C_FILE_LIST :=$(wildcard $(BSDIFF_PATH)/*.c)
include $(CLEAR_VARS)
LOCAL_MODULE := bsdiff
LOCAL_STATIC_LIBRARIES += bzip2
LOCAL_C_INCLUDES := BSDIFF_PATH
LOCAL_SRC_FILES :=$(BSDIFF_C_FILE_LIST:$(LOCAL_PATH)/%=%)
include $(BUILD_STATIC_LIBRARY)
#----------------------------------------------------

#編譯子目錄下的make file文件
include $(SUB_MK_FILES)

在jni_bsdiff目錄下面的Android.mk文件中編寫生成我們要用的動(dòng)態(tài)庫(kù)的腳本如下

LOCAL_PATH := $(call my-dir)
#----------------------------------------------------
#將bsdiff包裝編譯成動(dòng)態(tài)庫(kù)
JNI_BSDIFF_PATH :=$(LOCAL_PATH)
JNI_BSDIFF_CPP_FILE_LIST :=$(wildcard $(JNI_BSDIFF_PATH)/*.cpp)
include $(CLEAR_VARS)
LOCAL_MODULE := bsdiff_utils
LOCAL_C_INCLUDES := JNI_BSDIFF_PATH

LOCAL_SRC_FILES :=$(JNI_BSDIFF_CPP_FILE_LIST:$(LOCAL_PATH)/%=%)
LOCAL_STATIC_LIBRARIES += bsdiff
include $(BUILD_SHARED_LIBRARY)
#----------------------------------------------------

再接下來,在build.gradle里面編寫編譯腳本即可

    task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
        def ndkDir = project.plugins.getPlugin('com.android.library').sdkHandler.ndkFolder
        print "ndkDir=" + ndkDir + "\n"
        commandLine "$ndkDir\\ndk-build.cmd",
                'NDK_PROJECT_PATH=build/intermediates/ndk',
                'NDK_LIBS_OUT=libs',
                'APP_BUILD_SCRIPT=jni/Android.mk',
                'NDK_APPLICATION_MK=jni/Application.mk'
    }

    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn ndkBuild
    }

如果不出意外我們的libbsdiff_utils.so就可以生成了。然后我們編寫java層的調(diào)用代碼

public class BsDiff {

    static {
        try {
            System.loadLibrary("bsdiff_utils");
        } catch (UnsatisfiedLinkError e) {
            e.printStackTrace();
        }
    }

    public static native int diff(String oldPath, String newPath, String patchPath);

    public static native int patch(String oldPath, String newPath, String patchPath);
}

新apk文件的合成我們要用到的是patch方法,它的參數(shù)oldPath表示當(dāng)前apk的文件路徑,newPath表示合成后的apk文件路徑,patchPath則為下載的增量包的路徑。oldPath的取值,比較穩(wěn)妥的做法是把當(dāng)前安裝的apk文件拷貝到一個(gè)可讀可寫的目錄,防止bspatch對(duì)已安裝的apk文件產(chǎn)生破壞。附上獲取當(dāng)前apk文件的路徑的代碼:

    /**
     * 獲取已安裝apk的路徑
     *
     * @param context apk的上下文
     * @return apk文件路徑
     */
    public static String getApkPath(Context context) {
        if (context != null) {
            ApplicationInfo applicationInfo = context.getApplicationContext().getApplicationInfo();
            return applicationInfo.sourceDir;
        }
        return "";
    }

四、靜默安裝實(shí)現(xiàn)

靜默安裝這里采用pm install命令實(shí)現(xiàn),因此應(yīng)用需要獲取到Root權(quán)限才能執(zhí)行成功。

/**
     * 靜默安裝
     *
     * @param apkFilePath apk文件路徑
     * @return true表示安裝成功,否則返回false
     */
    public static boolean silentInstall(String apkFilePath) {
        boolean isInstallOk = false;
        if (isSupportSilentInstall()) {
            DataOutputStream dataOutputStream = null;
            BufferedReader bufferedReader = null;
            try {
                Process process = Runtime.getRuntime().exec("su");
                dataOutputStream = new DataOutputStream(process.getOutputStream());
                String command = "pm install -r " + apkFilePath + "\n";
                dataOutputStream.write(command.getBytes(Charset.forName("utf-8")));
                dataOutputStream.flush();
                dataOutputStream.writeBytes("exit\n");
                dataOutputStream.flush();
                process.waitFor();
                bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                StringBuilder msg = new StringBuilder();
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    msg.append(line);
                }
                if (msg.toString().contains("Success")) {
                    isInstallOk = true;
                }
            } catch (Exception e) {
            } finally {
                if (dataOutputStream != null) {
                    try {
                        dataOutputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

            }
        }
        return isInstallOk;
    }

五、封裝

為了打造一個(gè)可復(fù)用的軟件更新庫(kù),這里根據(jù)軟件更新的流程抽象了五個(gè)接口,流程與接口的對(duì)應(yīng)關(guān)系如下:

  1. 更新檢測(cè)(UpdateChecker)
  2. 更新檢測(cè)后的UI提示(UpdateCheckUIHandler)
  3. 更新文件下載(Downloader)
  4. 文件下載時(shí)的UI提示(DownloadUIHandler)
  5. 安裝文件(AppInstaller)
    如果使用者發(fā)現(xiàn)哪一步不符合自己的需求,只要實(shí)現(xiàn)這個(gè)步驟的接口并注入到全局配置中即可,從而實(shí)現(xiàn)“萬能”的軟件更新功能。
    具體實(shí)現(xiàn),請(qǐng)參照源碼:https://github.com/Money888/LibUpgrade.git

(1)更新庫(kù)的使用

第一步,在Application.onCreate方法中進(jìn)行初始化

    @Override
    public void onCreate() {
        super.onCreate();
        LibUpgradeInitializer.init(this);
    }

第二步,配置更新庫(kù)功能

      final UpdaterConfiguration config = new UpdaterConfiguration();
        config.updateChecker(new UpdateChecker() {
            @Override
            public void check(UpdateCheckCallback callback) {
                //此處模擬更新信息獲取,信息獲取后需要將UpdateInfo設(shè)置到配置信息中,然后要調(diào)用相應(yīng)的回調(diào)方法才能使整個(gè)流程完整執(zhí)行
                UpdateInfo updateInfo = new UpdateInfo();
                updateInfo.setVersionCode(2);
                updateInfo.setVersionName("v1.2");
                updateInfo.setUpdateTime("2016/10/28");
                updateInfo.setUpdateSize(1024);
                updateInfo.setUpdateInfo("更新日志:\n1.新增萬能更新庫(kù),實(shí)現(xiàn)更新功能只要幾行代碼。");
                //使用全量更新信息
                updateInfo.setUpdateType(UpdateInfo.UpdateType.TOTAL_UPDATE);
                UpdateInfo.TotalUpdateInfo totalUpdateInfo = new UpdateInfo.TotalUpdateInfo();
                totalUpdateInfo.setApkUrl("http://wap.apk.anzhi.com/data2/apk/201609/05/f06abcb0e2cba4c8ce2301c4b437a492_72932500.ap");
                updateInfo.setTotalUpdateInfo(totalUpdateInfo);
                if (updateInfo != null) {
                    //設(shè)置更新信息,這樣各模塊就可以通過config.getUpdateInfo()共享這個(gè)數(shù)據(jù)了,注意這個(gè)方法一定要調(diào)用且要在UpdateCheckCallback.onCheckSuccess之前調(diào)用
                    config.updateInfo(updateInfo);
                    callback.onCheckSuccess();
                } else {
                    callback.onCheckFail("");
                }
            }
        });
        Updater.getInstance().init(config);

第三步,啟用更新檢查功能

 //此處的Context默認(rèn)必須為Activity
 Updater.getInstance().check(this);

(2)自定義功能擴(kuò)展使用

1.增量更新

      config.updateChecker(new UpdateChecker() {
            @Override
            public void check(UpdateCheckCallback callback) {
                UpdateInfo updateInfo = new UpdateInfo();
                //....
                //設(shè)置增量更新信息,設(shè)置完整的apk的MD5及增量包下載地址(此處的增量包需要由bsdiff生成)
                updateInfo.setUpdateType(UpdateInfo.UpdateType.INCREMENTAL_UPDATE);
                UpdateInfo.IncrementalUpdateInfo incrementalUpdateInfo = new UpdateInfo.IncrementalUpdateInfo();
                incrementalUpdateInfo.setFullApkMD5("e7eec01baac70f8a3688570439b9b467");
                incrementalUpdateInfo.setPatchUrl("http://bmob-cdn-4990.b0.upaiyun.com/2016/10/28/aa0bc17f40a91b0b80915a49b40c0174.patch");
                updateInfo.setIncrementalUpdateInfo(incrementalUpdateInfo);
                //.......
            }
        });

2.全量更新

        config.updateChecker(new UpdateChecker() {
            @Override
            public void check(UpdateCheckCallback callback) {
                UpdateInfo updateInfo = new UpdateInfo();
                //....
                //設(shè)置全量更新信息
                updateInfo.setUpdateType(UpdateInfo.UpdateType.TOTAL_UPDATE);
                UpdateInfo.TotalUpdateInfo totalUpdateInfo = new UpdateInfo.TotalUpdateInfo();
                totalUpdateInfo.setApkUrl("http://wap.apk.anzhi.com/data2/apk/201609/05/f06abcb0e2cba4c8ce2301c4b437a492_72932500.apk");
                updateInfo.setTotalUpdateInfo(totalUpdateInfo);
                //.......
            }
        });

3.強(qiáng)制更新

        config.updateChecker(new UpdateChecker() {
            @Override
            public void check(UpdateCheckCallback callback) {
                UpdateInfo updateInfo = new UpdateInfo();
                //....
                //設(shè)置強(qiáng)制更新
                updateInfo.setIsForceInstall(true);
                //.......
            }
        });

4.普通安裝模式

        config.updateChecker(new UpdateChecker() {
            @Override
            public void check(UpdateCheckCallback callback) {
                UpdateInfo updateInfo = new UpdateInfo();
                //....
                //設(shè)置普通模式的安裝
                updateInfo.setInstallType(UpdateInfo.InstallType.NOTIFY_INSTALL);
                //.......
            }
        });

5.靜默安裝模式

      config.updateChecker(new UpdateChecker() {
            @Override
            public void check(UpdateCheckCallback callback) {
                UpdateInfo updateInfo = new UpdateInfo();
                //....
                //設(shè)置靜默安裝模式,設(shè)置此模式前必須確保手機(jī)對(duì)本應(yīng)用授予了Root權(quán)限
                updateInfo.setInstallType(UpdateInfo.InstallType.SILENT_INSTALL);
                //.......
            }
        });

6.修改更新時(shí)的提示UI

        //處理UI時(shí),在必要的時(shí)機(jī)需要調(diào)用config.getDownloader()的相關(guān)方法,才能保證流程正確執(zhí)行
        config.updateUIHandler(new UpdateCheckUIHandler() {
            @Override
            public void setContext(Context context) {
                //此處的context為Updater.getInstance().check(Context context)方法傳入的context
            }

            @Override
            public void hasUpdate() {
                //有更新時(shí)的UI展示
            }

            @Override
            public void noUpdate() {
                //沒有更新時(shí)的UI展示
            }

            @Override
            public void checkError(String error) {
                //更新檢查失敗時(shí)的UI展示
            }
        });

7.修改文件下載時(shí)的UI

        config.downloadUIHandler(new DownloadUIHandler() {
            @Override
            public void setContext(Context context) {
                //此處的context為Updater.getInstance().check(Context context)方法傳入的context
            }

            @Override
            public void downloadStart() {
                //開始下載時(shí)的UI展示
            }

            @Override
            public void downloadProgress(int progress, int total) {
               //下載進(jìn)度的展示
            }

            @Override
            public void downloadComplete(String path) {
              //下載完成時(shí)的處理,此處應(yīng)通過config.getUpdateInfo()獲取更信息,然后再通過相應(yīng)的安裝器進(jìn)行安裝
            }

            @Override
            public void downloadError(String error) {
               //下載失敗時(shí)的UI提示
            }

            @Override
            public void downloadCancel() {
              //下載取消時(shí)的UI提示
            }
        });
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容