咱們知道隨着功能不斷增長,apk 的體積也會不斷增大。若是每次更新都須要用戶下載全新的 apk 覆蓋用戶手機老的版本的話,會浪費用戶的流量,也會增長服務器帶寬。要想實現此需求的話,就須要瞭解一下 bisdiff/bspatch 。顧名思義,diff 就是經過算法計算兩個文件獲得差別包,patch 就是補丁(通過 diff 後的差別包),能夠經過 patch 將源文件和補丁文件組合成新的文件,使得用戶無需下載全新的文件,而只下載補丁文件就可獲得新的文件啦!java
補丁文件應該放在服務器端使用,用戶端經過正常的更新方式去下載補丁文件。下面的實現方案都在本地,不模擬從服務器下載補丁文件的流程。筆者是 Mac OS,讀者可使用 linux 系統來測試,好比 Ubuntu、Centos 等等。linux
下載連接 bsdiff/bspatch,當前下載的版本是 4.3。下載到本地後解壓可看到以下文件:android
能夠看到其實就只有兩個 C 源文件,還提供了 Makefile 文件,既然提供了 Makefile 源文件,那麼咱們就能夠執行 make 命令。如圖所示:git
這是由於 Makefile 文件中,命令前面沒有使用 tab 鍵,這個是 makefile 的語法規則。如圖所示:github
修改後在執行就能夠看到生成了可執行文件 bspatch、bsdiff算法
固然若是是 Mac 系統,你可能還會遇到一個報錯,找不到 u_char。這個時候須要在 bspatch.c 中加入shell
#ifdef __APPLE__
#include <sys/types.h>
#endif
複製代碼
到此環境就已經配置完畢,咱們接下來看 Android 如何實現。數組
#include <bzlib.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <err.h>
#include <unistd.h>
#include <fcntl.h>
.... 省略...
複製代碼
咱們都知道通常狀況下尖括號<> 都是系統的頭文件,可是這裏有個特殊的地方, 就是 #include <bzlib.h>, 這個頭文件系統並不存在,而須要咱們引入它的源碼。它的源碼下載地址 bzip2, 直接搜索關鍵字 bzip2 而後選擇,如圖所示:bash
將其解壓後的文件下圖所示:服務器
能夠看到有不少文件,既有 C 源文件,也有一些其餘文件,固然咱們這裏只關心 C 源文件。但是也發現有不少文件,一個辦法就是將其所有拷入,另外一種方式就是查看 Makefile 文件,看看其如何構建的。OK, 那咱們就來看看 Makefile
SHELL=/bin/sh
# To assist in cross-compiling
# 交叉編譯相關的工具
CC=gcc
AR=ar
RANLIB=ranlib
LDFLAGS=
BIGFILES=-D_FILE_OFFSET_BITS=64
# 傳遞給編譯器的指令
CFLAGS=-Wall -Winline -O2 -g $(BIGFILES)
# Where you want it installed when you do 'make install'
# 將其安裝到 /url/local 下
PREFIX=/usr/local
# OBJS 變量,這裏是關鍵,能夠看到 ***.o 文件其實就是經過 .c 源文件編譯獲得,這裏他們就會看成一個個目標來用。
OBJS= blocksort.o \
huffman.o \
crctable.o \
randtable.o \
compress.o \
decompress.o \
bzlib.o
# 通常狀況下,開源項目都會在 Makefile 中提供 all 目標,它告訴須要那些目標來構建最終的可執行文件
all: libbz2.a bzip2 bzip2recover test
# bzip2 目標依賴 libbz2.a bzip2.o
bzip2: libbz2.a bzip2.o
# 這裏就調用了 CC 編譯器以及指定一些參數,還有連接庫
$(CC) $(CFLAGS) $(LDFLAGS) -o bzip2 bzip2.o -L. -lbz2
bzip2recover: bzip2recover.o
$(CC) $(CFLAGS) $(LDFLAGS) -o bzip2recover bzip2recover.o
# 根據 OBJS 生成 libbz2.a
libbz2.a: $(OBJS)
rm -f libbz2.a
$(AR) cq libbz2.a $(OBJS)
@if ( test -f $(RANLIB) -o -f /usr/bin/ranlib -o \
-f /bin/ranlib -o -f /usr/ccs/bin/ranlib ) ; then \
echo $(RANLIB) libbz2.a ; \
$(RANLIB) libbz2.a ; \
fi
check: test
test: bzip2
@cat words1
./bzip2 -1 < sample1.ref > sample1.rb2
./bzip2 -2 < sample2.ref > sample2.rb2
./bzip2 -3 < sample3.ref > sample3.rb2
./bzip2 -d < sample1.bz2 > sample1.tst
./bzip2 -d < sample2.bz2 > sample2.tst
./bzip2 -ds < sample3.bz2 > sample3.tst
cmp sample1.bz2 sample1.rb2
cmp sample2.bz2 sample2.rb2
cmp sample3.bz2 sample3.rb2
cmp sample1.tst sample1.ref
cmp sample2.tst sample2.ref
cmp sample3.tst sample3.ref
@cat words3
# 安裝到 /usr/local/bin
install: bzip2 bzip2recover
if ( test ! -d $(PREFIX)/bin ) ; then mkdir -p $(PREFIX)/bin ; fi
if ( test ! -d $(PREFIX)/lib ) ; then mkdir -p $(PREFIX)/lib ; fi
if ( test ! -d $(PREFIX)/man ) ; then mkdir -p $(PREFIX)/man ; fi
if ( test ! -d $(PREFIX)/man/man1 ) ; then mkdir -p $(PREFIX)/man/man1 ; fi
if ( test ! -d $(PREFIX)/include ) ; then mkdir -p $(PREFIX)/include ; fi
cp -f bzip2 $(PREFIX)/bin/bzip2
cp -f bzip2 $(PREFIX)/bin/bunzip2
cp -f bzip2 $(PREFIX)/bin/bzcat
cp -f bzip2recover $(PREFIX)/bin/bzip2recover
chmod a+x $(PREFIX)/bin/bzip2
chmod a+x $(PREFIX)/bin/bunzip2
chmod a+x $(PREFIX)/bin/bzcat
chmod a+x $(PREFIX)/bin/bzip2recover
cp -f bzip2.1 $(PREFIX)/man/man1
chmod a+r $(PREFIX)/man/man1/bzip2.1
cp -f bzlib.h $(PREFIX)/include
chmod a+r $(PREFIX)/include/bzlib.h
cp -f libbz2.a $(PREFIX)/lib
chmod a+r $(PREFIX)/lib/libbz2.a
cp -f bzgrep $(PREFIX)/bin/bzgrep
ln -s -f $(PREFIX)/bin/bzgrep $(PREFIX)/bin/bzegrep
ln -s -f $(PREFIX)/bin/bzgrep $(PREFIX)/bin/bzfgrep
chmod a+x $(PREFIX)/bin/bzgrep
cp -f bzmore $(PREFIX)/bin/bzmore
ln -s -f $(PREFIX)/bin/bzmore $(PREFIX)/bin/bzless
chmod a+x $(PREFIX)/bin/bzmore
cp -f bzdiff $(PREFIX)/bin/bzdiff
ln -s -f $(PREFIX)/bin/bzdiff $(PREFIX)/bin/bzcmp
chmod a+x $(PREFIX)/bin/bzdiff
cp -f bzgrep.1 bzmore.1 bzdiff.1 $(PREFIX)/man/man1
chmod a+r $(PREFIX)/man/man1/bzgrep.1
chmod a+r $(PREFIX)/man/man1/bzmore.1
chmod a+r $(PREFIX)/man/man1/bzdiff.1
echo ".so man1/bzgrep.1" > $(PREFIX)/man/man1/bzegrep.1
echo ".so man1/bzgrep.1" > $(PREFIX)/man/man1/bzfgrep.1
echo ".so man1/bzmore.1" > $(PREFIX)/man/man1/bzless.1
echo ".so man1/bzdiff.1" > $(PREFIX)/man/man1/bzcmp.1
clean:
rm -f *.o libbz2.a bzip2 bzip2recover \
sample1.rb2 sample2.rb2 sample3.rb2 \
sample1.tst sample2.tst sample3.tst
# 目標依賴,經過 CC 命令生成, 也就是主要使用了以下的幾個源文件
blocksort.o: blocksort.c
@cat words0
$(CC) $(CFLAGS) -c blocksort.c
huffman.o: huffman.c
$(CC) $(CFLAGS) -c huffman.c
crctable.o: crctable.c
$(CC) $(CFLAGS) -c crctable.c
randtable.o: randtable.c
$(CC) $(CFLAGS) -c randtable.c
compress.o: compress.c
$(CC) $(CFLAGS) -c compress.c
decompress.o: decompress.c
$(CC) $(CFLAGS) -c decompress.c
bzlib.o: bzlib.c
$(CC) $(CFLAGS) -c bzlib.c
bzip2.o: bzip2.c
$(CC) $(CFLAGS) -c bzip2.c
bzip2recover.o: bzip2recover.c
$(CC) $(CFLAGS) -c bzip2recover.c
.... 省略 ....
複製代碼
簡單分析了下 Makefile,咱們能夠知道須要的源文件有以下的幾個, 如圖所示:
可是咱們不須要 bzip2.c, 由於不須要調用 bzip2 來壓縮文件。只須要將以下的導入到 Android Studio 中便可,如圖所示:
筆者這裏用的是 ndk-build 的方式來進行構建,固然你也可使用 CMamke 的方式。Android.mk 配置以下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# 導入 bzlib 下全部的頭文件
LOCAL_C_INCLUDES := bzlib
# 模塊名稱
LOCAL_MODULE := bspatch
# 若是換行,須要用換行符\, 而後前面必需要有一個 tab 鍵
LOCAL_SRC_FILES := bspatch_native.cpp bzlib/bspatch.c bzlib/blocksort.c bzlib/huffman.c bzlib/crctable.c bzlib/randtable.c bzlib/compress.c bzlib/decompress.c bzlib/bzlib.c
# 連接系統的 log 日誌庫
LOCAL_LDLIBS := -llog
# 生成動態庫
include $(BUILD_SHARED_LIBRARY)
複製代碼
Application.mk 文件只有一行配置,也能夠刪掉這個文件,在app 下的 build.gradle 中配置過濾。
# 生成 armeabi-v7a 平臺
APP_ABI := armeabi-v7a
複製代碼
配置 app 下的 build.gradle 文件
android {
.....
externalNativeBuild {
ndkBuild {
// 必需要加入這行,指定 ndk-build 查找到 Andorid.mk 路徑
path 'src/main/jni/Android.mk'
}
}
}
複製代碼
點擊 Build 下 Refresh Linked C++ Projects, 如圖:
若是看到頭文件都不報紅,也能夠正常運行起來就能夠接着下一步。
那咱們本地方法的的參數也就能夠肯定了,方法聲明以下:
public native void generateNewApkByPatch(String oldApkFile, String newApkFile, String patchFile);
複製代碼
本地方法如何生成呢? 首先讀者須要瞭解如下 JNI 頭文件規則,不瞭解的能夠看個人其餘文章。這裏直接給出頭文件定義。
#include <jni.h>
extern "C"
JNIEXPORT void JNICALL
Java_com_hxj_bsdiffdemo_MainActivity_generateNewApkByPatch(JNIEnv *env, jobject jobj,
jstring old_apk_file_, jstring new_apk_file_, jstring patch_file_);
void Java_com_hxj_bsdiffdemo_MainActivity_generateNewApkByPatch(JNIEnv *env, jobject jobj,
jstring old_apk_file_, jstring new_apk_file_, jstring patch_file_) {
const char *old_apk_file = env->GetStringUTFChars(old_apk_file_, NULL);
const char *new_apk_file = env->GetStringUTFChars(new_apk_file_, NULL);
const char *patch_file = env->GetStringUTFChars(patch_file_, NULL);
// 執行 dspatch 操做
//TODO
env->ReleaseStringUTFChars(old_apk_file_, old_apk_file);
env->ReleaseStringUTFChars(new_apk_file_, new_apk_file);
env->ReleaseStringUTFChars(patch_file_, patch_file);
}
複製代碼
請務必記得 GetStringUTFChars 和 ReleaseStringUTFChars 成對出現, 防止內存泄漏。
接下來咱們只要調用 bspatch 提供的函數便可,咱們能夠知道它有入口函數 main, 而且提供了兩個參數
int main(int argc,char * argv[])
{
FILE * f, * cpf, * dpf, * epf;
BZFILE * cpfbz2, * dpfbz2, * epfbz2;
int cbz2err, dbz2err, ebz2err;
int fd;
ssize_t oldsize,newsize;
ssize_t bzctrllen,bzdatalen;
u_char header[32],buf[8];
u_char *old, *new;
off_t oldpos,newpos;
off_t ctrl[3];
off_t lenread;
off_t i;
// 告訴咱們數組指針長度必須爲 4
if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);
/* Open patch file */
if ((f = fopen(argv[3], "r")) == NULL)
err(1, "fopen(%s)", argv[3]);
.....
return 0;
}
複製代碼
知道了須要傳入的參數,那麼咱們就將 JNI 本地函數修改,以下所示:
#include <jni.h>
extern int main(int argc,char * argv[]);
extern "C"
JNIEXPORT void JNICALL
Java_com_hxj_bsdiffdemo_MainActivity_generateNewApkByPatch(JNIEnv *env, jobject jobj,
jstring old_apk_file_, jstring new_apk_file_, jstring patch_file_);
void Java_com_hxj_bsdiffdemo_MainActivity_generateNewApkByPatch(JNIEnv *env, jobject jobj,
jstring old_apk_file_, jstring new_apk_file_, jstring patch_file_) {
const char *old_apk_file = env->GetStringUTFChars(old_apk_file_, NULL);
const char *new_apk_file = env->GetStringUTFChars(new_apk_file_, NULL);
const char *patch_file = env->GetStringUTFChars(patch_file_, NULL);
// 定一個數組指針,裏面存放都是 char * 指針
char *args[4];
// 拼接命令, 格式爲: bspatch old_apk_file new_apk_file patch_file
args[0] = (char *)"bspatch";
args[1] = (char *) old_apk_file;
args[2] = (char *) new_apk_file;
args[3] = (char *) patch_file;
main(4, args);
env->ReleaseStringUTFChars(old_apk_file_, old_apk_file);
env->ReleaseStringUTFChars(new_apk_file_, new_apk_file);
env->ReleaseStringUTFChars(patch_file_, patch_file);
}
複製代碼
public class MainActivity extends AppCompatActivity {
private Button mBtnUpdate;
private TextView mTvVersion;
static {
System.loadLibrary("bspatch");
}
public native void generateNewApkByPatch(String oldApkFile, String newApkFile, String patchFile);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnUpdate = findViewById(R.id.btn_update);
mTvVersion = findViewById(R.id.tv_version);
PackageManager packageManager = getPackageManager();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(getPackageName(), 0);
String versionName = packageInfo.versionName;
Log.i("James", "onCreate versionName: " + versionName);
mTvVersion.setText("當前的版本:" + versionName);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
mBtnUpdate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 檢查是否有寫 sdcard 權限
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// 自定義提示框.
} else {
ActivityCompat.requestPermissions(MainActivity.this,
new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
100);
}
} else {
update();
}
}
});
}
private void update() {
new UpdateApkAsyckTask().execute();
}
class UpdateApkAsyckTask extends AsyncTask<Void, Void, File> {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected File doInBackground(Void... voids) {
// 獲取當前 apk 安裝的路徑.
String oldApkFilePath = getApplicationInfo().sourceDir;
// 新 apk 所在目錄.
File newApkFile = new File(Environment.getExternalStorageDirectory(), "test_new.apk");
boolean newApkFileExist = false;
if (!newApkFile.exists()) {
try {
newApkFileExist = newApkFile.createNewFile();
} catch (IOException e) {
newApkFileExist = false;
e.printStackTrace();
}
} else {
newApkFileExist = true;
}
if (!newApkFileExist) {
Log.e("James", "doInBackground new apk file 文件不存在.");
return null;
}
// 服務器下載後的補丁文件
File patchFile = new File(Environment.getExternalStorageDirectory(), "test.patch");
if (!patchFile.exists()) {
Log.e("James", "doInBackground 暫未發現新版本.");
return null;
}
// 獲取新的 apk 安裝路徑.
String newApkFilePath = newApkFile.getAbsolutePath();
// 獲取補丁文件路徑
String patchFilePath = patchFile.getAbsolutePath();
// 調用 JNI 函數生成新的 apk
generateNewApkByPatch(oldApkFilePath, newApkFilePath, patchFilePath);
return newApkFile;
}
@Override
protected void onPostExecute(File file) {
if (file == null || !file.exists()) return;
Log.i("James", "onPostExecute: " + file.getTotalSpace());
// 安裝 apk, 請注意 7.0 以上須要 authorities
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 24) {
//Android 7.0及以上
// 參數2 清單文件中provider節點裏面的authorities ; 參數3 共享的文件,即apk包的file類
Uri apkUri = FileProvider.getUriForFile(MainActivity.this,
getApplicationInfo().packageName + ".provider", file);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
}
startActivity(intent);
}
}
}
複製代碼
下面來看測試,首先咱們看到版本爲 1.0 而且界面顏色爲白色。
生成補丁包
經過 adb 工具上傳到 sdcard 目錄,經常使用命令以下:
adb devices 查看當前鏈接設備
adb shell 進入到手機 shell 環境, 若是有若是手機鏈接,可使用 adb -s 設備名 shell 進入
adb install -r xxx.apk 強制安裝,去掉 r 普通安裝
adb push abc.txt /sdcard/ 將 abc.txt 上傳到 /sdcard/目錄下
複製代碼
上傳到 sdcard 下,如圖所示:
最後一步,點擊更新按鈕,能夠看到開始安裝
點擊打開後,能夠看到版本爲 2.0, 界面背景也變了顏色。
最主要的是,test.patch 很小,只有幾百k。
好了,到此就已經所有介紹完畢。其實還算簡單的,須要一些 NKD 開發基礎,ndk-build 工具的使用,以及 Makefile 的語法。
須要代碼的請 點擊