轉自:http://blog.csdn.net/lmj623565791/article/details/52761658
本文在我的微信公眾號:鴻洋(hongyangAndroid)首發。
轉載請標明出處:
http://blog.csdn.net/lmj623565791/article/details/52761658;
本文出自:【張鴻洋的博客】
一、概述
最近一直關注熱修復的東西,偶爾聊天談到了增量更新,當然了兩個完全不是一個東西。借此找了一些資料,收集整理了一下,本來是不想寫博客的,因為主要都是工具的實現,但是昨晚在整理資料的時候,忽然發現,我快要忘了這玩意,又要從頭找一圈工具。
So,權當一個記錄,也方便以后自己查找。
首先要明確的是,什么是增量更新:
相信大家都見過在應用市場省流量更新軟件,一個幾百M的軟件可能只需要下載一個20M的增量包就能完成更新。那么它是如何做的呢?
就是本篇博客的主題了。
增量更新的流程是:用戶手機上安裝着某個應用,下載了增量包,手機上的apk和增量包合並形成新的包,然后再次安裝(注意這個過程是要重新安裝的,當然部分應用市場有root權限你可能感知不到)。
ok,那么把整個流程細化為幾個關鍵點:
- 用戶手機上提取當前安裝應用的apk
- 如何利用old.apk和new.apk生成增量文件
- 增加文件與1.中的old.apk合並,然后安裝
解決了上述3個問題,就ok了。
下面開始解決,首先我們看下增量文件的生成與合並,這個環節可以說是整個流程的核心,也是技術難點,值得開心的是,這個技術難點已經有工具替我們實現了。
二、增量文件的生成與合並
這個其實就是利用工具做二進制的一個diff和patch了。
網址:
下載地址:
對了,本文環境為mac,其他系統如果阻礙,慢慢搜索解決即可。
下載好了,解壓,切到對應的目錄,然后執行make:
aaa:bsdiff-4.3 zhy$ make Makefile:13: *** missing separator. Stop.
- 1
- 2
恩,你沒看錯,報錯了,這個錯誤還比較好解決。
解壓文件里面有個文件: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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
然后,重新執行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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
這次比上次好點,這次生成了一個bsdiff,不過在生成bspatch的時候報錯了,好在其實我們只需要使用bsdiff,為什么這么說呢?
因為生成增量文件肯定是在服務端,或者是我們本地pc上做的,使用的就是bsdiff這個工具;
另外一個bspatch,合並old.apk和增量文件肯定是在我們應用內部做的。
當然這個問題也是可以解決的,搜索下,很多解決方案,我們這里就不繼續在這個上面浪費篇幅了。
我這里提供個下載地址:
https://github.com/hymanAndroid/tools/tree/master/bsdiff-4.3
下載完成,直接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
- 1
這樣就生成了一個增量文件old-to-new.patch
- 增量文件和old.apk合並成新的apk
./bspatch old.apk new2.apk old-to-new.patch
- 1
這樣就生成一個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
- 1
- 2
- 3
- 4
可以看到兩個文件的md5果然一樣~~
恩,假設你不是mac,怎么獲取一個文件的md5呢?(自己寫代碼,下載工具,不要遇到這樣的問題,還彈窗我,我會被扣工資的…)
那么到這里我們就已經知道了如何生成增量文件和將patch與舊的文件合並為新的文件。那么我們再次梳理下整個流程:
- 服務端已經做好了增量文件(本節完成)
- 客戶端下載增量文件+提取該應用的apk,使用bspatch合並
- 產生的新的apk,調用安裝程序
還是蠻清晰的,那么主要是第二點,第二點有兩件事,一個是提取應用的apk;一個是使用bspatch合並,那么這個合並肯定是需要native方法和so文件去做的,也就是說我們要自己打個so出來;
三、客戶端的行為
(1)提取應用的apk文件
其實提取當前應用的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; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
(2)制作bspatch so
首先聲明一個類,寫個native方法,如下:
public class BsPatch { static { System.loadLibrary("bsdiff"); } public static native int bspatch(String oldApk, String newApk, String patch); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
三個參數已經很明確了;
同時別忘了在module的build.gradle下面:
defaultConfig { ndk { moduleName = 'bsdiff' } }
- 1
- 2
- 3
- 4
- 5
注意該步驟需要你配置過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; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
方法名是有規律的,這個規律不用提了吧~~
注意bsdiff.c中並沒有patchMethod方法,這個方法實際上是main方法,直接修改為patchMethod即可,覺得復雜沒關系,文末有源碼。
ok,此時你可以嘗試運行,會提示依賴bzlib,其實從文件頂部的include中也能看出來。
既然依賴,那我們就導入吧:
首先下載:
下載完成后,解壓:
將其中的.h和.c文件提取出來,然后可以選擇連文件夾copy到我們module的app/main/jni下,結果如下:
記得修改bsdiff中的include:
#include "bzip2/bzlib.h"
- 1
再次運行;
然后會發現報一堆類似下面的錯誤:
Error:(70) multiple definition of `main'
- 1
提示main方法重復定義了,在出錯信息中會給出哪些類中包含main方法,可以選擇直接將這些類中的main方法直接刪除。
刪除以后,就ok了~~
那么到這里,我們就完成了JNI的編寫,當然文件是bsdiff提供的c源碼。
四、增量更新后安裝
上面的操作完成后,最后一步就簡單了,首先准備兩個apk:
old.apk new.apk
- 1
然后制作一個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()); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
記得開啟讀寫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); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
這里7.0可能會有問題,把路徑暴露給別的app了,應該需要FileProvider去實現(未實驗,猜測可能有可能)。
大致的效果圖如下:
五、總結
如果你只是單純的要使用該功能,大可以直接將生成的so文件拷入,直接loadLibrary使用即可。
其次,在做增量更新的時候,patch肯定是根據你當前的版本號與最新(或者目標)版本apk,比對下發diff文件,於此同時應該也把目標apk的md5下發,再做完合並后,不要忘記校驗下md5;
博客結束,雖然很簡單,主要利用工具實現,但是還是建議自己去實現一次,想一次性跑通還是需要一些時間的,可能過程中也會發現一些坑,也能提升自己對JNI的熟練度。
源碼:
也可以選擇直接使用so
歡迎關注我的微博:
http://weibo.com/u/3165018720