背景
你一定知道有些App应用商店在更新时会有增量更新的按钮,只需要下载本身App大小的一半甚至更小即可安装,这就是增量更新。
各个App开发商以及开发者自己的App由于业务日益增多、各种PM的需求不断叠加,安装包尺寸日益加大,小则几十M,大则几百M,用户的更新成本加大严重影响咱们的新用户加入。
增量更新就是这个背景下被开发出来了,原理简单来说就是通过二进制流比对apk间的差异并记录下来(需要记录上下文位置、差异位置和大小等等,所以差分包比两个Apk大小相减要大一些),生成差分包。最后客户端通过差分包记录的位置和流数据拼接到本地apk即可。
什么是增量更新?
增量更新的关键在于增量一词。平时我们的开发过程,往往都是今天在昨天的基础上修改一些代码,app的更新也是类似的,往往都是在旧版本的app上进行修改。这样看来,增量更新就是原有app的基础上只更新发生变化的地方,其余保持原样。
与之前每次更新都要下载完整apk包的做法相比,这样做的好处显而易见,每次变化的地方总是比较少的,因此更新包的体积就会小很多。咱们的App用户每次更新都需要下载大约170m左右的安装包,而采用增量更新这种方案之后每次可能只需要下载很小的更新包即可,相比原来做法大大减少了用户下载等待的时间和流量。
增量更新的原理
增量更新的原理是通过某种算法找出新版本和旧版本不一样的地方(这个过程也叫做差分),然后将不一样的地方抽取出来形成所谓的更新补丁(patch),也称之为差分包。客户端在检测到更新的时候,只需要下载差分包到本地,然后将差分包合并至本地的安装包,形成新版本的安装包,文件校验通过后再执行安装即可。
流程演示
流程简短说明
服务器运维:
运维的同学拿到客户端开发的新版本A,获得新版本A的MD5,然后跟已发布的旧版本B做了差分生成相应的差分包C。
生成差分包前提:1、差分包比全量包小于xM,x咱们自己定,过小就没必要差分,也就是咱们可能大版本更新了。 2、非大版本强更,强更没必要差分,避免不必要的替换错误。
客户端:
用户手动更新或程序主动请求更新:
1、客户端向服务端请求更新数据,若服务端没有差分包则返回全量包下载URL、MD5值。
2、若服务端存在相应的差分包则返回差分包下载URL,全量包和差分包MD5值。把差分包下载到本地之后(C),先做MD5值校验,确保下载的差分包数据的完整性,校验失败则走全量更新逻辑,校验
OK则和本地现有安装的旧版本(B)进行差分合并生成新版本(A),之后进行合成版本A的MD5值校验是否等同于全量包MD5,确保合成文件的完整性。校验无误后再进行安装。
服务端生成差分包
所需的native源码资源文章最后有链接
需要用BSDiff 的bsdiff/bspatch命令。
命令行或者终端输入bsdiff app-release.apk app-release_new.apk patch.patch为生成的差分包
生成
合并差分包
合并app-release.apk和patch.patch,生成新的安装包app-release_new2.apk。只要此处合并出来的new.apk和上面我们自己打出来的new.apk一样,那么就认为它就是我们需要的新版本安装包。
将app-release.apk和patch.patch放入bspatch命令所在的文件夹,
命令行或者终端输入bspatch app-release.apk app-release_new2.apk patch.patch 稍等一会便可以生成新的apk文件 app-release_new2.apk
生成了
合并而来的new2.apk应该和我们自己打出来的new.apk是一模一样的,可以通过验证两者的md5来认定。
Windows版本的bsdiff.exe和bspatch.exe 已经集成zip直接用就行。
但是Android版本的bspatch还需要自己集成bspatch.c的native代码。
Android接入bsPatch
1.接入Bspatch
这里我用的是Gitee上找的第三方Module,导入自己的项目作为module(bspatch)
jni通过cmake方式导入了patch.c和bzip的.h和.c文件
我是自己单独创建个module来做patch
这里我们使用cmake来开发native
build.gradle配置cmake
CmakeList.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
file(GLOB bzip_c src/main/cpp/bzip2/*.c)
add_library( # Sets the name of the library.
ApkPatch
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${bzip_c}
src/main/cpp/bsdiff.c
src/main/cpp/bspatch.c
src/main/cpp/ApkPatch.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
ApkPatch
# Links the target library to the log library
# included in the NDK.
${log-lib} )
所需的native源码资源文章最后有链接
ApkPatch.cpp是native的入口cpp文件
#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_cn_berfy_demo_apkpatch_PatchUtils_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_cn_berfy_demo_apkpatch_PatchUtils_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;
}
Java调用
package cn.berfy.demo.apkpatch;
/**
* 类说明: APK Patch工具类
*
* @author Cundong
* @version 1.0
*/
public class PatchUtils {
static {
System.loadLibrary("ApkPatch");
}
/**
* native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
* <p>
* 返回:0,说明操作成功
*
* @param oldApkPath 示例:/sdcard/old.apk
* @param newApkPath 示例:/sdcard/new.apk
* @param patchPath 示例:/sdcard/xx.patch
*/
public static native int bspatch(String oldApkPath, String newApkPath, String patchPath);
/**
* native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
* <p>
* 返回:0,说明操作成功
*
* @param oldApkPath 示例:/sdcard/old.apk
* @param newApkPath 示例:/sdcard/new.apk
* @param patchPath 示例:/sdcard/xx.patch
*/
public static native int bsdiff(String oldApkPath, String newApkPath, String patchPath);
}
核心代码就搭建好了,接下来就是调用了。
2、生成差分包
这里我们就没有下载差分包步骤,无非就是CDN或者ftp下载到本地的过程。
必要的权限,别忘了请求下写入权限再去做一切操作。
在你的app主Module的assets放入准备的apk新旧版本,代码随便改动即可。
在你的MainActivity把文件复制到cache目录
创建个文件路径专门存放生成的patch.patch
过程必须放在子线程,比较耗时
差分包就弄好了,接着我们合并它,最后能成功安装就代表测试通过。
3.合并差分包
下载差分包之后,合并old.apk(这里我们用的其他的apk测试)和path.patch为new2.apk
如果需要获取自己的apk可以用这个方法
4.安装apk
对于合并之后的apk,首先要做的事情也是进行MD5校验(通过服务器获取的全量包de MD5),校验通过之后,再进行安装:
这里我忽略了这步
到现在,增量更新就已经完成了,把增量包进行删除。
资源获取
服务端
Android端
链接不好放上去,可以私信我的邮箱446296114@163.com可以发给你。
问题总结
- 差分包的过程比较耗时,客户端来做不现实,需要放到测试那边或者发版本时服务器处理。
- 即使改动很小或者什么都没改,差分包依然有900多K大小(不过对于咱们的大包来说不算问题)。
- 需要判断好出现错误(MD5验证失败、下载失败)的跳转传统更新的逻辑。
增量更新的优缺点
优点显而易见不用说了。
缺点:
1、apk包之间的差异过小时,比如2m以下,此时生成的差分包仍然有几百k,此时使用增量更新得不偿失,毕竟形成差分包和合并的过程都非常耗时。另外,但版本之间变化非常大的时候,通常是是大版本好变化的时候使用完整更新也不错。
2、差分过程手机处理较慢(不过也没必要App自己处理),服务端处理没必要担心。