版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、概述
1.什么是增量更新
对于平常的 Android 应用更新,大部分时候是在旧版本的代码上进行修改,打包发布。这时候,新旧版本间的差异是比较小的,增量更新就是我们在旧的应用版本基础上,只更新发生改变的,而不再是完全下载新的 apk,覆盖安装。
普通更新:
在这更新过程中,一般采用异步更新,旧版本 apk 正常运行,后台异步进行下载,下载完成之后进行弹框提示安装。
增量更新:
增量更新通过哈夫曼算法,计算出旧的 apk 与新的 apk 之间不一样的地方(差分),客户端进行版本更新的时候,只需要下载差分包到本地与旧的 apk 进行合并,生成新的 apk,然后进行安装。
2.增量更新优点
几年前,当时网络环境不好,流量费较贵,当发布新版本的时候,用户升级的意愿不高。为解决这个问题,谷歌提出了 Smart App Update,即增量更新(也叫做差分升级)。
现在网络环境虽然好了,但是各个应用 apk 也越来越大。目前来说,增量更新仍然是解决更新包过大的较好的方案。可能几个 G 的应用,更新包只有几百 M 甚至几十 M,这不仅大大提高更新速度,而且对于服务端来说,也很大程度上减少了流量的费用。
3.增量更新缺点
客户端和服务端都需要添加对增量更新的支持,而且正常情况下是无法保证用户在用版本,所以需要对各个旧的版本生产对应的更新包,根据用户上传的版本号进行下载。
当 apk 很小的时候,比如只有几 M,这时候增量包再小也有几百 K 或者上 M。如果使用增量更新有点得不偿失,增量更新较复杂,而且差分与合并过程较为耗时。另外,当进行大版本更新的时候,增量更新包也较大,这时候也不适合进行增量更新。
4.哈夫曼算法
apk 在文件存储中也是以二进制方式存在的,运用哈夫曼算法进行对比新旧版本的 apk 的二进制文件。如果是相同内容,只保存索引;如果不同,则保存压缩的内容和索引。
5.增量更新与热更新、插件化
热更新是使用热更新 service,只更新某些文件,比如原先有个 ClassA 的类文件,要进行更新为 ClassB 的类文件,不进行整个应用的下载,属于轻量级的。
插件化使用了 pluginManager,以功能块划分插件化进行开发,更新的时候对整个功能块进行更新,比如有个 PluginA 模块要更新为 PluginB 模块,相比热更新会更重量级一些。
相比于热更新和插件化,热更新和插件化更多的是体现要更新什么,而增量更新是要怎么更新。在热更新和插件化中仍可以使用增量更新,只更新 ClassA 与 ClassB 之间的类差分包,或者 PluginA 与 PluginB 的模块差分包。
二、差分
差分与合并主要是使用了 bsdiff/bspatch 进行实现的,需用用到依赖 bzip2,下面是这两个下载地址,分别下载两个压缩包。
bsdiff/bspatch 官网
http://www.daemonology.net/bsdiff/
bzip2
http://www.bzip.org/downloads.html
https://github.com/getlantern/lantern
附 csdn 下载链接:http://download.csdn.net/download/qq_18983205/10244386
1.生成可执行文件
在 bsdiff/bspatch 官网点击 Windows port 进行下载 bsdiff4.3-win32-src.zip,这是 win环境的包,也可点击 here 下载 Linux 环境下的包 bsdiff-4.3.tar.gz。
在 bsdiff4.3-win32-src.zip 解压出来的文件夹下,文件夹 Release 中已经有打包好的 bsdiff.exe 和 bspatch.exe 两个可执行文件,在这边我们不使用这个,自己新建 C 项目进行打包。
1.使用 Visual Studio 新建一个空项目 Diff,在 Diff 下新建两个文件夹 include 与 src 分别存放头文件和源代码。
2.同时把 bsdiff4.3-win32-src.zip 中对应的 .h 文件拷贝到 include 文件夹下,除 bspatch.cpp 外的 .c 和 .cpp 文件拷贝到 src 文件夹下。(这边只做差分,不需要 bspatch.cpp 文件)
**3.**Visual Studio 右击头文件 –> 添加 –> 现有项,选择 include 下面的两个头文件,添加到 Diff 项目中。
同样的,把 src 下的文件添加到源文件下。
4.这时候项目会报错,没有找到头文件,是因为源代码与头文件我们没有放在同一个文件夹下。
右键工程 –> 属性 –> c++ –> 附含包目录,选择 include 文件夹。
5.执行项目,报错。
严重性 代码 说明 项目 文件 行 禁止显示状态
错误 C4996 ‘strcat’: This function or variable may be unsafe. Consider using strcat_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. Diff c:\users\zx\documents\visual studio 2015\projects\diff\diff\src\bzlib.c 1416
这是第三方库的警告,右键工程 –> 属性 –> c++ –> 命令行 添加 -D _CRT_SECURE_NO_WARNINGS
6.继续执行项目,继续报错。(这个错与上面的错误已经不一样了)
严重性 代码 说明 项目 文件 行 禁止显示状态
错误 C4996 ‘setmode’: The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant name: _setmode. See online help for details. Diff c:\users\zx\documents\visual studio 2015\projects\diff\diff\src\bzlib.c 1422
这是安全检查报错,直接进行关闭安全检查。右键工程 –> 属性 –> c++ –> 常规 –> SDL检查 否。
7.继续执行项目,生成可执行文件 Diff.exe。
注:当 Visual Studio 进行平台切换的时候,需要进上面的步骤进行重新配置。
2.生成差分包
准备新旧两个应用的安装包,拷贝到 Diff.exe 所在的文件夹下。
切换到 Diff.exe 所在的目录,运行命令 Diff.exe app-old.exe app-new.apk app.patch 生成差分包,第一个参数是 Diff.exe 的路径。第二个参数是旧的 apk 的路径,第三个参数是新的 apk 的路径,第四个参数是生成的差分包的保存路径。
这边由于测试的原因,使用的新旧 apk 本身较小,所以与差分包的大小区别不是很明显。这也说明 apk 很小的时候不适合进行增量更新。
3.生成库文件
上面是使用可执行文件生成差分包,但实际应用中不可能使用这种方式进行生成差分包,当有多个版本要生成差分包的时候,明显效率较低,一般是由 bsdiff 生成库文件,供后台进行 JNI 调用生成差分包。
1.这边使用的是 MyEclipse 作为后台开发,新建一个 web 项目,创建 Diff.java 类。(没有后台开发工具可以用 java 项目替代,jni 方法调起来即可)
public class Diff {
public static native void diff(String oldPath, String newPath, String patchPath);
}
2.接着就是 JNI 的基本流程,生成头文件,拷贝到上面 Visual Studio 创建的 Diff 项目中,并进行配置。
JNI基本流程不熟的可以参考下前面笔记:(一)JNI 基本流程、数据类型 http://blog.csdn.net/qq_18983205/article/details/78958813
把头文件拷贝到 include 文件夹下。
3.修改 bsdiff.cpp 中 main 函数的函数名为 diff_main。
4.在 bsdiff.cpp 中引入头文件 com_xiaoyue_Diff.h。
5.在 bsdiff.cpp 中实现 native 的方法,调用 diff_main 这个方法。
JNIEXPORT void JNICALL Java_com_xiaoyue_Diff_diff
(JNIEnv *env, jclass jclz, jstring old_path_jst, jstring new_path_jst, jstring patch_path_jst) {
int argc = 4;
char *argv[4];
char *old_path = (char *)(*env).GetStringUTFChars(old_path_jst, NULL);
char *new_path = (char *)(*env).GetStringUTFChars(new_path_jst, NULL);
char *patch_path = (char *)(*env).GetStringUTFChars(patch_path_jst, NULL);
argv[0] = "Diff";
argv[1] = old_path;
argv[2] = new_path;
argv[3] = patch_path;
diff_main(argc, argv);
(*env).ReleaseStringUTFChars(old_path_jst, old_path);
(*env).ReleaseStringUTFChars(new_path_jst, new_path);
(*env).ReleaseStringUTFChars(patch_path_jst, patch_path);
}
diff_main 需要两个参数,根据方法实现可知,第一个参数一定是 4,第二个参数是一个长度为 4 的字符串数组,4 个字符串参数分别是标签、旧的 apk 路径、新的 apk 路径和生成差分包的路径。
6.根据 JNI 基本流程生成 dll 库文件,拷贝到 web 项目中。(生成 dll 的时候,注意选择平台,切换平台需要重新进行配置)
4.JNI 调用生成差分
在 Diff.java 中添加加载。
Diff.java
public class Diff {
public static native void diff(String oldPath, String newPath, String patchPath);
static{
System.loadLibrary("Diff");
}
}
在 web 项目中编写 main 函数进行测试。
DiffTest .java
public class DiffTest {
//路径不能包含中文
public static final String OLD_APK_PATH = "E:/app/app-old.apk";
public static final String NEW_APK_PATH = "E:/app/app-new.apk";
public static final String PATCH_PATH = "E:/app/apk.patch";
public static void main(String[] args) {
// TODO Auto-generated method stub
Diff.diff(OLD_APK_PATH, NEW_APK_PATH, PATCH_PATH);
}
}
注:路径不能有中文,否则会乱码,需要进行处理。中文乱码处理可以参考: (三)JNI 中文乱码 http://blog.csdn.net/qq_18983205/article/details/78840507
结果:
5.Linux 下生成差分包
1.把 Linux 环境的 bsdiff/bspatch 包下的 bsdiff.c 和 bzip2 下的所有 .c 和 .h 文件拷贝到 Linux 平台下(虚拟机或百度云等都可以)。
2.命令行切换到对应文件夹下,运行命令 gcc -fPIC -shared blocksort.c decompress.c bsdiff.c randtable.c bzip2.c huffman.c compress.c bzlib.c crctable.c -o Diff.so。
这时候会报错,bsdiff.c 找不到 bzlib.h 头文件。
3.修改 bsdiff.c 文件中的 #include <bzlib.h>
为 #include "bzlib.h"
。
注:由于个人采用的是虚拟机共享文件,碰见几个问题这边提一下:1.直接在 window 平台修改共享文件,并不会同步到虚拟机的共享文件。2.在 Linux 下直接进行修改共享文件,权限不够,没有试用 root 用户,修改较为复杂。最后是在 Linux 下把共享文件重新复制一份,然后进行修改文件权限。
继续编译,仍然出错,这是由于其他文件中也包含有 main 函数,而对于一个可执行文件只能有一个 main 函数。
4.修改其他 .c 文件下的 main 函数方法名(这边个人是在 main 前面加上文件名),重新编译即可。
三、合并
1.库文件的整合
应用的增量更新,合并是在安卓客户端这边来实现的。对于客户端来说,这块相对来说更重要一些。
1. Android Studio 新建 C++ 支持项目,把 cpp 下默认生成的 .cpp 文件删除,以及 MainActivity 中相关的代码去除。
2.把 Linux 环境的 bsdiff/bspatch 包下的 bspatch.c 和 bzip2 下的所有 .c 和 .h 文件拷贝到 cpp 下。
跟 Linux 下编译差分库一样,修改 bspatch.c 文件中的 #include <bzlib.h>
为 #include "bzip2/bzlib.h"
,以及对 bzip2 下的 .c 文件修改 main 函数的函数名 。
3.修改编译配置文件 CMakeLists.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.
#添加整个目录的思路 指定一个变量 添加的时候使用变量值(my_c_path)
file(GLOB my_c_path src/main/cpp/bzip2/*.c)
add_library( # Sets the name of the library.
BsPatch
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${my_c_path}
src/main/cpp/bspatch.c)
# 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.
BsPatch
# Links the target library to the log library
# included in the NDK.
${log-lib} )
这边添加 bizp2 下的 .c 文件采用的是整个文件夹添加,也可以一个个进行添加。点击同步,然后 Clear Project。
4.修改 bspatch.c 文件中的 main 函数的函数名为 path_main。这时候 Rebuild Project 是可以成功的。
5.根据 JNI 基本生成一个 native 的方法 patch,调用 bspatch.c 下的方法。
JNI 基本流程不懂的可以参考下前面笔记:(一)JNI 基本流程、数据类型 http://blog.csdn.net/qq_18983205/article/details/78958813
创建 BsPatch.java 类。
public class BsPatch {
public native static int patch(String oldPath, String newPath, String patchPath);
static {
System.loadLibrary("BsPatch");
}
}
使用 javah 命令生成头文件,同时拷贝到 cpp 文件夹下。
在 bspatch.c 中引入该头文件 com_xiaoyue_bspatch_BsPatch.h,并实现 native 方法。
在 batch.c 中实现 native 方法 patch。
JNIEXPORT jint JNICALL Java_com_xiaoyue_bspatch_BsPatch_patch
(JNIEnv *env, jclass jclz, jstring old_path_jst, jstring new_path_jst, jstring patch_path_jst) {
int ret = -1;
int argc = 4;
char *argv[4];
char *old_path = (char *)(*env)->GetStringUTFChars(env, old_path_jst, NULL);
char *new_path = (char *)(*env)->GetStringUTFChars(env, new_path_jst, NULL);
char *patch_path = (char *)(*env)->GetStringUTFChars(env, patch_path_jst, NULL);
argv[0] = "Patch";
argv[1] = old_path;
argv[2] = new_path;
argv[3] = patch_path;
//成功返回 0
ret = path_main(argc, argv);
(*env)->ReleaseStringUTFChars(env, old_path_jst, old_path);
(*env)->ReleaseStringUTFChars(env, new_path_jst, new_path);
(*env)->ReleaseStringUTFChars(env, patch_path_jst, patch_path);
return ret;
}
2.调用合并
这边简单的实现以下下载差分包进行合并,然后安装的流程,方法不是很完善。实际下载跟安装,可使用自己原有的方法,只需要在下载完成后添加异步调用合并的 native 方法即可。
安卓端代码:
Contants :
public class Contants {
//差分文件服务器路径
public static final String PATCH_FILE = "apk.patch";
public static final String URL_PATCH_DOWNLOAD = "http://172.26.88.1:8080/UpdateService/" + PATCH_FILE;
public static final String SD_CARD = Environment.getExternalStorageDirectory() + File.separator;
//新版本apk的目录
public static final String NEW_APK_PATH = SD_CARD + "apk_new.apk";
//差分文件存储路径
public static final String PATCH_FILE_PATH = SD_CARD + PATCH_FILE;
}
Contants 管理各个文件的路径。
DownLoadUtils :
public class DownLoadUtils {
/**
* 下载差分包
* @param url
* @return
* @throws Exception
*/
public static File download(String url){
File file = null;
InputStream is = null;
FileOutputStream os = null;
try {
file = new File(Contants.PATCH_FILE_PATH);
if (file.exists()) {
file.delete();
}
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setDoInput(true);
is = conn.getInputStream();
os = new FileOutputStream(file);
byte[] buffer = new byte[1*1024];
int len = 0;
while((len = is.read(buffer)) != -1){
Log.d("Tim", String.valueOf(len));
os.write(buffer, 0, len);
}
} catch(Exception e){
e.printStackTrace();
}finally{
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
}
DownLoadUtils 是下载文件的工具类。
ApkUtils :
public class ApkUtils {
/**
* 获取APK版本号
* @param context
* @param packageName
* @return
*/
public static int getVersionCode (Context context, String packageName) {
PackageManager pm = context.getPackageManager();
try {
PackageInfo info = pm.getPackageInfo(packageName, 0);
Log.d("Tim","getVersionCode = "+info.versionCode);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取已安装Apk文件的源Apk文件
* 如:/data/app/my.apk
*
* @param context
* @param packageName
* @return
*/
public static String getSourceApkPath(Context context, String packageName) {
if (TextUtils.isEmpty(packageName))
return null;
try {
ApplicationInfo appInfo = context.getPackageManager()
.getApplicationInfo(packageName, 0);
return appInfo.sourceDir;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
/**
* 安装Apk
*
* @param context
* @param apkPath
*/
public static void installApk(Context context, String apkPath) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + apkPath),
"application/vnd.android.package-archive");
context.startActivity(intent);
}
}
ApkUtils 是 apk 的帮助类,主要提供获取 apk 版本号、获取已安装 apk 文件的源 apk 文件和安装 apk 三个方法,可采用自己实现的方法。
MainActivity :
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new ApkUpdateTask().execute();
}
});
}
class ApkUpdateTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
Log.d(TAG,"开始下载 。。。");
File patchFile = DownLoadUtils.download(Contants.URL_PATCH_DOWNLOAD) ;
Log.d(TAG,"下载完成 。。。");
String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
String newFile = Contants.NEW_APK_PATH;
String patchFileString = patchFile.getAbsolutePath();
Log.d(TAG,"开始合并");
int ret = BsPatch.patch(oldfile, newFile, patchFileString);
Log.d(TAG,"合并完成");
if (ret == 0) {
return true;
} else {
return false;
}
}
@Override
protected void onPostExecute(Boolean aBoolean) {
if (aBoolean) {
Log.d(TAG,"合并成功 开始安装新apk");
ApkUtils.installApk(MainActivity.this, Contants.NEW_APK_PATH);
}
}
}
}
MainActivity 中,使用了一个异步任务,简单的实现了一下下载差分包,整合与安装。
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.xiaoyue.bspatch.MainActivity">
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第一版 old apk"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="更新"/>
</android.support.constraint.ConstraintLayout>
另外需要在 AndroidManifest.xml 中添加权限。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
服务端
修改安卓端的界面和版本号,生成新旧两个 apk,应用前面的生成差分包代码进行生成差分包,直接放置于 web 项目的 WebRoot 目录下,部署在 tomcat 服务器即可。
注:如果没有服务器的话,直接生成差分包,把差分包拷贝到安卓对应目录下,修改代码,跳过下载这一步,直接进行差份,然后安装。
3.验证
验证差分包是否合并成功,一个是看合并后的差分包能否正常安装,另外可以通过查看 apk 安装包的 MD5 值进行对比。
在命令行输入: certutil -hashfile xxx.apk MD5 即可查看对应 apk 安装包的 MD5 值。