最近一直關注熱修復的東西,偶爾聊天談到了增量更新,固然了兩個徹底不是一個東西。藉此找了一些資料,收集整理了一下,原本是不想寫博客的,由於主要都是工具的實現,可是昨晚在整理資料的時候,突然發現,我快要忘了這玩意,又要從頭找一圈工具。javascript
So,權當一個記錄,也方便之後本身查找。html
首先要明確的是,什麼是增量更新:java
相信你們都見過在應用市場省流量更新軟件,一個幾百M的軟件可能只須要下載一個20M的增量包就能完成更新。那麼它是如何作的呢?android
就是本篇博客的主題了。git
增量更新的流程是:用戶手機上安裝着某個應用,下載了增量包,手機上的apk和增量包合併造成新的包,而後再次安裝(注意這個過程是要從新安裝的,固然部分應用市場有root權限你可能感知不到)。github
ok,那麼把整個流程細化爲幾個關鍵點:微信
解決了上述3個問題,就ok了。app
下面開始解決,首先咱們看下增量文件的生成與合併,這個環節能夠說是整個流程的核心,也是技術難點,值得開心的是,這個技術難點已經有工具替咱們實現了。ide
這個其實就是利用工具作二進制的一個diff和patch了。工具
網址:
下載地址:
對了,本文環境爲mac,其餘系統若是阻礙,慢慢搜索解決便可。
下載好了,解壓,切到對應的目錄,而後執行make:
aaa:bsdiff-4.3 zhy$ make
Makefile:13: *** missing separator. Stop.複製代碼
恩,你沒看錯,報錯了,這個錯誤還比較好解決。
解壓文件裏面有個文件:Makefile,以文本的形式打開,將install:下面的if,endif添加一個縮進。
修改完成是這個樣子的:
CFLAGS += -O3 -lbz2
PREFIX ?= /usr/local
INSTALL_PROGRAM ?= ${INSTALL} -c -s -m 555
INSTALL_MAN ?= ${INSTALL} -c -m 444
all: bsdiff bspatch
bsdiff: bsdiff.c
bspatch: bspatch.c
install:
${INSTALL_PROGRAM} bsdiff bspatch ${PREFIX}/bin
.ifndef WITHOUT_MAN
${INSTALL_MAN} bsdiff.1 bspatch.1 ${PREFIX}/man/man1
.endif複製代碼
而後,從新執行make:
aaa:bsdiff-4.3 zhy$ make
cc -O3 -lbz2 bsdiff.c -o bsdiff
cc -O3 -lbz2 bspatch.c -o bspatch
bspatch.c:39:21: error: unknown type name 'u_char'; did you mean 'char'?
static off_t offtin(u_char *buf)
^~~~~~
char複製代碼
此次比上次好點,此次生成了一個bsdiff,不過在生成bspatch的時候報錯了,好在其實咱們只須要使用bsdiff,爲何這麼說呢?
由於生成增量文件確定是在服務端,或者是咱們本地pc上作的,使用的就是bsdiff這個工具;
另一個bspatch,合併old.apk和增量文件確定是在咱們應用內部作的。
固然這個問題也是能夠解決的,搜索下,不少解決方案,咱們這裏就不繼續在這個上面浪費篇幅了。
我這裏提供個下載地址:
下載完成,直接make,bsdiff和bspatch都會生成(mac環境下)。
=============神奇的分割線==============
ok,假設到這裏,無論你使用何種手段,我們已經有了bsdiff和bspacth,下面演示下這個工具的使用:
首先咱們準備兩個apk,old.apk和new.apk,你能夠本身隨便寫個項目,先運行一次拿到生成的apk做爲old.apk;而後修改些代碼,或者加一些功能,再運行一次生成new.apk;
./bsdiff old.apk new.apk old-to-new.patch複製代碼
這樣就生成了一個增量文件old-to-new.patch
./bspatch old.apk new2.apk old-to-new.patch複製代碼
這樣就生成一個new2.apk
那麼怎麼證實這個生成的new2.apk和咱們的new.apk如出一轍呢?
咱們能夠查看下md5的值,若是兩個文件md5值一致,那麼幾乎能夠確定兩個文件時如出一轍的(不要跟我較真說什麼碰撞能夠產生同樣的md5的值~~)。
aaa:bsdiff-4.3 zhy$ md5 new.apk
MD5 (new.apk) = 0900d0d65f49a0cc3b472e14da11bde7
aaa:bsdiff-4.3 zhy$ md5 new2.apk
MD5 (new2.apk) = 0900d0d65f49a0cc3b472e14da11bde7複製代碼
能夠看到兩個文件的md5果真同樣~~
恩,假設你不是mac,怎麼獲取一個文件的md5呢?(本身寫代碼,下載工具,不要遇到這樣的問題,還彈窗我,我會被扣工資的...)
那麼到這裏咱們就已經知道了如何生成增量文件和將patch與舊的文件合併爲新的文件。那麼咱們再次梳理下整個流程:
仍是蠻清晰的,那麼主要是第二點,第二點有兩件事,一個是提取應用的apk;一個是使用bspatch合併,那麼這個合併確定是須要native方法和so文件去作的,也就是說咱們要本身打個so出來;
其實提取當前應用的apk很是簡單,以下代碼:
public class ApkExtract {
public static String extract(Context context) {
context = context.getApplicationContext();
ApplicationInfo applicationInfo = context.getApplicationInfo();
String apkPath = applicationInfo.sourceDir;
Log.d("hongyang", apkPath);
return apkPath;
}
}複製代碼
首先聲明一個類,寫個native方法,以下:
public class BsPatch {
static {
System.loadLibrary("bsdiff");
}
public static native int bspatch(String oldApk, String newApk, String patch);
}複製代碼
三個參數已經很明確了;
同時別忘了在module的build.gradle下面:
defaultConfig {
ndk {
moduleName = 'bsdiff'
}
}複製代碼
注意該步驟須要你配置過ndk的環境(下載ndk,設置ndk.dir)~
ok,接下來就是去完成c的代碼的編寫了;
首先在app/main目錄下新建一個文件夾jni,把以前下載的bsdiff中的bspatch.c拷貝進去;
而後按照jni的規則,在裏面新建一個方法:
JNIEXPORT jint JNICALL Java_com_zhy_utils_BsPatch_bspatch
(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));
int ret = patchMethod(argc, argv);
(*env)->ReleaseStringUTFChars(env, old, argv[1]);
(*env)->ReleaseStringUTFChars(env, new, argv[2]);
(*env)->ReleaseStringUTFChars(env, patch, argv[3]);
return ret;
}複製代碼
方法名是有規律的,這個規律不用提了吧~~
注意bsdiff.c中並無patchMethod方法,這個方法其實是main方法,直接修改成patchMethod便可,以爲複雜不要緊,文末有源碼。
ok,此時你能夠嘗試運行,會提示依賴bzlib,其實從文件頂部的include中也能看出來。
既然依賴,那咱們就導入吧:
首先下載:
下載完成後,解壓:
將其中的.h和.c文件提取出來,而後能夠選擇連文件夾copy到咱們module的app/main/jni下,結果以下:
記得修改bsdiff中的include:
#include "bzip2/bzlib.h"複製代碼
再次運行;
而後會發現報一堆相似下面的錯誤:
Error:(70) multiple definition of `main'複製代碼
提示main方法重複定義了,在出錯信息中會給出哪些類中包含main方法,能夠選擇直接將這些類中的main方法直接刪除。
刪除之後,就ok了~~
那麼到這裏,咱們就完成了JNI的編寫,固然文件時bsdiff提供的c源碼。
上面的操做完成後,最後一步就簡單了,首先準備兩個apk:
old.apk new.apk複製代碼
而後製做一個patch,下面代碼中的PATCH.patch;
將old.apk安裝,而後將new.apk以及PATCH.patch放置到存儲卡;
最後在Activity中觸發調用:
private void doBspatch() {
final File destApk = new File(Environment.getExternalStorageDirectory(), "dest.apk");
final File patch = new File(Environment.getExternalStorageDirectory(), "PATCH.patch");
//必定要檢查文件都存在
BsPatch.bspatch(ApkExtract.extract(this),
destApk.getAbsolutePath(),
patch.getAbsolutePath());
if (destApk.exists())
ApkExtract.install(this, destApk.getAbsolutePath());
}複製代碼
記得開啓讀寫SDCard權限,記得在代碼中校驗須要的文件都存在。
install實際就是經過Intent去安裝了:
public static void install(Context context, String apkPath) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setDataAndType(Uri.fromFile(new File(apkPath)),
"application/vnd.android.package-archive");
context.startActivity(i);
}複製代碼
這裏7.0可能會有問題,把路徑暴露給別的app了,應該須要FileProvider去實現(未實驗,猜想可能有可能)。
大體的效果圖以下:
若是你只是單純的要使用該功能,大能夠直接將生成的so文件拷入,直接loadLibrary使用便可。
其次,在作增量更新的時候,patch確定是根據你當前的版本號與最新(或者目標)版本apk,比對下發diff文件,於此同時應該也把目標apk的md5下發,再作完合併後,不要忘記校驗下md5;
博客結束,雖然很簡單,主要利用工具實現,可是仍是建議本身去實現一次,想一次性跑通仍是須要一些時間的,可能過程當中也會發現一些坑,也能提高本身對JNI的熟練度。
源碼:
也能夠選擇直接使用so
歡迎關注個人微信公衆號:hongyangAndroid
(歡迎關注,不要錯過每一篇乾貨,支持投稿)