Android增量更新从入坑到成功

作者: Coding小学生 | 来源:发表于2019-01-04 19:10 被阅读7次

    原文链接

    流程

    • step1 使用bsdiff生成差异包PATCH.patch
    • step2 在手机上合并base包和差异包,生成新版本的安装包
    • step3 安装新的安装包

    准备

    1. bsdiff-4.3 (用于生成差异包,合并新包)
    2. bzip2 (bsdiff要使用到)

    试验

    • step1 解压bsdiff4.3的压缩包
    • step2 修改Makefile文件,将.ifndef.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
    
    • step3 在该文件夹的命令行里执行make命令,会生成bsdiffbspatch两个可执行的文件。如果是从上面的地址下载的bsdiff的话,会爆出bspatch.c:39:21: error: unknown type name 'u_char'; did you mean 'char'?这样的错误。这是由于bspatch.c中缺少了#include <sys/types.h>,加上即可。然后执行make命令。
    bogon:bsdiff-4.3 Tony$ make
    cc -O3 -lbz2    bsdiff.c   -o bsdiff
    cc -O3 -lbz2    bspatch.c   -o bspatch
    
    • step4 准备两个同一签名的apk,old.apk.apk,放在bsdiff-4.3解压后的文件夹里。
    • step5 使用bsdiff生成差异文件PATCH.patch,命令格式:bsdiff oldfile newfile patchfile(这一步操作可以放在服务端来执行)
    bogon:bsdiff-4.3 Tony$ ./bsdiff old.apk new.apk PATCH.patch
    

    -step6 使用bspatch合成新的apk,dest.apk。命令格式:bspatch oldfile newfile patchfile

    bogon:bsdiff-4.3 Tony$ ./bspatch old.apk  dest.apk PATCH.patch 
    
    • step7 验证生成的dest.apk和之前的new.apk使用一样,验证两个apk的md5值,即可。
    bogon:bsdiff-4.3 Tony$ md5 new.apk 
    MD5 (new.apk) = 55005cec2de8ad3668a0fd5bd8746f43
    bogon:bsdiff-4.3 Tony$ md5 dest.apk 
    MD5 (dest.apk) = 55005cec2de8ad3668a0fd5bd8746f43
    bogon:bsdiff-4.3 Tony$ md5 old.apk 
    MD5 (old.apk) = 822cc0938694008089ea5523f86585d7
    bogon:bsdiff-4.3 Tony$ 
    

    在Android中使用增量更新

    ndk部分

    • step1 新建一个PatchUtils的类,用于调用native的方法
    public class PatchUtils {
        static PatchUtils instance;
    
        public static PatchUtils getInstance() {
            if (instance == null) {
                instance = new PatchUtils();
            }
            return instance;
        }
    
        static {
            System.loadLibrary("patchutils");
        }
    
        /**
         * native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
         * <p>
         * 返回:0,说明操作成功
         *
         * @param oldApkPath 示例:/sdcard/old.apk
         * @param newApkPath 示例:/sdcard/new.apk
         * @param patchPath  示例:/sdcard/xx.patch
         * @return
         */
        public static native int bspatch(String oldApkPath, String newApkPath, String patchPath);
    
    }
    
    • step2 使用javah命令生成PatchUtils的头文件,或者使用ndk-build里的javah生成头文件。会在jni文件夹下生成一个com_bigademo_updatedemo_PatchUtils.h的文件,其中com_bigademo_updatedemo是我的包名。

    • step3 将bsdiff4.3文件夹中的bspatch.c文件导入到jni文件夹下。

    • step4 将bzip2文件夹导入到jni文件夹下

    • step5 删除bzip2中的除了以c和h结尾的其他文件
      如图:

      jni文件夹目录
    • step6 修改bspatch.c。引入bzip的文件,引入com_bigademo_updatedemo_PatchUtils.h,在bspatch.c中重写JNIEXPORT jint JNICALL Java_com_bigademo_updatedemo_PatchUtils_bspatch方法。代码如下

    #if 0
    __FBSDID("$FreeBSD: src/usr.bin/bsdiff/bspatch/bspatch.c,v 1.1 2005/08/06 01:59:06 cperciva Exp $");
    #endif
    
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <err.h>
    #include <unistd.h>
    #include <fcntl.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 <com_bigademo_updatedemo_PatchUtils.h>
    JNIEXPORT jint JNICALL Java_com_bigademo_updatedemo_PatchUtils_bspatch(JNIEnv *env,
            jclass cls, jstring old, jstring new, jstring patch) {
        int argc = 4;
        char * argv[argc];
        argv[0] = "bsdiff";
        argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
        argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
        argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));
    
        printf("old apk = %s \n", argv[1]);
        printf("new apk = %s \n", argv[2]);
        printf("patch = %s \n", argv[3]);
    
        int ret = genpatch(argc, argv);
    
        printf("genDiff result = %d ", ret);
    
        (*env)->ReleaseStringUTFChars(env, old, argv[1]);
        (*env)->ReleaseStringUTFChars(env, new, argv[2]);
        (*env)->ReleaseStringUTFChars(env, patch, argv[3]);
    
        return ret;
    }
    
    
    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 genpatch(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;
    }
    
    • step7 在jni下创建Android.mk和Application.mk两个文件

    Android.mk

    # 构建系统提供的宏函数 my-dir 将返回当前目录(包含 Android.mk 文件本身的目录)的路径,基本上是固定的,不需要去动
    LOCAL_PATH := $(call my-dir)
    # 会清除很多 LOCAL_XXX 变量,不会清除 LOCAL_PATH,基本上是固定的,不需要去动
    include $(CLEAR_VARS)
    # 需要构建模块的名称,会自动生成相应的 libNDKSample.so 文件,每个模块名称必须唯一,且不含任何空格
    LOCAL_MODULE := patchutils
    # 包含要构建到模块中的 C 或 C++ 源文件列表
    LOCAL_SRC_FILES := bspatch.c
    LOCAL_C_INCLUDES := /Users/Tony/Code/android/AndroidStudioProjects/demo/updatedemo/app/src/main/jni/bzip2
    # 帮助系统将所有内容连接到一起,固定的,不需要去动
    include $(BUILD_SHARED_LIBRARY)
    

    Application.mk

    APP_PLATFORM := android-16
    # 选择不同的 ABI,多个使用空格作为分隔符,全部是all
    APP_ABI := all
    
    • step8 在module中的build.gradle中配置ndk。
    android{
        ...
        defaultConfig{
            ndk{
                //这个名字就是在PatchUtils中loadLibrary的名字,同时和Android.mk中的LOCAL_MODULE的名字一样,也和生成的so的文件的名字类似。
                moduleName "patchutils"
            }
        }
        externalNativeBuild {
            ndkBuild {
                path "src/main/jni/Android.mk"
            }
        }
    }
    

    java部分

    创建一个ApkExtract类,用于获取当前app的apk和安装新的apk。因为涉及到Android 7.0的,需要处理Provider的问题,同时在AndroidManifest.xml增加一些代码

    public class ApkExtract {
        public static String extract(Context context) {
            context = context.getApplicationContext();
            ApplicationInfo applicationInfo = context.getApplicationInfo();
            String apkPath = applicationInfo.sourceDir;
            Log.d("info", apkPath);
            return apkPath;
        }
        public static void install(Context context, String apkPath) {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                Uri contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", new File(apkPath));
                intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
            } else {
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.setDataAndType(Uri.fromFile(new File(apkPath)),
                        "application/vnd.android.package-archive");
            }
            context.startActivity(intent);
        }
    }
    

    androidmanifest.xml

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
                <provider
                android:name="android.support.v4.content.FileProvider"
                android:authorities="${applicationId}.fileProvider"
                android:grantUriPermissions="true"
                android:exported="false">
                <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths" />
      ...
      </application>
    

    最后在需要更新的时候调用下面这个方法,当然先做一下动态调用读写权限的操作。

    if (ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                        ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, 2);
                    } else {
                        doBspatch();
    //                    startActivity(new Intent(MainActivity.this, SecondActivity.class));
                    }
    
     private void doBspatch() {
            final File destApk = new File(Environment.getExternalStorageDirectory(), "dest.apk");
            final File patch = new File(Environment.getExternalStorageDirectory(), "PATCH.patch");
    
            //一定要检查文件都存在
          int result=  PatchUtils.bspatch(ApkExtract.extract(this),
                    destApk.getAbsolutePath(),
                    patch.getAbsolutePath());
            Log.e("info","patch result   "+result);
            if (destApk.exists()) {
                ApkExtract.install(this, destApk.getAbsolutePath());
            }
        }
    

    demo项目:https://github.com/yyt231/updatedemo

    巨人的肩膀:
    https://blog.csdn.net/lmj623565791/article/details/52761658
    https://www.cnblogs.com/lping/p/5833090.html
    https://blog.csdn.net/qq_33750826/article/details/75540738

    相关文章

      网友评论

        本文标题:Android增量更新从入坑到成功

        本文链接:https://www.haomeiwen.com/subject/zdikrqtx.html