漫談Android 增量更新
在前幾年,整體行動網路環境相比現在差很多,加之流量費用又相對較高,因此每當我們釋出新版本的時候,一些使用者升級並不是很積極,這就造成了新版本的升級率並不高。而google為了解決了這個問題,提出了Smart App Update,即增量更新(也叫做差分升級)。
儘管現在網路環境有了很大的提升,但一個不爭的事實就是應用越做越大,因此,增量更新在目前的仍然是一種解決APP更新包過大的有效方案。今天,我們就來聊聊增量更新。
什麼是增量更新?
增量更新的關鍵在於如何理解增量一詞。來想想平時我們的開發過程,往往都是今天在昨天的基礎上修改一些程式碼,app的更新也是類似的:往往都是在舊版本的app上進行修改。這樣看來,增量更新就是原有app的基礎上只更新發生變化的地方,其餘保持原樣。
與原來每次更新都要下載完整apk包的做法相比,這樣做的好處顯而易見:每次變化的地方總是比較少,因此更新包的體積就會小很多。比如“師父說”安裝包的體積在6m左右,如果不採用增量更新,使用者每次更新都需要下載大約6m左右的安裝包,而採用增量更新這種方案之後每次只需要下載2m左右的更新包即可,相比原來做法大大減少了使用者下載等待的時間。
增量更新的原理
增量更新的原理非常簡單,簡單的說就是通過某種演算法找出新版本和舊版本不一樣的地方(這個過程也叫做差分),然後將不一樣的地方抽取出來形成所謂的更新補丁(patch),也稱之為差分包。客戶端在檢測到更新的時候,只需要下載差分包到本地,然後將差分包合併至本地的安裝包,形成新版本的安裝包,檔案校驗通過後再執行安裝即可。本地的安裝包通過提取當前已安裝應用的apk得到。
演示:差分包的生成與合併
如下圖所示:
現在的問題在於如何生成差分包以及合併差分包。這裡,我們藉助開源庫bsdiff來解決以上兩個問題。首先我們先演示一下差分包的形成與合併。
下載bsdiff_win_exe.zip,解壓到本地。如下圖:
然後,我們先打出一個安裝包,假設為old.apk。對原始碼做修改後,再打出一個新的安裝包new.apk。此處old.apk相當於老版本的應用,而new.apk相當於新版本的應用。接下來,我們利用bsdiff來生成差分包patch.patch。
生成差分包
將上面的old.apk和new.apk放入bsdiff解壓後的目錄,然後在控制檯中執行命令bsdiff old.apk new.apk patch.patch
合併差分包
合併old.apk和patch.patch,生成新的安裝包new.apk。只要此處合併出來的new.apk和上面我們自己打出來的new.apk一樣,那麼就可以認為它就是我們需要的新版本安裝包。
我們來看看如何合併。將old.apk和patch.patch放入bsdiff資料夾,合併之前為:
然後執行命令bspatch old.apk new.apk patch.patch
,稍等一會之後便可以看到合併出的new.apk.如下:
不出意外,合併而來的new.apk應該和我們自己打出來的new.apk是一模一樣的,這可以通過驗證兩者的md5來認定。
我們已經弄明白增量更行是怎麼一回事。下面,我們就以“師父說”為物件進實踐一把。
實踐:讓師父說支援增量更新
客戶端支援增量更新總體和上面的演示差不多,唯一的區別在於客戶端要自行編譯bspatch.c來實現合併差分包,也就是所謂的ndk開發,這裡我們首先要下載bsdiff的原始碼以及bszip的原始碼,以便後面使用。在as中如何進行ndk開發不是本文的重點。
1.編寫BsPatchUtil類
BsPatchUtil中只有一個natvie方法patch(String oldApkPath,String newApkPath,String patchPath)
用於實現增量包的合併:
public class BsPatchUtil {
static {
System.loadLibrary("apkpatch");
}
public static native int patch(String oldApkPath, String newApkPath, String patchPath);
}
2.編寫C程式碼
在實現BsPatchUtil之前,我們需要將bspatch.c以及bzip的相關程式碼拷貝到jni目錄下(bzip只保留.h標頭檔案和.c檔案)。並將bspatch.c中的main()方法名修改為executePatch()
,並且修改其中bzip的引入頭為#include "bzip2/bzlib.h"
.目錄結構如下:
注意:上圖當中的em.c是一個空檔案,用來避免在window下編譯產生的未知錯誤。
接下來我們就可以在bspatch_util.c中實現相關的程式碼了:
//
// Created by God on 2016/10/25.
//
#include "com_closedevice_fastapp_util_BsPatchUtil.h"
JNIEXPORT jint JNICALL Java_com_closedevice_fastapp_util_BsPatchUtil_patch
(JNIEnv *env, jclass clazz, jstring old, jstring new, jstring patch){
int args=4;
char *argv[args];
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));
//此處executePathch()就是上面我們修改出的
int result = executePatch(args, argv);
(*env)->ReleaseStringUTFChars(env, old, argv[1]);
(*env)->ReleaseStringUTFChars(env, new, argv[2]);
(*env)->ReleaseStringUTFChars(env, patch, argv[3]);
return result;
}
至此,大部分工作已經完成了。配置app moudle中的build.gradle中新增ndk配置
defaultConfig {
applicationId "com.closedevice.fastapp"
minSdkVersion 14
targetSdkVersion 24
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//ndk配置
ndk{
moduleName "apkpatch"
abiFilters "armeabi", "armeabi-v7a","x86"
}
}
接下來,我們編譯試試(ndk環境的配置這裡不做說明,自行配置即可),不出意外會遇到以下錯誤:
該問題的解決方法也非常簡單,註釋掉對應檔案的main()方法即可。重新編譯,不出意外沒什麼問題了。接下來,我們就需要在合適的地方合併差分包了。
3.合併差分包
上面的過程做完之後,就可以通過BsPatchUtil.patch()
來合併當前安裝包和差分包了。
這裡,我們假設差分包已經從伺服器下載到本地了。
首先來看如何獲取當前安裝包。我們安裝的應用通常在、data/app下,可以通過一下程式碼獲取其路徑:
public static String getApkInstalledSrc(){
return BaseApplication.context().getApplicationInfo().sourceDir;
}
下面就可以通過BsPatchUtil.patch(String oldApkPath,String newApkPath,String pathPath)
來進行合併了。此處需要注意兩點:
- 合併的地方建議放在外接儲存(SDcard)當中
- 合併的過程比較耗時,需要放到子執行緒中進行。
4.安裝
任何更新包在下載完成後首先要做的就是進行MD5校驗,以便確認該更新包是正規途徑下載而來的。同樣,對於合併之後的更新包,首先要做的事情也是進行MD5校驗,校驗通過之後,再進行安裝:
public static void installAPK(Context context, File file) {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
context.startActivity(intent);
}
到現在,增量更新已經完成。現在可以把增量包以及合併之後的安裝包進行刪除了。
大體程式碼如下:
private void smartupdate() {
Observable.create(new Observable.OnSubscribe<File>() {
@Override
public void call(Subscriber<? super File> subscriber) {
//定義生成的新包
File newApk = new File(Environment.getExternalStorageDirectory(), "newApk.apk");
//假設patch.patch檔案已經下載到sdcard上,切已經校驗通過
File patch = new File(Environment.getExternalStorageDirectory(), "patch.patch");
if(!patch.exists()){
subscriber.onError(new IOException("patch file not exist!"));
return;
//合併差分包
BsPatchUtil.patch(OSUtil.getApkInstalledSrc(), newApk.getAbsolutePath(), patch.getAbsolutePath());
if (newApk.exists()) {
subscriber.onNext(newApk);
subscriber.onCompleted();
patch.delete();
}else{
subscriber.onError(new IOException("bspatch failed,file not exist!"));
}
}
}).subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(new Action0() {
@Override
public void call() {
showDialog("正在應用差分包");
}
})
.subscribe(new Subscriber<File>() {
@Override
public void onCompleted() {
hideDialog();
}
@Override
public void onError(Throwable e) {
hideDialog();
LogUtils.d(e.getMessage());
}
@Override
public void onNext(File file) {
OSUtil.installAPK(getActivity(),file);
}
});
}
增量更新的缺點
增量更新雖讓有效的解決了更新包過大的問題,但是存在以下幾點問題:
- 客戶端和服務端需要加入相應的支援。每次釋出新版本,服務端都需要為以前所有的老版本生成對應的差分包,並根據客戶端端請求返回對應的更新包,維護過程將會變得相對複雜。客戶端需要對差分包做更為詳細的驗證,防止出錯,除此之外,客戶端應該可以根據服務端更新開關來確定當前是使用完整更新還是增量更新。
- apk包之間的差異過小時,比如2m以下,此時生成的差分包仍然有幾百k,此時使用增量更新得不償失,畢竟形成差分包和合並的過程都非常耗時。另外,但版本之間變化非常大的時候,通常是是大版本好變化的時候,比如從v 1.0.0到2.0.0,此時使用完整更新也不錯。
在師父說中已經新增主要程式碼,可自行練習。效果如下: