demo地址
https://github.com/po1arbear/bsdiff-android
已驗(yàn)證過(guò)與tinker的兼容性,支持manifest修改牡直,支持activity新增纳决,如有其它風(fēng)險(xiǎn)和隱藏漏洞,歡迎告知 ^ ^
最近apk的更新有些頻繁饵史,各個(gè)系統(tǒng)發(fā)版都要向用戶推送一波更新约急,每次都要全量下載苗分,流量消耗大,用戶等待時(shí)間長(zhǎng)奴饮,打開(kāi)小米應(yīng)用商店择浊,發(fā)現(xiàn)大部分app更新包都比實(shí)際的包要小琢岩,于是研究了一下,發(fā)現(xiàn)是使用的增量更新担孔,了解了其原理并運(yùn)用到項(xiàng)目中實(shí)踐
一糕篇、 什么是增量更新?
首先需要明確挑豌,Android增量更新與熱修復(fù)是不同的技術(shù)概念。
熱修復(fù)一般是用于當(dāng)已經(jīng)發(fā)布的app有Bug需要修復(fù)的時(shí)候侯勉,開(kāi)發(fā)者修改代碼并發(fā)布補(bǔ)丁壳鹤,讓應(yīng)用能夠在不需要重新安裝的情況下實(shí)現(xiàn)更新饰迹,主流方案有Tinker啊鸭、AndFix等匿值。
而增量更新的目的是為了減少更新app所需要下載的包體積大小,常見(jiàn)如手機(jī)端游戲钟些,apk包體積為幾百M(fèi)政恍,但有時(shí)更新只需下載十幾M的安裝包即可完成更新达传。
二、增量更新原理
自從 Android 4.1 開(kāi)始宗弯, Google Play 引入了應(yīng)用程序的增量更新功能蒙保,App使用該升級(jí)方式,可節(jié)省約2/3的流量邓厕。
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.
三邑狸、應(yīng)用市場(chǎng)現(xiàn)狀
筆者使用的小米手機(jī)涤妒,可以看到,小米的應(yīng)用商店已經(jīng)開(kāi)始支持增量更新屿储,會(huì)比原有的方式節(jié)省超過(guò)一半的流量
四渐逃、實(shí)現(xiàn)方案
- 服務(wù)端
服務(wù)端的同學(xué)拿到客戶端同學(xué)開(kāi)發(fā)的新版本A茄菊,跟已發(fā)布的舊版本B1,B2竖哩,B3...做了差分生成相應(yīng)的差分包C1脊僚,C2辽幌,C3...,并生成相應(yīng)差分包的MD5值
- 客戶端
客戶端用版本號(hào)作為參數(shù)向服務(wù)端請(qǐng)求更新數(shù)據(jù)虑润,若服務(wù)端沒(méi)有差分包或者差分包大小比全量包大時(shí)逛犹,則返回全量包下載URL虽画、MD5值
若服務(wù)端存在相應(yīng)的差分包則返回差分包下載URL,全量包和差分包MD5值渗柿,全量包簽名值和MD5值朵栖。把差分包下載到本地之后(C1)柴梆,先做MD5值校驗(yàn),確保下載的差分包數(shù)據(jù)的完整性门扇,校驗(yàn)失敗則走全量更新邏輯,校驗(yàn)無(wú)誤和本地現(xiàn)有安裝的舊版本(B1)進(jìn)行差分合并生成新版本(A)霸奕,之后進(jìn)行合成版本的MD5值校驗(yàn)和簽名校驗(yàn)质帅,確保合成文件的完整性和簽名信息的正確性留攒。校驗(yàn)無(wú)誤后再進(jìn)行安裝。
五盟庞、操作步驟
macOS操作驗(yàn)證
- 安裝
brew install bsdiff
- 準(zhǔn)備oldfile和newfie
- 生成差量文件
bsdiff oldfile newfile patchfile
- 合成新包
bspatch oldfile newfile patchfile
Android上的實(shí)現(xiàn)
因?yàn)椴盍堪菑慕涌讷@取的,所以客戶端只需要處理bspatch的過(guò)程票彪,合成新的apk文件然后安裝即可
1.bsdiff下載
bsdiff下載后降铸,解壓bsdiff-4.3.tar.gz推掸,取出目錄中的bspatch.c文件,我們要用的就是這個(gè)文件中的bspatch_main方法登渣。
bzip2下載
取出文件blocksort.c胜茧,bzip2.c仇味,bzlib.c丹墨,bzlib.h贩挣,bzlib_private.h没酣,compress.c四康,crctable.c闪金,decompress.c论颅,huffman.c恃疯,randtable.c今妄,因?yàn)閎sdiff的編譯需要依賴bzip2,所以需要這些c文件犬性。將bspatch.c以及bzip的相關(guān)代碼拷貝到j(luò)ni目錄下
-
編寫update-lib.cpp
#include <jni.h> #include <string> #include <android/log.h> #include <exception> #include "patchUtils.h" extern "C" JNIEXPORT jint JNICALL Java_com_fcbox_hivebox_update_PatchUtils_patch(JNIEnv *env, jclass type, jstring oldApkPath_, jstring newApkPath_, jstring patchPath_) { int argc = 4; char *ch[argc]; ch[0] = (char *) "bspatch"; ch[1] = const_cast<char *>(env->GetStringUTFChars(oldApkPath_, 0)); ch[2] = const_cast<char *>(env->GetStringUTFChars(newApkPath_, 0)); ch[3] = const_cast<char *>(env->GetStringUTFChars(patchPath_, 0)); int ret = applypatch(argc, ch); __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "applypatch result = %d ", ret); env->ReleaseStringUTFChars(oldApkPath_, ch[1]); env->ReleaseStringUTFChars(newApkPath_, ch[2]); env->ReleaseStringUTFChars(patchPath_, ch[3]); return ret; }
編寫PatchUtils.java
public class PatchUtils {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("update-lib");
}
/**
* native方法 使用路徑為oldApkPath的apk與路徑為patchPath的補(bǔ)丁包,合成新的apk鹤耍,并存儲(chǔ)于newApkPath
*
* 返回:0稿黄,說(shuō)明操作成功
*
* @param oldApkPath 示例:/sdcard/old.apk
* @param outputApkPath 示例:/sdcard/output.apk
* @param patchPath 示例:/sdcard/xx.patch
* @return
*/
public static native int patch(String oldApkPath, String outputApkPath,
String patchPath);
}
- 調(diào)用bspatch生成新的apk
private void genNewApk() {
String oldpath = getApplicationInfo().sourceDir;
String newpath = (Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
+ "composed_hivebox_apk.apk");
String patchpath = (Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
+ "bs_patch");
PatchUtils.patch(oldpath, newpath, patchpath);
}
五抛猖、與Tinker的差異
首先簡(jiǎn)單了解下Dex文件财著,大家在反編譯的時(shí)候撑碴,都清楚apk中會(huì)包含一個(gè)或者多個(gè)*.dex文件,該文件中存儲(chǔ)了我們編寫的代碼伟姐,一般情況下我們還會(huì)通過(guò)工具轉(zhuǎn)化為jar,然后通過(guò)一些工具反編譯查看愤兵。
jar文件大家應(yīng)該都清楚鹿霸,類似于class文件的壓縮包,一般情況下秆乳,我們直接解壓就可以看到一個(gè)個(gè)class文件懦鼠。而dex文件我們無(wú)法通過(guò)解壓獲取內(nèi)部的一個(gè)個(gè)class文件,說(shuō)明dex文件擁有自己特定的格式:
dex對(duì)JAVA類文件重新排列屹堰,將所有JAVA類文件中的常量池分解肛冶,消除其中的冗余信息,重新組合形成一個(gè)常量池扯键,所有的類文件共享同一個(gè)常量池睦袖,使得相同的字符串、常量在DEX文件中只出現(xiàn)一次荣刑,從而減小了文件的體積馅笙。
微信通過(guò)深入Dex格式延蟹,實(shí)現(xiàn)一套diff差異小斥杜,內(nèi)存占用少以及支持增刪改的算法
它格式無(wú)關(guān)忘渔,但對(duì)Dex效果不是特別好畦粮,當(dāng)前微信對(duì)于so與部分資源,依然使用bsdiff算法
核心思想:
- 將舊文件二進(jìn)制使用后綴排序或哈希算法形成一個(gè)字符串索引。
- 使用該字符串索引對(duì)比新文件钩蚊,生成差異文件(difference file)和新增文件(extra file)鸣驱。
- 將差異文件和新增文件及必要的索引控制信息壓縮為差異更新包踊东。