Android 差量更新实践

Android 差量更新(增量更新)

原理

1、服务器端利用文件对比工具,将新旧安装包对比生成差异包。
2、将差异包下发到客户端
3、将差异包与本地旧安装包做文件合并,然后安装

今天主要是讲解如何使用:、bspatch工具生成差分文件以及文件合并,开发环境为Mac+Android Studio

bsdiff、bspatch下载及使用

下载链接是http://www.daemonology.net/bsdiff/
上面讲解了bsdiff的特点和优势,下载下来的是一个压缩包,里面包含了下面几个文件这里写图片描述
我们主要使用其中两个C文件就够了。

打开其中文件bsdiff.c可以看到

#include <sys/types.h>

#include <bzlib.h>
#include <err.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

bsdiff的算法里面使用到bzlib这个压缩库。大家可以在我的源代码里下载这个库https://github.com/zhichaoZhang/AndroidTrain/tree/master/AndroidTrain/app/src/main/jni/bzip2
把bzip2文件夹和bsdiff.c文件放在同一目录,并修改引用路径如下图:

#include <sys/types.h>
#include "bzip2/bzlib.c"
#include "bzip2/crctable.c"
#include "bzip2/compress.c"
#include "bzip2/decompress.c"
#include "bzip2/randtable.c"
#include "bzip2/blocksort.c"
#include "bzip2/huffman.c"
#include <err.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

打开命令行并进入到bsdiff所在目录,执行gcc bsdiff.c命令会变异bsdiff.c文件,然后生成一个a.out文件。编译完成之后就可以运行了。下面看bsdiff.c的main方法:

int main(int argc,char *argv[])
{
    int fd;
    u_char *old,*new;
    off_t oldsize,newsize;
    off_t *I,*V;
    off_t scan,pos,len;
    off_t lastscan,lastpos,lastoffset;
    off_t oldscore,scsc;
    off_t s,Sf,lenf,Sb,lenb;
    off_t overlap,Ss,lens;
    off_t i;
    off_t dblen,eblen;
    u_char *db,*eb;
    u_char buf[8];
    u_char header[32];
    FILE * pf;
    BZFILE * pfbz2;
    int bz2err;

    if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);

    /* Allocate oldsize+1 bytes instead of oldsize bytes to ensure
        that we never try to malloc(0) and get a NULL pointer */
    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(((I=malloc((oldsize+1)*sizeof(off_t)))==NULL) ||
        ((V=malloc((oldsize+1)*sizeof(off_t)))==NULL)) err(1,NULL);

    qsufsort(I,V,old,oldsize);

    free(V);

    /* Allocate newsize+1 bytes instead of newsize bytes to ensure
        that we never try to malloc(0) and get a NULL pointer */
    if(((fd=open(argv[2],O_RDONLY,0))<0) ||
        ((newsize=lseek(fd,0,SEEK_END))==-1) ||
        ((new=malloc(newsize+1))==NULL) ||
        (lseek(fd,0,SEEK_SET)!=0) ||
        (read(fd,new,newsize)!=newsize) ||
        (close(fd)==-1)) err(1,"%s",argv[2]);

    if(((db=malloc(newsize+1))==NULL) ||
        ((eb=malloc(newsize+1))==NULL)) err(1,NULL);
    dblen=0;
    eblen=0;

    /* Create the patch file */
    if ((pf = fopen(argv[3], "w")) == NULL)
        err(1, "%s", argv[3]);
        ...
        省略后面
        ...

main方法总共需要三个参数:oldPath(旧文件路径)、newPath(新文件路径)、patchPath(生成的差分文件路径)。所以生成差分包的命令为:

/Users/joye/Desktop/patch_update/jni/a.out  /Users/joye/Desktop/patch_update/app-debug.apk /Users/joye/Desktop/patch_update/app-debug_new.apk /Users/joye/Desktop/patch_update/apk.patch
joyedeMacBook-Pro:jni zczhang$ 

注意:a.out一定是全路径。

客户端合并差分包并安装

服务器生成好差分包后,会通过网络下发到客户端。这里为了实验差分包的合并,就省去了通过网络下载的步骤,直接把差分包放到手机内部存储的根目录,并取名为apk.patch。

差分包有了,那么原始安装包从哪来?

答案是data/app/packagename/目录下的1.apk文件,取到这个文件的方法也很简单,代码如下:

ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(), 0);
//sourceDir即原始apk文件路径
String sourceDir = applicationInfo.sourceDir;

文件合并要用到bspatch.c这个文件,既然用到C代码,肯定离不了jni了。新建Class文件FileDiffer,并声明一个native方法fileCombine方法。

public class FileDiffer {
    public native int fileCombine(String oldFile, String newFile, String patchFile);
}

然后编译并生成头文件。在java目下执行命令

javah com.zzc.androidtrain.apk_patch.FileDiffer

将头文件拷贝到main/jni目录下。同时将bspatch.c文件和bzip2文件夹拷贝jni目录下。为了能执行bspatch.c文件,我们将其main方法改造成普通的方法如applyPatch。然后添加我们在头文件中生成的方法fileCombine,组装后参数后转调用applyPatch方法。
改造后的bspatch.c文件如下:

#if 0
__FBSDID("$FreeBSD: src/usr.bin/bsdiff/bspatch/bspatch.c,v 1.1 2005/08/06 01:59:06 cperciva Exp $");
#endif
//这里引入我们自动生成的头文件
#include <com_zzc_androidtrain_apk_patch_FileDiffer.h>
#include "bzip2/bzlib.c"
#include "bzip2/crctable.c"
#include "bzip2/compress.c"
#include "bzip2/decompress.c"
#include "bzip2/randtable.c"
#include "bzip2/blocksort.c"
#include "bzip2/huffman.c"
#include <err.h>
#include <unistd.h>
#include <fcntl.h>
#include "../../../../../../../tools/sdk/android-sdk/ndk-bundle/platforms/android-23/arch-arm/usr/include/jni.h"

//main方法改为普通方法 applyPatch
int appplyPatch(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;
    ...
    省略部分代码
    ...

//添加头文件中我们自定义的方法,并构造所需参数。
jint JNICALL Java_com_zzc_androidtrain_apk_1patch_FileDiffer_fileCombine
        (JNIEnv *env, jclass clazz, jstring old, jstring new, jstring patch) {
    //构造参数数组调用applyPatch方法
    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 result = appplyPatch(argc, argv);
    //释放资源
    (*env) -> ReleaseStringUTFChars(env,old,argv[1]);
    (*env) -> ReleaseStringUTFChars(env,new,argv[2]);
    (*env) -> ReleaseStringUTFChars(env,patch,argv[3]);

    return result;
}

为了不用每次执行编译过程,我们可以使用ndk-build命令将其编译成so包。
在Application.文件中添加硬件平台种类:

APP_ABI := armeabi mips mips64 x86_64 x86 arm64-v8a armeabi-v7a

在Android.mk中指定模块名称和编译文件名称以及本地依赖路径:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := bspatch
LOCAL_SRC_FILES :=  bspatch.c

LOCAL_STATIC_LIBRARIES := \lbz2

LOCAL_C_INCLUDES += \ $(JNI_H_INCLUDE) bzip2

include $(BUILD_SHARED_LIBRARY)

完事之后在命令行中执行ndk-build命令,就会看到工程目录中多出了一个lib目录和obj目录。两个目录下都有对应平台的so包。随便拷贝一份至main/jniLibs目录下。

在FileDiffer.java文件中加载so包:

public class FileDiffer {

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

    public native int fileCombine(String oldFile, String newFile, String patchFile);

}

下面步骤就很简单了,新建一个线程执行fileCombine方法,传入旧apk文件路径、新apk文件路径、差分包文件路径。等待执行完成后唤起系统的应用安装器就可以了。

public static void installApk(Context context, String path) {
        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        i.setDataAndType(Uri.parse("file://" + path), "application/vnd.android.package-archive");
        context.startActivity(i);
    }

好了,上面就差量更新的所有关键步骤实现。相信大家一定有一些疑问,欢迎留言回复。

ps:实际应用,要为每一个低版本生成差分包,那岂不是很繁琐。
原理上是要单独生成差分包的,但是考虑到实际情况,80%用户使用的版本也就2、3个,只为主流版本生成差分包进行更新就可以了。使用人数较少的版本可以用全量更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值