一、概述
什么是增量更新?
增量更新就是在安装新版本的apk(以下简称new.apk)时,可以不用下载完整的apk文件,只需要下载一个增量包(.patch文件),然后将旧版本apk(以下简称old.apk)和该增量包合并就成了新版本的apk。
本文参考了鸿洋大神的这篇文章http://blog.csdn.net/lmj623565791/article/details/52761658,在此基础上完善一下步骤,总结一下遇到的坑。
二、实现
服务器端上
首先当有新版本时,服务器端肯定要提供patch文件,该文件通过old.apk和new.apk得到(要根据不同版本的old.apk提供不同的patch文件),使用bsdiff工具https://download.pokorra.de/coding/bsdiff_win_exe.zip
解压缩后,将old.apk和new.apk移到该目录,在命令行下输入以下命令,可以得到PATCH.patch文件。
目录下另一个文件bspatch是将old.apk和patch文件合并为new.apk的,这里不使用,在手机客户端也就是代码上实现合并。
客户端上
首先要下载bsdiff的源码,因为它依赖于bzip2,所以也要下载bzip2的源码。
bsdiff源码下载地址:http://www.daemonology.net/bsdiff/bsdiff-4.3.tar.gz
bzip2源码下载地址:http://www.bzip.org/1.0.6/bzip2-1.0.6.tar.gz
(下载不成功的可以在我的github上下载,最后会给链接)
在Android Studio上创建好项目后,在app的main目录下新建jni目录,将bsdiff压缩包解压缩后得到的bspatch.c文件复制进jni目录,在jni目录下创建一个bzip2目录,将bzip2压缩包解压缩后得到的.h和.c文件(总共15个)都复制进bzip2目录,如下图所示:
然后在app目录下的build.gradle目录下配置ndk:
defaultConfig {
...
ndk {
moduleName = 'bspatch'
}
}
这步前提是要已经安装了ndk(local.properties文件中有ndk.dir这一行),没有的话在SDK Manager的SDK Tools目录下勾选NDK这项。
local.properties文件中确定有
这两行后,在gradle.properties文件中添加
android.useDeprecatedNdk=true
然后在创建类BsPatchUtil来调用jni
public class BsPatchUtil {
static {
System.loadLibrary("bspatch"); //这里的library名称对应build.gradle目录下ndk的moduleName值
}
public static native int patch(String oldApkPath, String newApkPath, String patchPath);
}
再然后在jni目录下的bspatch.c文件中添加上面patch对应的方法
JNIEXPORT jint JNICALL Java_com_hzh_apkpatchdemo_BsPatchUtil_patch
(JNIEnv *env, jclass cls,
jstring old, jstring new, jstring patch){
int argc = 4;
char * argv[argc];
argv[0] = "bspatch";
argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));
int ret = patchMethod(argc, argv);
(*env)->ReleaseStringUTFChars(env, old, argv[1]);
(*env)->ReleaseStringUTFChars(env, new, argv[2]);
(*env)->ReleaseStringUTFChars(env, patch, argv[3]);
return ret;
}
这里的方法名Java_com_hzh_apkpatchdemo_BsPatchUtil_patch换成自己对应的,其中com_hzh_apkpatchdemo对应包名,BsPatchUtil是类名,patch是方法名。
int ret = patchMethod(argc, argv);
这行中的patchMethod方法就是把bspatch.c文件中的main方法改成patchMethod
文件中的
#include <bzlib.h>
改成
#include <bzip2/bzlib.h>
同时添加
#include "jni.h"
然后运行,会发现报一堆以下错误
Error:(70) multiple definition of `main'
提示main方法重复定义了,在出错信息中直接进入相应文件删除main方法就行了。
至此,ndk配置完毕了,接下了就是将old.apk与patch文件合并成new.apk并安装它的方法。
创建一个ApkExtract类,在该类中给一个获取当前版本apk路径的方法
public static String extract(Context context) {
context = context.getApplicationContext();
ApplicationInfo applicationInfo = context.getApplicationInfo();
String apkPath = applicationInfo.sourceDir;
Log.d("hzh", apkPath);
return apkPath;
}
给一个安装apk的方法
public static void install(Context context, String apkPath) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
File file = new File(apkPath);
Uri uri;
if (Build.VERSION.SDK_INT >= 24){
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
uri = FileProvider.getUriForFile(context,"com.hzh.apkpatchdemo.fileprovider",file);
}else{
uri = Uri.fromFile(file);
}
i.setDataAndType(uri,
"application/vnd.android.package-archive");
context.startActivity(i);
}
这里Android 7.0以上版本要用FileProvider来获取uri
uri = FileProvider.getUriForFile(context,"com.hzh.apkpatchdemo.fileprovider",file);
“com.hzh.apkpatchdemo.fileprovider”该属性要在AndroidManifest.xml文件中声明
<application
...>
<provider
android:authorities="com.hzh.apkpatchdemo.fileprovider"
android:name="android.support.v4.content.FileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths"
/>
</provider>
</application>
@xml/filepaths文件内容如下
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path path="" name="update"/>
</paths>
其中external-path代表Environment.getExternalStorageDirectory(),path中的内容是该路径下的子目录,name不能为空,可以随便取。
最后在MainActivity中触发调用
private void doBspatch() {
final File destApk = new File(Environment.getExternalStorageDirectory(), "new.apk");
final File patch = new File(Environment.getExternalStorageDirectory(), "PATCH.patch");
//一定要检查文件都存在
BsPatchUtil.patch(ApkExtract.extract(MainActivity.this),
destApk.getAbsolutePath(),
patch.getAbsolutePath());
if (destApk.exists())
ApkExtract.install(MainActivity.this, destApk.getAbsolutePath());
}
最后注意要给读写权限,这是重点,之前就因为忘记加了搞了半天搞不出来,也没显示哪里错。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
public void onClick(View view) {
if (ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, 2);
} else {
doBspatch();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 2) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
doBspatch();
}
}
}
接下来就是打包生成apk,生成patch文件,将patch文件放到手机客户端的指定目录
在手机上安装old.apk,运行之后效果图如下:
三、总结
服务器端下发patch文件时,同时也要下发md5值,合并完成后,不要忘记校验下md5。
附上github地址:https://github.com/HZHAboom/ApkPatchDemo
工具下载地址:
https://github.com/HZHAboom/-/tree/master/Android%E5%A2%9E%E9%87%8F%E6%9B%B4%E6%96%B0%E5%B7%A5%E5%85%B7%E5%8C%85
除了上面讲的配置ndk的方法,还可以用CMakeLists.txt文件来配置
在新建项目时勾选Include C++ Support
在完成时勾选这两项,点击Finish,项目就会自动在app目录下生成CMakeLists.txt文件,自动生成cpp目录、native-lib.cpp文件和调用的演示方法。
然后将之前的jni目录下的文件拷贝到cpp目录下,将CMakeLists.txt文件中的native-lib和路径改成bspatch就行了
# Sets the minimum version of CMake required to build the native
# library. You should either keep the default value or only pass a
# value of 3.4.0 or lower.
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 it for you.
# Gradle automatically packages shared libraries with your APK.
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).
# Associated headers in the same location as their source
# file are automatically included.
src/main/cpp/bspatch.c )
# Searches for a specified prebuilt library and stores the path as a
# variable. Because system libraries are included 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 the
# 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} )
以上就配置好了,或者不在新建项目时勾选Include C++ Support也行,在项目创建之后,自己在app目录下创一个CMakeLists.txt文件,在build.gradle文件中配置就行了
android {
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}