Android Apk差分與合成更新

Android增量更新的原理是使用比較2個apk,而後經過差別與手機apk程序合成一個新的apk。java

 

咱們知道,獲取手機端app中的app能夠經過以下方法,相似經常使用的插件化讀取第三方app資源的方式。android

 

方法:getPackageCodePathshell

釋義:返回android 安裝包的完整路徑,這個包是一個zip的壓縮文件,它包括應用程序的代碼和assets文件。windows

方法:getPackageResourcePath服務器

釋義:返回android 安裝包的完整路徑,這個包是一個ZIP的壓縮文件,它包括應用程序的私有資源。app

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文件,只須要下載更新部分就能夠,而更新部分可能只有三、4M,能夠很大程度上減小流量的損失。
在用戶下載了差分包以後,須要在手機端將他們組合起來。能夠參考的作法是先將手機端的舊版本軟件(多半在/data/下),複製到SD卡或者cache中,將它們和以前的差分patch進行組合,獲得一個新版本的apk應用,若是不出意外的話,這個生成的apk和你以前作差分的apk是一致的。測試

增量升級的操做spa

首先是差分包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十萬個爲何 發表

相關文章
相關標籤/搜索