咱们知道随着功能不断增长,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 的语法。
须要代码的请 点击