Android增量更新原理及实现方法
2018-4-23 by 大强
一、前言:
Android客户端需要频繁更新版本,每次更新都要下载新apk,1是浪费流量,2是我的服务器是低配,下载速度慢,怎么节省流量呢?增量更新,学习下其原理及实现过程。
二、几个概念:
- 热修复/热更新 轻量级 的更新,抢先上线,打补丁
- 插件化:为提高开发效率,每个人做一个模块,解耦模块,模块更新
- 增量更新: 基础技术,属于重量级的更新
三、传统的版本更新流程
四、增量更新流程
五、疑问:差分包大小是 新版本和旧版本大小的差吗?
差分流程:
newApk与oldApk相同的部分会已地址的方式放入差分包里,不同的部分,经过压缩放到差分包里,以双向链表保存:压缩部分的地址<–>压缩部分地址。
六、实现,开始撸码
1. 二进制文件 拆分/合并 的工具地址:http://www.daemonology.net/bsdiff/
bsdiff 和 bspatch 用的是 bzip2
需要下载2个文件:
bsdiff-4.3.tar.gz: http://www.daemonology.net/bsdiff/bsdiff-4.3.tar.gz
bzip2:
http://www.bzip.org/downloads.html
http://www.bzip.org/1.0.6/bzip2-1.0.6.tar.gz
2.下面用Kotlin语法来实现增量更新:
- 新建一个Android工程,勾选Include C++ support 和 Include Kotlin support 选项
- 新建BsPatch类
public class BsPatch {
public static native int bsPatch(String oldPath,String newPath,String patchPath);
static {
System.loadLibrary("native-lib");
}
}
- 生成对应的c++方法
native-lib.cpp
#include <jni.h>
extern "C"
JNIEXPORT jint JNICALL
Java_com_hbck_bspatchdiff_BsPatch_bsPatch(JNIEnv *env, jclass type, jstring oldPath_,
jstring newPath_, jstring patchPath_) {
const char *oldPath = env->GetStringUTFChars(oldPath_, 0);
const char *newPath = env->GetStringUTFChars(newPath_, 0);
const char *patchPath = env->GetStringUTFChars(patchPath_, 0);
// TODO
env->ReleaseStringUTFChars(oldPath_, oldPath);
env->ReleaseStringUTFChars(newPath_, newPath);
env->ReleaseStringUTFChars(patchPath_, patchPath);
}
- 解压bsdiff-4.3.tar.gz ,bzip2-1.0.6.zip
- 拷贝所有的.c和.h文件到cpp目录下,如下图:
- 修改CMakeLists.txt,配置下添加的c代码
file(GLOB bzip_c src/main/cpp/bzip2/*.c)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${bzip_c}
src/main/cpp/bspatch.c
src/main/cpp/native-lib.cpp )
Build->Rebuild project,报错,如下图,这是因为c文件里有多个入口main函数,重复导致的,把所有的main方法修改下,我这里以c文件的名字命名了。
bspatch.c分析
apk合并的方法在bspatch里,
int bspatch(int argc, char *argv[]) 这个方法,直接调用这个方法,argc是参数的个数,argv是参数数组
if (argc != 4) errx(1, “usage: %s oldfile newfile patchfile\n”, argv[0]);
这行代码说明参数是4个,依次为报错信息,旧apk路径,新apk路径,差分包路径贴出native-lib的代码,一般拆分是服务端做的,可以用dsdiff.exe以命令方式生成差分包,然后上传到服务器,Android端版本更新时,下载差分包,然后和旧的安装包进行合并,生成新的apk
#include <jni.h>
#include "android/log.h"
#ifdef __cplusplus
extern "C" {
#endif
int bspatch(int argc, char *argv[]);
int bsdiff(int argc,char *argv[]);
#ifdef __cplusplus
}
#endif
#define TAG "daqiang_bspatch"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__ )
extern "C"
JNIEXPORT jint JNICALL
Java_com_hbck_bspatchdiff_BsPatch_bsPatch(JNIEnv *env, jclass type, jstring oldPath_,
jstring newPath_, jstring patchPath_) {
LOGD("native patch begin");
char *oldPath = (char *) env->GetStringUTFChars(oldPath_, 0);
char *newPath = (char *) env->GetStringUTFChars(newPath_, 0);
char *patchPath = (char *) env->GetStringUTFChars(patchPath_, 0);
int argc = 4;
char *argv[4];
argv[0] = TAG;
argv[1] = oldPath;
argv[2] = newPath;
argv[3] = patchPath;
int ret = bspatch(argc, argv);
env->ReleaseStringUTFChars(oldPath_, oldPath);
env->ReleaseStringUTFChars(newPath_, newPath);
env->ReleaseStringUTFChars(patchPath_, patchPath);
LOGD("native patch end");
return ret;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_hbck_bspatchdiff_BsPatch_bsDiff(JNIEnv *env, jclass type, jstring oldPath_,
jstring newPath_, jstring patchPath_) {
LOGD("native diff begin");
char *oldPath = (char *) env->GetStringUTFChars(oldPath_, 0);
char *newPath = (char *) env->GetStringUTFChars(newPath_, 0);
char *patchPath = (char *) env->GetStringUTFChars(patchPath_, 0);
int argc = 4;
char *argv[4];
argv[0] = TAG;
argv[1] = oldPath;
argv[2] = newPath;
argv[3] = patchPath;
int ret = bsdiff(argc, argv);
env->ReleaseStringUTFChars(oldPath_, oldPath);
env->ReleaseStringUTFChars(newPath_, newPath);
env->ReleaseStringUTFChars(patchPath_, patchPath);
LOGD("native diff end");
return ret;
}
- 调用核心代码
//合并差分包
private fun doBsPatch() {
Thread {
val oldPath = Environment.getExternalStorageDirectory().absolutePath + File.separator + "old2.apk"
val newPath = Environment.getExternalStorageDirectory().absolutePath + File.separator + "newPatch.apk"
val patchPath = Environment.getExternalStorageDirectory().absolutePath + File.separator + "apkDiff.patch"
val ret = BsPatch.bsPatch(oldPath, newPath, patchPath)
runOnUiThread {
if (ret < 0) {
Toast.makeText(this, "合并失败", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "合并成功", Toast.LENGTH_SHORT).show()
this@MainActivity.installApk(newPath)
}
}
}.start()
}