Android7.0增量更新完整方案及踩坑 for Mac OSX 10.12 发表于 2017-03-10 | 分类于 Android |

简介

什么是增量更新呢?增量更新可以帮助我们减少用户更新apk所耗费的流量。
具体的做法是,在老版本apk和新版本apk中,差分出这两个apk文件之间,不同的部分,得到一个patch(补丁)文件。
比如我们之前的apk是10M,新的apk是12M,一般情况下,差分出来的补丁文件的大小在2M左右。
因此,用户在更新apk时,就只需要下载这2M的patch文件,通过app将patch文件和旧apk合成,就可以得到新的apk。

增量更新的步骤

我们来总结一下,

【服务器端】
我们需要通过【diff(差分)】操作,得到patch文件。
【app端】
我们需要从服务器端下载patch文件,并将其与旧apk文件合并,得到新版本的apk文件,然后提示用户安装。

预备环境

Mac 10.12.2(Linux)
Android Studio 2.3.3

通过SDK Manager安装

  • NDK
  • CMake
  • LLDB

bsdiff 差分与合并工具: http://www.daemonology.net/bsdiff
bsdiff 4.3下载地址:http://www.daemonology.net/bsdiff/bsdiff-4.3.tar.gz
bzip2 bsdiff的依赖库: http://www.bzip.org/downloads.html
bzip2-1.0.6下载地址:http://www.bzip.org/1.0.6/bzip2-1.0.6.tar.gz

bsdiff是用c写的库,它提供了两个工具:bsdiff和bspatch,分别用来差分和合并。

在服务器端安装bspatch

首先我们将下载好的两个包bsdiff-4.3.tar.gz和bzip2-1.0.6.tar.gz解压到同一个目录下,这里我们仅保留bsdiff的MakeFile文件。

然后我们打开MakeFile文件,在CFLAGS中添加-Du_char=”unsigned char”,并且【重要】,
在第13行和15行前面按Tab键,使得这两行代码向后缩进一格。

保存文件之后,我们在此目录下执行cmake命令,此时应该会编译得到bsdiff和bspatch两个可执行程序,服务器端我们只需要bspatch。
我们可以在终端中使用cp bspatch /usr/local/bin 将bspatch移动到用户应用目录下,之后就可以直接在终端中使用它。

bspatch命令有3个参数,
oldfile 旧apk文件路径
newfile 新apk文件路径
patchfile patch文件生成的路径

app端

我们新建一个工程,

注意选择支持C++,之后保持默认选项就好。

然后,我们在布局中添加一个Button,指定text为“执行增量更新”。

删除Android Studio为我们生成的native-lib.cpp文件,然后将刚刚解压出来的那些.c/.h文件复制到cpp目录下,
将AS自动生成的那个CMakeLists.txt文件拷贝到cpp目录下,并且在bzip2目录下再创建一个CMakeLists.txt
形成这样的结构:

编辑bzip2目录下的那个CMakeLists.txt

     
     
1
2
     
     
#bzip2
PROJECT(bzip2)

编辑cpp目录下的CMakeLists.txt

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
     
     
cmake_minimum_required(VERSION 3.4. 1)
# 支持-std=gnu++11
set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -Wall -DGLM_FORCE_SIZE_T_LENGTH")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGLM_FORCE_RADIANS")
set(bzip2_src_DIR ${CMAKE_SOURCE_DIR})
add_subdirectory( ${bzip2_src_DIR}/bzip2)
add_library(
bspatch
SHARED
bspatch.c )
find_library(
log-lib
log )
target_link_libraries( # Specifies the target library.
bspatch
# Links the target library to the log library
# included in the NDK.
${log-lib} )

CMake的语法这里就不再介绍了,有兴趣请自行查阅相关资料。

然后,我们需要修改一下bspatch.c的内容。

在bspatch.c中:

     
     
1
2
3
4
5
6
7
8
9
10
11
12
     
     
...
#include <sys/types.h>
#include <jni.h>
...
// 添加这些引用
#include "bzip2/crctable.c"
#include "bzip2/compress.c"
#include "bzip2/decompress.c"
#include "bzip2/randtable.c"
#include "bzip2/blocksort.c"
#include "bzip2/huffman.c"
...

接下来,我们需要编写native方法,来调用差分函数。

在MainActivity.java中添加native方法,以及引入动态库文件:

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
     
     
public class MainActivity extends AppCompatActivity {
...
static {
System.loadLibrary( "bspatch");
}
/**
* Native方法 合并更新文件
* @param oldAPKPath 原APK路径
* @param newAPKPath 要生成的新APK路径
* @param patchPath 增量更新补丁包路径
* @return 成功返回 0
*/
public native int patch(String oldAPKPath, String newAPKPath, String patchPath);
}

然后回到bspatch.c文件中,添加jni方法:

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
     
     
JNIEXPORT jint JNICALL
Java_cn_xiaoniaojun_diff_MainActivity_patch (JNIEnv *env, jobject instance, jstring oldAPKPath_,
jstring newAPKPath_, jstring patchPath_) {
const char *oldAPKPath = (*env)->GetStringUTFChars(env, oldAPKPath_, 0);
const char *newAPKPath = (*env)->GetStringUTFChars(env, newAPKPath_, 0);
const char *patchPath = (*env)->GetStringUTFChars(env, patchPath_, 0);
int argc = 4;
char* argv[ 4];
argv[ 0] = "bspatch";
argv[ 1] = oldAPKPath;
argv[ 2] = newAPKPath;
argv[ 3] = patchPath;
int ret = bspatch_main(argc, argv);
(*env)->ReleaseStringUTFChars(env, oldAPKPath_, oldAPKPath);
(*env)->ReleaseStringUTFChars(env, newAPKPath_, newAPKPath);
(*env)->ReleaseStringUTFChars(env, patchPath_, patchPath);
}

注意,这里用到了一个小技巧,要把main()函数修改成patch_main(),才可以调用。

OK,native方法写好了,接下来我们就来编写Java代码,实现增量合并更新。

完整的MainActivity.java

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
     
     
public class MainActivity extends AppCompatActivity {
public static final String SDCARD_PATH
= Environment.getExternalStorageDirectory() + File.separator;
public static final String PATCH_FILE = "old-to-new.patch";
public static final String NEW_APK_FILE = "latest.apk";
// load increment update lib;
static {
System.loadLibrary( "bspatch");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// setup incremental update click action
Button btnIncrementalUpdate = (Button) findViewById(R.id.btn_incremental_update);
btnIncrementalUpdate.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) {
new IncrementalUpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
LogI(getPatchFilePath());
}
private class IncrementalUpdateTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
String oldApkPath = SDCARD_PATH + "oldapk.apk";
File oldApkFile = new File(oldApkPath);
File patchFile = new File(getPatchFilePath());
if (oldApkFile.exists() && patchFile.exists()) {
LogI( "正在合并增量文件...");
String newApkPath = getNewApkFilePath();
patch(oldApkPath, newApkPath, getPatchFilePath());
LogI( "增量文件的MD5值为:" + SignUtils.getMd5ByFile(patchFile));
LogI( "新文件的MD5值为:" + SignUtils.getMd5ByFile( new File(newApkPath)));
return true;
}
LogI( "找不到补丁文件");
return false;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result) {
LogI( "合并成功,开始安装");
ApkUtils.installApk(MainActivity. this, getNewApkFilePath());
} else {
LogI( "合并失败");
}
}
}
private String getNewApkFilePath() {
return SDCARD_PATH + NEW_APK_FILE;
}
private String getPatchFilePath() {
return SDCARD_PATH + PATCH_FILE;
}
private void LogI(String log) {
Log.i( "MainActivity", log);
}
/**
* Native方法 合并更新文件
* @param oldAPKPath 原APK路径
* @param newAPKPath 要生成的新APK路径
* @param patchPath 增量更新补丁包路径
* @return 成功返回 0
*/
public native int patch(String oldAPKPath, String newAPKPath, String patchPath);
}

工具类:

MD5检测工具类:

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
     
     
public class SignUtils {
/**
* 判断文件的MD5值是否为指定值
* @param file1
* @param md5
* @return
*/
public static boolean checkMd5(File file1, String md5) {
if(TextUtils.isEmpty(md5)) {
throw new RuntimeException( "md5 cannot be empty");
}
if(file1 != null && file1.exists()) {
String file1Md5 = getMd5ByFile(file1);
return file1Md5.equals(md5);
}
return false;
}
/**
* 获取文件的MD5值
* @param file
* @return
*/
public static String getMd5ByFile(File file) {
String value = null;
FileInputStream in = null;
try {
in = new FileInputStream(file);
MessageDigest digester = MessageDigest.getInstance( "MD5");
byte[] bytes = new byte[ 8192];
int byteCount;
while ((byteCount = in.read(bytes)) > 0) {
digester.update(bytes, 0, byteCount);
}
value = bytes2Hex(digester.digest());
} catch (Exception e) {
e.printStackTrace();
} finally {
if ( null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return value;
}
private static String bytes2Hex(byte[] src) {
char[] res = new char[src.length * 2];
final char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
for ( int i = 0, j = 0; i < src.length; i++) {
res[j++] = hexDigits[src[i] >>> 4 & 0x0f];
res[j++] = hexDigits[src[i] & 0x0f];
}
return new String(res);
}
}

Apk安装工具类

注意,在Android 7.0以上,为了提高私有文件的安全性,面向 Android 7.0 或更高版本的应用私有目录被限制访问 (0700)。此设置可防止私有文件的元数据泄漏,如它们的大小或存在性。
因此,在Android 7以上,在应用之间共享文件受到了很大的限制。

如果使用如下的代码:

     
     
1
2
3
4
5
6
7
8
     
     
public void installApk(File file) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}

系统将会抛出一个 FileUriExposedException 异常。
解决办法是,使用FileProvider。

在Manifest文件中,<application></application>标签之间,添加:

     
     
1
2
3
4
5
6
7
8
9
     
     
<provider
android:authorities= "cn.xiaoniaojun.fileprovider"
android:name= "android.support.v4.content.FileProvider"
android:exported= "false"
android:grantUriPermissions= "true" >
<meta-data
android:name= "android.support.FILE_PROVIDER_PATHS"
android:resource= "@xml/file_paths" />
</provider>

以启用FileProvider。注意<meta-data>标签,我们还需要提供一个资源文件,用来标识需要共享的文件。

在res文件下新建一个xml目录,并添加file_paths.xml文件。

     
     
1
2
3
4
     
     
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="latest.apk" path="" />
</paths>

这里<root_path>并未在官方文档中指出,它指代Environment.getExternalStorageDirectory()目录,
这个目录一般为/storage/emulator/0/,path为添加在其后的路径,name为文件名。
这里,指明我们的新apk文件在/storage/emulator/0/latest.apk路径下。

OK,配置完FileProvider后,我们编写ApkUtils.java:

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
     
     
public class ApkUtils {
public static void installApk(Context context, String apkPath) {
File file = new File(apkPath);
if (file.exists()) {
Uri apkUri = FileProvider.getUriForFile(context, "cn.xiaoniaojun.fileprovider", file);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
context.startActivity(intent);
}
}
}

请大家自行与之前的installApk()方法对比,看看使用FileProvider后,获取文件Uri的方式有怎样的变化。

至此,增量更新的代码就编写完成了。

测试

首先,在当前工程,使用Build->Build Apk生成旧版本apk文件。

然后,我们编辑Button的文本,改为“开始增量更新 V2”。还可以在资源文件中放几张图片,以扩充apk大小,使得增量更新的效果更明显。
改好了之后,再Build Apk一次。

现在,我们得到了两个apk文件,改名为newapk.apk和oldapk.apk,执行dspatch命令,得到old-to-new.patch文件。

我们将oldapk.apk文件和old-to-new.patch文件上传至SDCard根目录下:

在终端中,执行命令($表示本地终端,#表示adb终端):

     
     
1
2
3
4
5
     
     
$ adb shell # 进入手机终端
# su # 取得超级权限
$ adb push newapk.apk /storage/emulated/0/ # 上传
$ adb push old-to-new.patch /storage/emulated/0/
# ls /storage/emulated/0 # 可以看到新上传的文件

最后别忘了给app读取外部存储空间的权限哦!
在manifest中,添加:

     
     
1
2
3
     
     
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>

好了,接下来我们把旧apk安装到模拟器上,然后记得给权限哦!

准备工作完成,启动旧apk,点击“启动增量更新”:

增量更新

可以看到,点击增量更新后,app自动在后台合成apk并提示用户更新。

转载自:http://xiaoniaojun.cn/2017/03/10/Android-7-%E5%A2%9E%E9%87%8F%E6%9B%B4%E6%96%B0-Mac.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值