@[增量更新,差分包,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/ ,該算法是開源的,可根據平臺的不同在對應平臺使用源代碼進行編譯集成。
編碼實現
準備工具
-
打開Tools->Android->SDK Manager->SDK Tools選中LLDB和NDK,點擊確認,軟件會自動安裝NDK。見下圖:
enter image description here -
配置環境變量,點擊File->Project Structure打開設置頁面,點擊SDK Location選項卡設置NDK路徑。
image.png
生成差分包
- 編譯bsdiff/patch,Mac環境編譯方法如下:
- 解壓下載的bsdiff-4.3.tar.gz
tar -zxvf bsdiff-4.3.tar.gz - 進入bsdiff-4.3目錄,在終端下執行構建
cd bsdiff-4.3
make
Window/linux平臺可參考這篇文章 增量更新:bsdiff工具的安裝和使用
- bsdiff命令:
- 生成差分包:
命令:bsdiff old.file new.file add.patch ,即old.file是舊的文件,new.file是新更改變化的文件,add.patch是這兩個文件的差異文件(即差分包).
生成差分包需要較多的內存和時間,所幸這些操作只需要在服務器后端執行。 - 舊文件和差分包合成新文件:
命令: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等。