转自:http://blog.csdn.net/lmj623565791/article/details/52761658

本文在我的微信公众号:鸿洋(hongyangAndroid)首发。

转载请标明出处: 
http://blog.csdn.net/lmj623565791/article/details/52761658; 
本文出自:【张鸿洋的博客】

一、概述

最近一直关注热修复的东西,偶尔聊天谈到了增量更新,当然了两个完全不是一个东西。借此找了一些资料,收集整理了一下,本来是不想写博客的,因为主要都是工具的实现,但是昨晚在整理资料的时候,忽然发现,我快要忘了这玩意,又要从头找一圈工具。

So,权当一个记录,也方便以后自己查找。

首先要明确的是,什么是增量更新:

相信大家都见过在应用市场省流量更新软件,一个几百M的软件可能只需要下载一个20M的增量包就能完成更新。那么它是如何做的呢?

就是本篇博客的主题了。

增量更新的流程是:用户手机上安装着某个应用,下载了增量包,手机上的apk和增量包合并形成新的包,然后再次安装(注意这个过程是要重新安装的,当然部分应用市场有root权限你可能感知不到)。

ok,那么把整个流程细化为几个关键点:

  1. 用户手机上提取当前安装应用的apk
  2. 如何利用old.apk和new.apk生成增量文件
  3. 增加文件与1.中的old.apk合并,然后安装

解决了上述3个问题,就ok了。

下面开始解决,首先我们看下增量文件的生成与合并,这个环节可以说是整个流程的核心,也是技术难点,值得开心的是,这个技术难点已经有工具替我们实现了。

二、增量文件的生成与合并

这个其实就是利用工具做二进制的一个diff和patch了。

网址:

下载地址:

对了,本文环境为mac,其他系统如果阻碍,慢慢搜索解决即可。

下载好了,解压,切到对应的目录,然后执行make:

aaa:bsdiff-4.3 zhy$ make
Makefile:13: *** missing separator. Stop.
  • 1
  • 2

恩,你没看错,报错了,这个错误还比较好解决。

解压文件里面有个文件:Makefile,以文本的形式打开,将install:下面的if,endif添加一个缩进。

修改完成是这个样子的:

CFLAGS      +=  -O3 -lbz2

PREFIX      ?=  /usr/local
INSTALL_PROGRAM ?= ${INSTALL} -c -s -m 555
INSTALL_MAN ?= ${INSTALL} -c -m 444 all: bsdiff bspatch
bsdiff: bsdiff.c
bspatch: bspatch.c install:
${INSTALL_PROGRAM} bsdiff bspatch ${PREFIX}/bin
.ifndef WITHOUT_MAN
${INSTALL_MAN} bsdiff.1 bspatch.1 ${PREFIX}/man/man1
.endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

然后,重新执行make:

aaa:bsdiff-4.3 zhy$ make
cc -O3 -lbz2 bsdiff.c -o bsdiff
cc -O3 -lbz2 bspatch.c -o bspatch
bspatch.c:39:21: error: unknown type name 'u_char'; did you mean 'char'?
static off_t offtin(u_char *buf)
^~~~~~
char
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这次比上次好点,这次生成了一个bsdiff,不过在生成bspatch的时候报错了,好在其实我们只需要使用bsdiff,为什么这么说呢?

因为生成增量文件肯定是在服务端,或者是我们本地pc上做的,使用的就是bsdiff这个工具;

另外一个bspatch,合并old.apk和增量文件肯定是在我们应用内部做的。

当然这个问题也是可以解决的,搜索下,很多解决方案,我们这里就不继续在这个上面浪费篇幅了。

我这里提供个下载地址:

https://github.com/hymanAndroid/tools/tree/master/bsdiff-4.3

下载完成,直接make,bsdiff和bspatch都会生成(mac环境下)。

=============神奇的分割线==============

ok,假设到这里,不管你使用何种手段,咱们已经有了bsdiff和bspacth,下面演示下这个工具的使用:

首先我们准备两个apk,old.apk和new.apk,你可以自己随便写个项目,先运行一次拿到生成的apk作为old.apk;然后修改些代码,或者加一些功能,再运行一次生成new.apk;

  • 生成增量文件
./bsdiff old.apk new.apk old-to-new.patch
  • 1

这样就生成了一个增量文件old-to-new.patch

  • 增量文件和old.apk合并成新的apk
./bspatch old.apk new2.apk old-to-new.patch
  • 1

这样就生成一个new2.apk

那么怎么证明这个生成的new2.apk和我们的new.apk一模一样呢?

我们可以查看下md5的值,如果两个文件md5值一致,那么几乎可以肯定两个文件时一模一样的(不要跟我较真说什么碰撞可以产生一样的md5的值~~)。

aaa:bsdiff-4.3 zhy$ md5 new.apk
MD5 (new.apk) = 0900d0d65f49a0cc3b472e14da11bde7
aaa:bsdiff-4.3 zhy$ md5 new2.apk
MD5 (new2.apk) = 0900d0d65f49a0cc3b472e14da11bde7
  • 1
  • 2
  • 3
  • 4

可以看到两个文件的md5果然一样~~

恩,假设你不是mac,怎么获取一个文件的md5呢?(自己写代码,下载工具,不要遇到这样的问题,还弹窗我,我会被扣工资的…)

那么到这里我们就已经知道了如何生成增量文件和将patch与旧的文件合并为新的文件。那么我们再次梳理下整个流程:

  1. 服务端已经做好了增量文件(本节完成)
  2. 客户端下载增量文件+提取该应用的apk,使用bspatch合并
  3. 产生的新的apk,调用安装程序

还是蛮清晰的,那么主要是第二点,第二点有两件事,一个是提取应用的apk;一个是使用bspatch合并,那么这个合并肯定是需要native方法和so文件去做的,也就是说我们要自己打个so出来;

三、客户端的行为

(1)提取应用的apk文件

其实提取当前应用的apk非常简单,如下代码:

public class ApkExtract {
public static String extract(Context context) {
context = context.getApplicationContext();
ApplicationInfo applicationInfo = context.getApplicationInfo();
String apkPath = applicationInfo.sourceDir;
Log.d("hongyang", apkPath);
return apkPath;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

(2)制作bspatch so

首先声明一个类,写个native方法,如下:

public class BsPatch {

    static {
System.loadLibrary("bsdiff");
} public static native int bspatch(String oldApk, String newApk, String patch); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

三个参数已经很明确了;

同时别忘了在module的build.gradle下面:

defaultConfig {
ndk {
moduleName = 'bsdiff'
}
}
  • 1
  • 2
  • 3
  • 4
  • 5

注意该步骤需要你配置过ndk的环境(下载ndk,设置ndk.dir)~

ok,接下来就是去完成c的代码的编写了;

首先在app/main目录下新建一个文件夹jni,把之前下载的bsdiff中的bspatch.c拷贝进去;

然后按照jni的规则,在里面新建一个方法:

JNIEXPORT jint JNICALL Java_com_zhy_utils_BsPatch_bspatch
(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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

方法名是有规律的,这个规律不用提了吧~~

注意bsdiff.c中并没有patchMethod方法,这个方法实际上是main方法,直接修改为patchMethod即可,觉得复杂没关系,文末有源码。

ok,此时你可以尝试运行,会提示依赖bzlib,其实从文件顶部的include中也能看出来。

既然依赖,那我们就导入吧:

首先下载:

下载完成后,解压:

将其中的.h和.c文件提取出来,然后可以选择连文件夹copy到我们module的app/main/jni下,结果如下:

记得修改bsdiff中的include:

#include "bzip2/bzlib.h"
  • 1

再次运行;

然后会发现报一堆类似下面的错误:

Error:(70) multiple definition of `main'
  • 1

提示main方法重复定义了,在出错信息中会给出哪些类中包含main方法,可以选择直接将这些类中的main方法直接删除。

删除以后,就ok了~~

那么到这里,我们就完成了JNI的编写,当然文件是bsdiff提供的c源码。

四、增量更新后安装

上面的操作完成后,最后一步就简单了,首先准备两个apk:

old.apk new.apk
  • 1

然后制作一个patch,下面代码中的PATCH.patch;

将old.apk安装,然后将new.apk以及PATCH.patch放置到存储卡;

最后在Activity中触发调用:

private void doBspatch() {
final File destApk = new File(Environment.getExternalStorageDirectory(), "dest.apk");
final File patch = new File(Environment.getExternalStorageDirectory(), "PATCH.patch"); //一定要检查文件都存在 BsPatch.bspatch(ApkExtract.extract(this),
destApk.getAbsolutePath(),
patch.getAbsolutePath()); if (destApk.exists())
ApkExtract.install(this, destApk.getAbsolutePath());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

记得开启读写SDCard权限,记得在代码中校验需要的文件都存在。

install实际就是通过Intent去安装了:

 public static void install(Context context, String apkPath) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setDataAndType(Uri.fromFile(new File(apkPath)),
"application/vnd.android.package-archive");
context.startActivity(i);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里7.0可能会有问题,把路径暴露给别的app了,应该需要FileProvider去实现(未实验,猜测可能有可能)。

大致的效果图如下:

五、总结

如果你只是单纯的要使用该功能,大可以直接将生成的so文件拷入,直接loadLibrary使用即可。

其次,在做增量更新的时候,patch肯定是根据你当前的版本号与最新(或者目标)版本apk,比对下发diff文件,于此同时应该也把目标apk的md5下发,再做完合并后,不要忘记校验下md5;

博客结束,虽然很简单,主要利用工具实现,但是还是建议自己去实现一次,想一次性跑通还是需要一些时间的,可能过程中也会发现一些坑,也能提升自己对JNI的熟练度。

源码:

也可以选择直接使用so


欢迎关注我的微博: 
http://weibo.com/u/3165018720

Android 增量更新完全解析 是增量不是热修复(转)的更多相关文章

  1. Android 增量更新研究

    Android 增量更新实例(Smart App Updates) http://blog.csdn.net/duguang77/article/details/17676797 Android AP ...

  2. 一句话的Android增量更新框架(增量更新)

    转自:http://www.jianshu.com/p/a9ec8fa780e2 Android应用更新要使用完整的新版本Apk安装,增量更新则是提供一个新旧版本偏差数据的patch包供应用下载,然后 ...

  3. Android studio 2.0--android增量更新的那些事

    用了这么久的AS 2.0预览版本号.4.7日谷歌最终公布了android studio 2.0正式版,小编当日便下载了.玩了一下.感觉第二次build编译明显快了,并且好像并没有又一次部署apk.经过 ...

  4. 前端遇上Go: 静态资源增量更新的新实践

    前端遇上Go: 静态资源增量更新的新实践https://mp.weixin.qq.com/s/hCqQW1F8FngPPGZAisAWUg 前端遇上Go: 静态资源增量更新的新实践 原创: 洋河 美团 ...

  5. uni-app: 如何实现增量更新功能?

    都知道,很多APP都有增量更新功能,Uni APP也是在今年初,推出了增量更新功能,今天我们就来学习一波. 当然,很多应用市场为了防止开发者不经市场审核许可,给用户提供违法内容,对增量更新大多持排斥态 ...

  6. Elasticsearch系列---增量更新原理及优势

    概要 本篇主要介绍增量更新(partial update,也叫局部更新)的核心原理,介绍6.3.1版本的Elasticsearch脚本使用实例和增量更新的优势. 增量更新过程与原理 简单回顾 前文我们 ...

  7. Android局部更新(RecyclerView+ DiffUtil)

    一 概述 DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集->新数据集的最小变化量. 说到数据集,相信大家知道它是和谁相关的了,就是我的最爱 ...

  8. android黑科技系列——应用市场省流量更新(增量升级)原理解析

    一.前言 最近在看热修复相关的框架,之前我们已经看过了阿里的Dexposed和AndFix这两个框架了,不了解的同学可以点击这里进行查看:Dexposed框架原理解析 和 AndFix热修复框架原理解 ...

  9. 一个简单的数据增量更新策略(Android / MongoDB / Django)

    我在做个人APP - CayKANJI - 的时候遇到一个问题: 如何增量式地把日语汉字数据地从server更新到APP端,即每次用户运行更新操作时,仅仅获取版本号高于本地缓存的内容. 数据格式 为了 ...

随机推荐

  1. (PASS)JAVA数组去重 三种方法 (不用集合)

    第一种方法(只学到数组的看): 定义一个新的数组长度和旧数组的长度一样,存储除去重复数据的旧数组的数据和0, package demo01; import java.sql.Array; import ...

  2. python 常用技巧 — 字典 (dictionary)

    目录: 1. python 相加字典所有的键值 (python sum all values in dictionary) 2. python 两个列表分别组成字典的键和值 (python two l ...

  3. PHP获取用户是否关注公众号。获取微信openid和用户信息

    <?php /* * 首先填写授权地址为当前网址 * 将$appid和$secret参数替换成自己公众号对应参数,需要外网可以访问服务器环境测试 */ header("Content- ...

  4. 过滤字符串中的html标签

    C#中,我们有时需要过滤掉字符串中的部分html标签,以下是一些简单的html标签过滤方法,使用的主要方式是正则表达式 public static string ClearHtml(string ht ...

  5. IPv6时代已来:双十一中的IPv6大规模应用实践

    摘要: 刚刚过去的双十一,大家对猫晚.抢红包.组团购还记忆犹新.大家不了解的是,不知不觉间,你可能已经成为首批互联网IPv6用户了.今年天猫双十一期间,阿里巴巴全面支持了IPv6,这是IPv6在我国的 ...

  6. UNP学习 非阻塞I/O

    缺省状态下,套接口时阻塞方式的.这意味着当一个套接口调用不能立即完成时,进程进入睡眠状态,等待操作完成.我们将可能阻塞的套接口调用分成四种. 1.输入操作:read.readv.recv.recvfr ...

  7. 网络-Docker 提供的几种原生网络和自定义网络(11)

    Docker 网络从覆盖范围可分为单个 host 上的容器网络和跨多个 host 的网络,本章重点讨论前一种 Docker 安装时会自动在 host 上创建三个网络,我们可用 docker netwo ...

  8. (转)Android Studio解决unspecified on project app resolves to an APK archive which is not supported

    出现该问题unspecified on project app resolves to an APK archive which is not supported as a compilation d ...

  9. python基础三(深浅拷贝)

    1.赋值操作 list_1 = [1,2,3,['barry','Jerry']] list_2 = list_1 list_1[0] = 111 print(list_1) # [111, 2, 3 ...

  10. Sublime text 3 3103 注册码(2016.2.9更新)

    Sublime text 3 (Build 3103) license key,these all tested available on 2016/02/10 .Feel free to enjoy ...