@[增量更新,差分包,bsdiff/patch]
背景
隨著Android app的不斷迭代升級氧骤,功能越來越多叮贩,apk體積也越來越大,雖然當(dāng)前移動網(wǎng)絡(luò)環(huán)境較幾年前有巨大提升话侧,但流量資費依然不便宜栗精,因此每次發(fā)布新版時用戶升級并不是很積極,自從Android4.1開始掂摔,Google引入了應(yīng)用程序的Smart App Update
术羔,即增量更新,增量更新提供了一個更好的方式將更新推送到設(shè)備乙漓,相對于全量更新而言前者只需要將變化的部分推送出去级历,這有助于用戶更快的下載更新、節(jié)省設(shè)備電量消耗叭披,最重要的是有效降低了應(yīng)用升級時消耗的網(wǎng)絡(luò)流量寥殖,國內(nèi)小米、360應(yīng)用市場已經(jīng)使用了該更新機制推出了省流量更新功能涩蜘。
官方說明
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
實現(xiàn)原理
增量更新原理其實比較簡單嚼贡,就是通過差分算法將新舊版本進行對比將有差異的地方抽取出來生成更新補丁patch
,也稱之為差分包同诫≡敛撸客戶端在檢測到更新的時候,只需要將差分包下載到本地误窖,然后通過合成算法將差分包與當(dāng)前應(yīng)用合并叮盘,生成最新安裝包秩贰,在文件校驗通過后執(zhí)行安裝即可。目前主流的差分比較算法是bsdiff/patch
柔吼,來自http://www.daemonology.net/bsdiff/ 毒费,該算法是開源的,可根據(jù)平臺的不同在對應(yīng)平臺使用源代碼進行編譯集成愈魏。
編碼實現(xiàn)
準(zhǔn)備工具
-
打開Tools->Android->SDK Manager->SDK Tools選中LLDB和NDK培漏,點擊確認溪厘,軟件會自動安裝NDK。見下圖:
-
配置環(huán)境變量牌柄,點擊File->Project Structure打開設(shè)置頁面桩匪,點擊SDK Location選項卡設(shè)置NDK路徑。
生成差分包
- 編譯bsdiff/patch友鼻,Mac環(huán)境編譯方法如下:
- 解壓下載的bsdiff-4.3.tar.gz
tar -zxvf bsdiff-4.3.tar.gz - 進入bsdiff-4.3目錄,在終端下執(zhí)行構(gòu)建
cd bsdiff-4.3
make
Window/linux平臺可參考這篇文章 增量更新:bsdiff工具的安裝和使用
- bsdiff命令:
- 生成差分包:
命令:bsdiff old.file new.file add.patch ,即old.file是舊的文件闺骚,new.file是新更改變化的文件彩扔,add.patch是這兩個文件的差異文件(即差分包).
生成差分包需要較多的內(nèi)存和時間,所幸這些操作只需要在服務(wù)器后端執(zhí)行僻爽。 - 舊文件和差分包合成新文件:
命令:bspatch old.file createNew.file add.patch 其中createNew.file是合并后的新文件
合并差分包
- 創(chuàng)建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對應(yīng)路徑下生成PatchUtils.class文件敦捧,打開終端切換到該目錄,輸入命令行javah com.yyh.lib.bsdiff.PatchDroid(包名.類名)
,生成頭文件com_yyh_lib_bsdiff_PatchUtils.h
碰镜。
- 實現(xiàn)Native方法
將上一個步驟生成的頭文件拷貝到工程jni目錄下兢卵,同時解壓bzip2包和bspatch源碼到該目錄下,將bspatch.c重命名為com_yyh_lib_bsdiff_PatchUtils.c
(注意命名方式為包名.類名)绪颖,并在其中實現(xiàn)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目錄下創(chuàng)建Android.mk
文件窃款,寫入以下代碼,其中LOCAL_MODULE
表示SO模塊名稱牍氛,LOCAL_SRC_FILES
表示源文件路徑晨继,用相對路徑即可,不必寫絕對路徑搬俊,具體語法可參考:http://www.cnblogs.com/wainiwann/p/3837936.html紊扬,這里一定要注意加上這句代碼APP_PLATFORM:=android-14
,其中android-14與你工程的minSDKVersion一致即可蜒茄,否則運行在某些低版本設(shè)備上會出現(xiàn)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目錄下創(chuàng)建Application.mk文件珠月,復(fù)制以下代碼:
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 實現(xiàn)(無需命令行)
將差分包與當(dāng)前應(yīng)用合成新包啤挎,注意生產(chǎn)上要注意對差分包驻谆、本地包以及生成后的新包做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命令修改權(quán)限,否則在高版本Android系統(tǒng)上可能會報錯伙判。
private void install(String dir) {
String command = "chmod 777 " + dir;
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec(command); // 可執(zhí)行權(quán)限
} 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);
}
結(jié)語:
使用增量更新方式可以解決往常使用全量更新時安裝包過大的問題象对,但其本身還有以下不足:
- 多版本運營繁瑣,當(dāng)線上存在多個版本時宴抚,要給每個版本分別生成差分包勒魔;
- 使用多渠道包時,要針對每個渠道包分別生成差分包菇曲,造成差分包非常多冠绢,難以維護;
- patch依賴本地版本安裝包完整性常潮,如果本地文件損壞或者被篡改弟胀,就無法增量升級,只能下載全量包進行升級喊式;
- 使用
bs diff/patch
算法生成的差分包體積依然比較大孵户,以同學(xué)會為例,新老包大小約為15M左右岔留,修改少量代碼并生成差分包體積達到了5M左右夏哭,與官方宣稱的差量包體積約為全量包體積的1/3一致,但上述差分算法還有待優(yōu)化的空間贸诚,如果需要對差分算法進行改進可參考HDiffPatch 和 rsync rolling等方庭。