增量更新和全量更新
我想玩过大型手游的人都知道,手游的安装包非常大,因为资源图片众多。而你每次更新都把所有文件都更新下来,是非常耗时的,对吧。耗时是一个方面,有些人在户外开的是移动网络,动不动就几个G。这对于用户来说,是一笔不小的浪费。那么就引入了我们今天的话题,增量更新。
说起增量更新,那么就不得不提及一下与它相对的概念,全量更新。全量更新,是大多数,乃至几乎所有app的更新方式,即把整个安装包都下载下来。那既然大家都用全量更新,那必定有其存在的合理之处,存在即合理,对吧。全量更新流程非常简洁,而增量更新流程相对比较复杂,且一般应用的安装包不是很大,使用增量更新就像高射炮打苍蝇,这是一个方面,也是主要的方面。还有一个方面是因为,增量更新有一定的门槛,需要有一定的NDK和C语言的基础。
增量更新的流程(服务端)
增量更新无需下载最新的完整版本的apk文件,而只需下载本地版本升到最新版本的补丁文件,然后使用本地正在运行的app的merge补丁的功能合并成一个完整的最新版本的apk文件,最后手动将这个合并好的apk文件安装。
安装包1.1版本 = 安装包1.0版本 merge 1.0版本升1.1版本的补丁文件
1.0版本升1.1版本的补丁文件 = 安装包1.1版本 diff 安装包1.0版本
我们补丁拆分的过程是后端完成的,而补丁合并的过程是Android端完成的。后端需要使用云服务器下载一个类似于bsdiff-4.3.tar.gz的压缩包,然后解压并使用其命令进行补丁文件的拆分。
wget https://src.fedoraproject.org/lookaside/pkgs/bsdiff/bsdiff-4.3.tar.gz/e6d812394f0e0ecc8d5df255aa1db22a/bsdiff-4.3.tar.gz
tar zxvf bsdiff-4.3.tar.gz
cd bsdiff-4.3
然后编译,运行,结束。
你以为事情会有如此顺利?
vim Makefile
注意看.ifndef WITHOUT_MAN这一行和.endif这一行,输入i进入insert模式,光标移动到这两行最前面,按下tab键缩进下格式,然后esc,退出insert模式到命令模式,输入冒号“:”,最后输入wq!回车保存退出。
最后make进行编译。
make
这样我们就得到了两个绿色的可执行文件。
bsdiff [oldfile] [newfile] [patchfile]
最后我们就可以正常执行文件拆分命令了。比如我们电脑本地的文件上传。
sftp root@dorachat.com
先使用sftp登录服务器,root@后面改成你服务器的公网ip,不要跟我的一样,你登不上来。
put old_apk_1.0.apk
put new_apk_1.1.apk
bsdiff命令的使用格式是这样。
bsdiff [oldfile] [newfile] [patchfile]
那我们就执行。
./bsdiff old_apk_1.0.apk new_apk_1.1.apk patch_1.0_1.1.patch
执行完成就会在当前目录生成patch_1.0_1.1.patch补丁文件了,这个路径要根据你apk实际存在服务器的路径,要不然也是没法读取的。最后将补丁文件的下载地址通过接口返回给客户端,服务端的事情就做完了。
增量更新的流程(Android端)
Android端要使用到NDK,所以,你得先确保安装好了NDK和CMake的包。
然后写一个PatchUtils工具类。
package com.dorachat.dorachat.util;
public class PatchUtils {
public native static int mergePatch(String oldApkPath, String newApkPath, String patchPath);
static {
System.loadLibrary("patchUpdate");
}
}
然后将这些bzip相关的c文件扔进cpp文件夹,再修改app模块的build.gradle.kts,也可能是build.gradle,看你用的什么构建脚本了。
android {
splits {
abi {
isEnable = true
reset()
include("x86", "x86_64", "armeabi-v7a", "arm64-v8a")
// select ABIs to build APKs for
isUniversalApk = true
// generate an additional APK that contains all the ABIs
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.18.1"
}
}
}
CMakeLists.txt的写法。
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.18.1)
# Declares and names the project.
project("app")
# 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 them for you.
# Gradle automatically packages shared libraries with your APK.
find_library( # Sets the name of the path variable.
log-lib
log)
# 不要加,跟增量更新没有关系
add_library(
cryptoMsg
SHARED
RSAUtils.h
RSAUtils.cpp)
# 指定要编译成动态库的c文件
add_library(
patchUpdate
SHARED
bspatch.c
bzip2/blocksort.c
bzip2/bzip2.c
bzip2/bzip2recover.c
bzip2/bzlib.c
bzip2/compress.c
bzip2/crctable.c
bzip2/decompress.c
bzip2/dlltest.c
bzip2/huffman.c
bzip2/mk251.c
bzip2/randtable.c
bzip2/spewG.c
bzip2/unzcrash.c
)
#include_directories(src/main/cpp/bzip2)
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
#include_directories(src/main/cpp/)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries 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.
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
# cryptoMsg
patchUpdate
${log-lib})
命令的入口代码bspatch.c。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <jni.h>
#include <err.h>
#include <string.h>
#include "bzip2/bzlib.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 patchMethod(int argc, 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 *old, *new;
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) ||
((old = malloc(oldsize + 1)) == NULL) ||
(lseek(fd, 0, SEEK_SET) != 0) ||
(read(fd, old, oldsize) != oldsize) ||
(close(fd) == -1))
err(1, "%s", argv[1]);
if ((new = 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, new + 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))
new[newpos + i] += old[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, new + 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, new, newsize) != newsize) || (close(fd) == -1))
err(1, "%s", argv[2]);
free(new);
free(old);
return 0;
}
JNIEXPORT jint JNICALL Java_com_dorachat_dorachat_util_PatchUtils_mergePatch
(JNIEnv *env, jclass cls,
jstring old_apk_path, jstring new_apk_path, jstring patch_path) {
int argc = 4;
char *argv[argc];
argv[0] = "bspatch";
argv[1] = (char *) ((*env)->GetStringUTFChars(env, old_apk_path, 0));
argv[2] = (char *) ((*env)->GetStringUTFChars(env, new_apk_path, 0));
argv[3] = (char *) ((*env)->GetStringUTFChars(env, patch_path, 0));
int ret = patchMethod(argc, argv);
(*env)->ReleaseStringUTFChars(env, old_apk_path, argv[1]);
(*env)->ReleaseStringUTFChars(env, new_apk_path, argv[2]);
(*env)->ReleaseStringUTFChars(env, patch_path, argv[3]);
return ret;
}
bzip2的源文件,网上很多。比如这个
https://github.com/hongyangAndroid/BsDiff_And_Patch 。
提取本地apk文件可以使用这个方法。
public static File extractApk(Context context) {
ApplicationInfo applicationInfo = context.getApplicationContext().getApplicationInfo();
String apkPath = applicationInfo.sourceDir;
File apkFile = new File(apkPath);
return apkFile;
}
或者你直接使用
https://github.com/dora4/dora/blob/master/dora-mvvm/src/main/java/dora/util/ApkUtils.java 。
说到最后
最后聊一聊更新的具体逻辑。增量更新有固定数量法和固定业务法。你可以规定,相差版本小于等于3个,就采用增量更新,大于3个,直接走全量更新。此为固定数量法。固定业务法则是,版本分为大小版本号的更新,每个大版本必须走全量更新,而大版本中的小版本则采用增量更新。大版本采用强制更新,而小版本则可采用选择更新。每次更新到大版本,可能会再次拉取该版本的最终小版本的补丁文件发起二次更新。