文档章节

Android Apk差分与合成更新

IamOkay
 IamOkay
发布于 2016/06/20 16:49
字数 2409
阅读 311
收藏 7

Android增量更新的原理是使用比较2个apk,然后通过差异与手机apk程序合成一个新的apk。

 

我们知道,获取手机端app中的app可以通过如下方法,类似常用的插件化读取第三方app资源的方式。

 

方法:getPackageCodePath

释义:返回android 安装包的完整路径,这个包是一个zip的压缩文件,它包括应用程序的代码和assets文件。

方法:getPackageResourcePath

释义:返回android 安装包的完整路径,这个包是一个ZIP的压缩文件,它包括应用程序的私有资源。

Context context = activity.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY|Context.CONTEXT_INCLUDE_CODE);
PathClassLoader classLoader = new PathClassLoader(context.getPackageResourcePath(),context.getClassLoader());
Class<?> forName = Class.forName(packageName+".R$layout", true, classLoader);

我们这里并不需要读取第三方,显然也没必要使用上面的代码,这里的更新是把2个压缩包合成一个。

增量升级的原理

首先将应用的旧版本Apk与新版本Apk做差分,得到更新的部分的补丁,例如旧版本的APK有5M,新版的有8M,更新的部分则可能只有3M左右(这里需要说明的是,得到的差分包大小并不是简单的相减,因为其实需要包含一些上下文相关的东西),使用差分升级的好处显而易见,那么你不需要下载完整的8M文件,只需要下载更新部分就可以,而更新部分可能只有3、4M,可以很大程度上减少流量的损失。
在用户下载了差分包之后,需要在手机端将他们组合起来。可以参考的做法是先将手机端的旧版本软件(多半在/data/下),复制到SD卡或者cache中,将它们和之前的差分patch进行组合,得到一个新版本的apk应用,如果不出意外的话,这个生成的apk和你之前做差分的apk是一致的。

增量升级的操作

首先是差分包patch的生成。

下载bsdiff 查分工具

命令:bsdiff oldfile newfile patchfile
例如: bsdiff xx_v1.0.apk xx_v2.0.apk xx.patch

将生成的补丁包 xx.patch放置在升级服务器上,供用户下载升级,对应多版本需要对不同的版本进行差分,对于版本跨度较大的,建议整包升级。 用户在下载了 xx.patch补丁包后,需要用到补丁所对应的apk,即原来系统安装的旧版本apk和补丁合成的bspatch工具。系统旧版本的apk可以通过copy系统data/app目录下的apk文件获取,而补丁合成的bspatch可以通过将bspatch源码稍作修改,封装成一个so库,供手机端调用。

bspatch的命令格式为:
bspatch oldfile newfile patchfile

和差分时的参数一样。合成新的apk便可以用于安装。 以上只是简单的操作原理,增量升级还涉及很多其他方面,例如,升级补丁校验等问题,可以参考android源码中bootable\recovery\applypatch的相关操作,本文只是浅析,在此不表。 不足 增量升级并非完美无缺的升级方式,至少存在以下两点不足:

1.增量升级是以两个应用版本之间的差异来生成补丁的,你无法保证用户每次的及时升级到最新,所以你必须对你所发布的每一个版本都和最新的版本作差分,以便使所有版本的用户都可以差分升级,这样操作相对于原来的整包升级较为繁琐,不过可以通过自动化的脚本批量生成。

2.增量升级成功的前提是,用户手机端必须有能够让你拷贝出来且与你服务器用于差分的版本一致的apk,这样就存在,例如,系统内置的apk无法获取到,无法进行增量升级;对于某些与你差分版本一致,但是内容有过修改的(比如破解版apk),这样也是无法进行增量升级的,为了防止合成补丁错误,最好在补丁合成前对旧版本的apk进行sha1sum校验,保证基础包的一致性。 小实验 多说无益,实践才是王道。下面就来简单实践一下,检测之前理论的正确性。

├── bsdiff-4.3 //bsdiff的源码路径,官网获取
│ ├── bsdiff.1
│ ├── bsdiff.c
│ ├── bspatch.1
│ ├── bspatch.c
│ └── Makefile
├── bsdiff-4.3.tar.gz
├── bsdiff4.3-win32 //windows PC端的测试工具
│ ├── Binary diff.txt
│ ├── bsdiff.exe
│ ├── bspatch.exe
│ └── LICENSE
├── bspatch //手机端的测试工具
├── oldAPK1.6.2.apk // 旧版本的apk
└── newAPK1.8.0.apk //新版本的apk

APK来做测试,在shell进入test\bsdiff4.3-win32文件夹,并下运行命令:


bsdiff.exe oldAPK1.6.2.apk newAPK1.8.0.apk apk.patch

原来的apk(2.94M),新版本的(3.24M),得到的patch文件为1.77M,用户需要下载的就只是1.77M,流量节省了很多。

 

下面先在电脑端将他们合并。

bspatch.exe oldAPK1.6.2.apk new.apk apk.patch

执行后得到名为new.apk 的合成版本应用。这个和我们newAPK1.8.0.apk其实是一样的。

在Android程序中,我们需要下载第三方库以下程序

bzlib.c 
blocksort.c 
compress.c
crctable.c
decompress.c
huffman.c 
randtable.c 
bzip2.c


现在写一个安卓小DEMO出来,测试一下这个工具。直接在创建安卓工程的时候添加native支持,在CPP文件中添加以下代码

#include "com_droidupdate_jni_PatchUtil.h"
#include "bzlib_private.h"
#include "bzlib.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <err.h>
#include <unistd.h>
#include <fcntl.h>
#include <android/log.h>
 
static off_t offtin(u_char *buf) {
	off_t y;
 
	y = buf[7] & 0x7F;
	y = y * 256;
	y += buf[6];
	y = y * 256;
	y += buf[5];
	y = y * 256;
	y += buf[4];
	y = y * 256;
	y += buf[3];
	y = y * 256;
	y += buf[2];
	y = y * 256;
	y += buf[1];
	y = y * 256;
	y += buf[0];
 
	if (buf[7] & 0x80)
		y = -y;
 
	return y;
}
 
int applypatch(int argc, const char* argv[]) {
	FILE * f, *cpf, *dpf, *epf;
	BZFILE * cpfbz2, *dpfbz2, *epfbz2;
	int cbz2err, dbz2err, ebz2err;
	int fd;
	ssize_t oldsize, newsize;
	ssize_t bzctrllen, bzdatalen;
	u_char header[32], buf[8];
	u_char *oldStr, *newStr;
	off_t oldpos, newpos;
	off_t ctrl[3];
	off_t lenread;
	off_t i;
 
	if (argc != 4)
		errx(1, "usage: %s oldfile newfile patchfile\n", argv[0]);
 
	/* Open patch file */
	if ((f = fopen(argv[3], "r")) == NULL)
		err(1, "fopen(%s)", argv[3]);
 
	/*
	 File format:
	 0   8   "BSDIFF40"
	 8   8   X
	 16  8   Y
	 24  8   sizeof(newfile)
	 32  X   bzip2(control block)
	 32+X    Y   bzip2(diff block)
	 32+X+Y  ??? bzip2(extra block)
	 with control block a set of triples (x,y,z) meaning "add x bytes
	 from oldfile to x bytes from the diff block; copy y bytes from the
	 extra block; seek forwards in oldfile by z bytes".
	 */
 
	/* Read header */
	if (fread(header, 1, 32, f) < 32) {
		if (feof(f))
			errx(1, "Corrupt patch\n");
		err(1, "fread(%s)", argv[3]);
	}
 
	/* Check for appropriate magic */
	if (memcmp(header, "BSDIFF40", 8) != 0)
		errx(1, "Corrupt patch\n");
 
	/* Read lengths from header */
	bzctrllen = offtin(header + 8);
	bzdatalen = offtin(header + 16);
	newsize = offtin(header + 24);
	if ((bzctrllen < 0) || (bzdatalen < 0) || (newsize < 0))
		errx(1, "Corrupt patch\n");
 
	/* Close patch file and re-open it via libbzip2 at the right places */
	if (fclose(f))
		err(1, "fclose(%s)", argv[3]);
	if ((cpf = fopen(argv[3], "r")) == NULL)
		err(1, "fopen(%s)", argv[3]);
	if (fseeko(cpf, 32, SEEK_SET))
		err(1, "fseeko(%s, %lld)", argv[3], (long long) 32);
	if ((cpfbz2 = BZ2_bzReadOpen(&cbz2err, cpf, 0, 0, NULL, 0)) == NULL)
		errx(1, "BZ2_bzReadOpen, bz2err = %d", cbz2err);
	if ((dpf = fopen(argv[3], "r")) == NULL)
		err(1, "fopen(%s)", argv[3]);
	if (fseeko(dpf, 32 + bzctrllen, SEEK_SET))
		err(1, "fseeko(%s, %lld)", argv[3], (long long) (32 + bzctrllen));
	if ((dpfbz2 = BZ2_bzReadOpen(&dbz2err, dpf, 0, 0, NULL, 0)) == NULL)
		errx(1, "BZ2_bzReadOpen, bz2err = %d", dbz2err);
	if ((epf = fopen(argv[3], "r")) == NULL)
		err(1, "fopen(%s)", argv[3]);
	if (fseeko(epf, 32 + bzctrllen + bzdatalen, SEEK_SET))
		err(1, "fseeko(%s, %lld)", argv[3],
				(long long) (32 + bzctrllen + bzdatalen));
	if ((epfbz2 = BZ2_bzReadOpen(&ebz2err, epf, 0, 0, NULL, 0)) == NULL)
		errx(1, "BZ2_bzReadOpen, bz2err = %d", ebz2err);
 
	if (((fd = open(argv[1], O_RDONLY, 0)) < 0)
			|| ((oldsize = lseek(fd, 0, SEEK_END)) == -1) || ((oldStr =
					(u_char*) malloc(oldsize + 1)) == NULL)
			|| (lseek(fd, 0, SEEK_SET) != 0)
			|| (read(fd, oldStr, oldsize) != oldsize) || (close(fd) == -1))
		err(1, "%s", argv[1]);
	if ((newStr = (u_char*) malloc(newsize + 1)) == NULL)
		err(1, NULL);
 
	oldpos = 0;
	newpos = 0;
	while (newpos < newsize) {
		/* Read control data */
		for (i = 0; i <= 2; i++) {
			lenread = BZ2_bzRead(&cbz2err, cpfbz2, buf, 8);
			if ((lenread < 8)
					|| ((cbz2err != BZ_OK) && (cbz2err != BZ_STREAM_END)))
				errx(1, "Corrupt patch\n");
			ctrl[i] = offtin(buf);
		};
 
		/* Sanity-check */
		if (newpos + ctrl[0] > newsize)
			errx(1, "Corrupt patch\n");
 
		/* Read diff string */
		lenread = BZ2_bzRead(&dbz2err, dpfbz2, newStr + newpos, ctrl[0]);
		if ((lenread < ctrl[0])
				|| ((dbz2err != BZ_OK) && (dbz2err != BZ_STREAM_END)))
			errx(1, "Corrupt patch\n");
 
		/* Add old data to diff string */
		for (i = 0; i < ctrl[0]; i++)
			if ((oldpos + i >= 0) && (oldpos + i < oldsize))
				newStr[newpos + i] += oldStr[oldpos + i];
 
		/* Adjust pointers */
		newpos += ctrl[0];
		oldpos += ctrl[0];
 
		/* Sanity-check */
		if (newpos + ctrl[1] > newsize)
			errx(1, "Corrupt patch\n");
 
		/* Read extra string */
		lenread = BZ2_bzRead(&ebz2err, epfbz2, newStr + newpos, ctrl[1]);
		if ((lenread < ctrl[1])
				|| ((ebz2err != BZ_OK) && (ebz2err != BZ_STREAM_END)))
			errx(1, "Corrupt patch\n");
 
		/* Adjust pointers */
		newpos += ctrl[1];
		oldpos += ctrl[2];
	};
 
	/* Clean up the bzip2 reads */
	BZ2_bzReadClose(&cbz2err, cpfbz2);
	BZ2_bzReadClose(&dbz2err, dpfbz2);
	BZ2_bzReadClose(&ebz2err, epfbz2);
	if (fclose(cpf) || fclose(dpf) || fclose(epf))
		err(1, "fclose(%s)", argv[3]);
 
	/* Write the new file */
	if (((fd = open(argv[2], O_CREAT | O_TRUNC | O_WRONLY, 0666)) < 0)
			|| (write(fd, newStr, newsize) != newsize) || (close(fd) == -1))
		err(1, "%s", argv[2]);
 
	free(newStr);
	free(oldStr);
 
	return 0;
}
 
jint JNICALL Java_com_droidupdate_jni_PatchUtil_applyPatchToOldApk(JNIEnv *pEnv,
		jclass clazz, jstring oldPath, jstring newPath, jstring patchPath) {
	const char* pOldPath = pEnv->GetStringUTFChars(oldPath, JNI_FALSE);
	const char* pNewPath = pEnv->GetStringUTFChars(newPath, JNI_FALSE);
	const char* pPatchPath = pEnv->GetStringUTFChars(patchPath, JNI_FALSE);
 
	const char* argv[4];
	argv[0] = "bspatch";
	argv[1] = pOldPath;
	argv[2] = pNewPath;
	argv[3] = pPatchPath;
 
	int ret = -1;
	ret = applypatch(4, argv);
 
	pEnv->ReleaseStringUTFChars(oldPath, pOldPath);
	pEnv->ReleaseStringUTFChars(newPath, pNewPath);
	pEnv->ReleaseStringUTFChars(patchPath, pPatchPath);
	return ret;
}

Android.mk文件

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := DroidUpdate
LOCAL_SRC_FILES := \
DroidUpdate.cpp \
bzlib.c \
blocksort.c \
compress.c \
crctable.c \
decompress.c \
huffman.c \
randtable.c \
bzip2.c

LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)

java代码

package com.droidupdate.jni;

import java.io.IOException;

import android.content.Context;

public class PatchUtil {

	static {
		System.loadLibrary("DroidUpdate");
	};

	private static native int applyPatchToOldApk(String oldapk_filepath,
			String newapk_savepath, String patchpath);

	/**
	 * @param oldApkPath 旧版apk文件的路径
	 * @param newApkPath 新版apk文件的路径
	 * @param patchPath 增量包的路径
	 * @throws IOException
	 */
	public static int applyPatch(String oldApkPath, String newApkPath,
			String patchPath) throws IOException {
		return applyPatchToOldApk(oldApkPath, newApkPath, patchPath);
	}

	public static int applyPatchToOwn(Context context, String newApkPath,
			String patchPath) throws IOException {
		String old = context.getApplicationInfo().sourceDir;
		return applyPatchToOldApk(old, newApkPath, patchPath);
	}

}

需要发布升级包的时候,把新打好的包用windows的bsdiff.exe制作好.patch文件,然后我们程序检测到新版本的时候就下载这个.patch文件,然后调用这个JNI函数把.patch文件和当前的版本比较生成一个最新版本的apk文件,然后通过application/vnd.android.package-archive来安装即可!

下面是工具和安卓端测试源码
bsdiff4.3-win32
DroidUpdate

参考:leehom 2015年01月19日 于 IT十万个为什么 发表

 

© 著作权归作者所有

IamOkay

IamOkay

粉丝 203
博文 483
码字总数 403074
作品 0
海淀
程序员
私信 提问
Android 增量更新实例(Smart App Updates)

目录[-] 官方说明 实现原理 实现 (1)生成差异包 (2)使用旧apk+差异包,在客户端合成新apk 注意事项 demo 自从 Android 4.1 开始,Google引入了应用程序的增量更新。 官方说明 Smart app ...

嘻哈开发者
2013/12/23
397
0
APP升级问题汇总(测试环境/正式环境/真实环境)

1.开发初期统一在测试环境中操作; 2.出版本后,进行版本迭代升级,需要照顾好测试环境与正式环境,同时可运行无误; 3.测试环境更多为开发使用,正式环境为外辅助协助测试使用; 4.测试环境...

陈贤冲
2016/12/07
1
0
Tinker 1.9.6 发布,微信开源的 Android 热修复框架

Tinker 1.9.6 发布了。Tinker 是微信开源的 Android 热修复框架,支持在无需升级 APK 的前提下更新 dex、library 和 resources 文件。 主要更新内容: 修复1.9.5在MIUI机器上无法启动JobSche...

达尔文
2018/04/10
803
1
Tinker 1.9.1 发布,微信开源的 Android 热修复框架

Tinker 是微信开源的 Android 热修复框架,支持在无需升级 APK 的前提下更新 dex、library 和 resources 文件。 Tinker 1.9.1 是 1.9.0 的 bug 修复版本,该版本的更新内容如下: TinkerMult...

周其
2017/11/10
746
1
Tinker 1.9.8 发布,修复 OPPO 与 MIUI 等机型补丁问题

Tinker 1.9.8 发布了,Tinker 是腾讯开源的 Android 热解决方案库,它支持在不重新安装 apk 的情况下对 dex、library 和 resources 进行更新。 此次更新主要修复了以下问题: OPPO、VIVO机型...

h4cd
2018/06/26
1K
3

没有更多内容

加载失败,请刷新页面

加载更多

哪些情况下适合使用云服务器?

我们一直在说云服务器价格适中,具备弹性扩展机制,适合部署中小规模的网站或应用。那么云服务器到底适用于哪些情况呢?如果您需要经常原始计算能力,那么使用独立服务器就能满足需求,因为他...

云漫网络Ruan
今天
5
0
Java 中的 String 有没有长度限制

转载: https://juejin.im/post/5d53653f5188257315539f9a String是Java中很重要的一个数据类型,除了基本数据类型以外,String是被使用的最广泛的了,但是,关于String,其实还是有很多东西...

低至一折起
今天
17
0
OpenStack 简介和几种安装方式总结

OpenStack :是一个由NASA和Rackspace合作研发并发起的,以Apache许可证授权的自由软件和开放源代码项目。项目目标是提供实施简单、可大规模扩展、丰富、标准统一的云计算管理平台。OpenSta...

小海bug
昨天
11
0
DDD(五)

1、引言 之前学习了解了DDD中实体这一概念,那么接下来需要了解的就是值对象、唯一标识。值对象,值就是数字1、2、3,字符串“1”,“2”,“3”,值时对象的特征,对象是一个事物的具体描述...

MrYuZixian
昨天
9
0
解决Mac下VSCode打开zsh乱码

1.乱码问题 iTerm2终端使用Zsh,并且配置Zsh主题,该主题主题需要安装字体来支持箭头效果,在iTerm2中设置这个字体,但是VSCode里这个箭头还是显示乱码。 iTerm2展示如下: VSCode展示如下: 2...

HelloDeveloper
昨天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部