Android應用增量更新/升級方案

@[增量更新,差分包,bsdiff/patch]

背景

隨著Android app的不斷迭代升級,功能越來越多,apk體積也越來越大,雖然當前移動網絡環境較幾年前有巨大提升,但流量資費依然不便宜,因此每次發布新版時用戶升級并不是很積極,自從Android4.1開始,Google引入了應用程序的Smart App Update,即增量更新,增量更新提供了一個更好的方式將更新推送到設備,相對于全量更新而言前者只需要將變化的部分推送出去,這有助于用戶更快的下載更新、節省設備電量消耗,最重要的是有效降低了應用升級時消耗的網絡流量,國內小米、360應用市場已經使用了該更新機制推出了省流量更新功能。


官方說明

Smart app updates is a new feature of Google Play that introduces a better way of delivering app updates to devices. When developers publish an update, Google Play now delivers only the bits that have changed to devices, rather than the entire APK. This makes the updates much lighter-weight in most cases, so they are faster to download, save the device’s battery, and conserve bandwidth usage on users’ mobile data plan. On average, a smart app update is about 1/3 the sizeof a full APK update.
http://developer.android.com/about/versions/jelly-bean.html


實現原理

增量更新原理其實比較簡單,就是通過差分算法將新舊版本進行對比將有差異的地方抽取出來生成更新補丁patch,也稱之為差分包。客戶端在檢測到更新的時候,只需要將差分包下載到本地,然后通過合成算法將差分包與當前應用合并,生成最新安裝包,在文件校驗通過后執行安裝即可。目前主流的差分比較算法是bsdiff/patch,來自http://www.daemonology.net/bsdiff/ ,該算法是開源的,可根據平臺的不同在對應平臺使用源代碼進行編譯集成。


編碼實現

準備工具

  • bsdiff/patch源碼(點擊下載)
  • 由于bsdiff/patch依賴bzip2庫,因此還需要下載bzip2。(點擊下載)
  • Android studio配置NDK環境
  1. 打開Tools->Android->SDK Manager->SDK Tools選中LLDB和NDK,點擊確認,軟件會自動安裝NDK。見下圖:


    enter image description here
  2. 配置環境變量,點擊File->Project Structure打開設置頁面,點擊SDK Location選項卡設置NDK路徑。


    image.png

生成差分包

  • 編譯bsdiff/patch,Mac環境編譯方法如下:
  1. 解壓下載的bsdiff-4.3.tar.gz
    tar -zxvf bsdiff-4.3.tar.gz
  2. 進入bsdiff-4.3目錄,在終端下執行構建
    cd bsdiff-4.3
    make
    Window/linux平臺可參考這篇文章 增量更新:bsdiff工具的安裝和使用
  • bsdiff命令:
  1. 生成差分包:
    命令:bsdiff old.file new.file add.patch ,即old.file是舊的文件,new.file是新更改變化的文件,add.patch是這兩個文件的差異文件(即差分包).
    生成差分包需要較多的內存和時間,所幸這些操作只需要在服務器后端執行。
  2. 舊文件和差分包合成新文件:
    命令:bspatch old.file createNew.file add.patch 其中createNew.file是合并后的新文件

合并差分包

  • 創建Native方法類
 public class PatchUtils {

    static PatchUtils instance;

    public static PatchUtils getInstance() {
        if (instance == null)
            instance = new PatchUtils();
        return instance;
    }

    static {
        System.loadLibrary("ApkPatchLibrary");
    }

    /**
     * native方法 使用路徑為oldApkPath的apk與路徑為patchPath的補丁包,合成新的apk,并存儲于newApkPath
     * 
     * 返回:0,說明操作成功
     * 
     * @param oldApkPath
     *            示例:/sdcard/old.apk
     * @param newApkPath
     *            示例:/sdcard/new.apk
     * @param patchPath
     *            示例:/sdcard/xx.patch
     * @return
     */
    public native int patch(String oldApkPath, String newApkPath, String patchPath);
}

編譯之后在工程build/intermediates/classes對應路徑下生成PatchUtils.class文件,打開終端切換到該目錄,輸入命令行javah com.yyh.lib.bsdiff.PatchDroid(包名.類名),生成頭文件com_yyh_lib_bsdiff_PatchUtils.h

  • 實現Native方法
    將上一個步驟生成的頭文件拷貝到工程jni目錄下,同時解壓bzip2包和bspatch源碼到該目錄下,將bspatch.c重命名為com_yyh_lib_bsdiff_PatchUtils.c(注意命名方式為包名.類名),并在其中實現Java_com_yyh_lib_bsdiff_PatchUtils_patch方法,注意方法名一定要包含Native方法類所在的包名絕對路徑,包名可以自定義。

JNIEXPORT jint JNICALL Java_com_yyh_lib_bsdiff_PatchUtils_patch
  (JNIEnv *env, jclass cls,
            jstring old, jstring new, jstring patch){
    int argc = 4;
    char * argv[argc];
    argv[0] = "bspatch";
    argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
    argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
    argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));

    printf("old apk = %s \n", argv[1]);
    printf("patch = %s \n", argv[3]);
    printf("new apk = %s \n", argv[2]);

    int ret = applypatch(argc, argv);

    printf("patch result = %d ", ret);

    (*env)->ReleaseStringUTFChars(env, old, argv[1]);
    (*env)->ReleaseStringUTFChars(env, new, argv[2]);
    (*env)->ReleaseStringUTFChars(env, patch, argv[3]);
    return ret;
}

編譯SO模塊

在jni目錄下創建Android.mk文件,寫入以下代碼,其中LOCAL_MODULE表示SO模塊名稱,LOCAL_SRC_FILES表示源文件路徑,用相對路徑即可,不必寫絕對路徑,具體語法可參考:http://www.cnblogs.com/wainiwann/p/3837936.html,這里一定要注意加上這句代碼APP_PLATFORM:=android-14,其中android-14與你工程的minSDKVersion一致即可,否則運行在某些低版本設備上會出現java.lang.UnsatisfiedLinkError錯誤。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := ApkPatchLibrary
LOCAL_LDFLAGS := -Wl,--build-id
LOCAL_SRC_FILES := \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/com_yyh_lib_bsdiff_DiffUtils.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/com_yyh_lib_bsdiff_PatchUtils.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/blocksort.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/bzip2.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/bzip2recover.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/bzlib.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/compress.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/crctable.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/decompress.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/huffman.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/randtable.c \
    /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni/bzip2/readMe.txt \

LOCAL_C_INCLUDES += /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/main/jni
LOCAL_C_INCLUDES += /Users/xiayang075/Documents/項目/IncrementallyUpdate/app/src/debug/jni

include $(BUILD_SHARED_LIBRARY)
APP_PLATFORM:=android-14

在jni目錄下創建Application.mk文件,復制以下代碼:

APP_MODULES := libApkPatchLibrary (lib+so文件名)
APP_ABI := all

修改app module下的build.gradle文件,如下:

    ndk{
        moduleName "ApkPatchLibrary"
    }
    sourceSets {
        main {
            jni.srcDirs = [] //禁用gradle編譯jni
            jniLibs.srcDirs = ['libs'] // libs為so文件所在包路徑
        }
    }

推薦參考以下文章編譯NDK,超級簡單的Android Studio jni 實現(無需命令行)

將差分包與當前應用合成新包,注意生產上要注意對差分包、本地包以及生成后的新包做MD5文件校驗,防止文件被篡改,確保最后生成新包的MD5值與全量包一致。

    private class PatchTask extends AsyncTask<String, Void, Integer> {

        @Override
        protected Integer doInBackground(String... params) {

            try {

                int result = PatchUtils.getInstance().patch(srcDir, destDir2, patchDir);
                if (result == 0) {
                    handler.obtainMessage(4).sendToTarget();
                    return WHAT_SUCCESS;
                } else {
                    handler.obtainMessage(5).sendToTarget();
                    return WHAT_FAIL_PATCH;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return WHAT_FAIL_PATCH;
        }

        @Override
        protected void onPostExecute(Integer integer) {
            super.onPostExecute(integer);
            loadding.setVisibility(View.GONE);
        }
    }

安裝新包

注意使用chmod命令修改權限,否則在高版本Android系統上可能會報錯。

    private void install(String dir) {
        String command = "chmod 777 " + dir;
        Runtime runtime = Runtime.getRuntime();
        try {
            runtime.exec(command); // 可執行權限
        } catch (IOException e) {
            e.printStackTrace();
        }

        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(Uri.parse("file://" + dir), "application/vnd.android.package-archive");
        startActivity(intent);
    }

結語:

使用增量更新方式可以解決往常使用全量更新時安裝包過大的問題,但其本身還有以下不足:

  • 多版本運營繁瑣,當線上存在多個版本時,要給每個版本分別生成差分包;
  • 使用多渠道包時,要針對每個渠道包分別生成差分包,造成差分包非常多,難以維護;
  • patch依賴本地版本安裝包完整性,如果本地文件損壞或者被篡改,就無法增量升級,只能下載全量包進行升級;
  • 使用bs diff/patch算法生成的差分包體積依然比較大,以同學會為例,新老包大小約為15M左右,修改少量代碼并生成差分包體積達到了5M左右,與官方宣稱的差量包體積約為全量包體積的1/3一致,但上述差分算法還有待優化的空間,如果需要對差分算法進行改進可參考HDiffPatch 和 rsync rolling等。

參考:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,662評論 25 708
  • 在前幾年,整體移動網絡環境相比現在差很多,加之流量費用又相對較高,因此每當我們發布新版本的時候,一些用戶升級并不是...
    涅槃1992閱讀 5,507評論 2 39
  • 1.概述 1.1.什么是應用增量更新 當我們要更新一個應用的時候,以前很多更新的做法是下載一個新版本去覆蓋一個舊版...
    揚靈閱讀 3,215評論 8 19
  • 風在四處的游走 一直捕風捉影般告白自己 我游走在城市鄉村山野 尋覓并且沉思 成長的痛是煉獄的魔鬼 直到體無完膚遍體...
    營州布衣閱讀 144評論 1 5
  • 消息中間件是目前互聯網服務常用的技術服務。消息中間件為應用系統提供高效、靈活的消息同步和異步傳輸處理、存儲轉發、可...
    王帥199207閱讀 522評論 0 2