一、NDK:增量更新

增量更新在Android开发中是一种很常见的技术。

增量更新的原理

增量更新的原理非常简单,就是将本地apk与服务器端最新版本比对,并得到差异包。用户更新App时只需要下载差异包即可。
关键有两点:

  1. 根据新旧版本的APK生成差异包
  2. 使用差异包和旧版的APK合并成新版APK

生成差异包使用的是一个C++的开源库:bsdiff
百度第一项就是

bsdiff

由于我们是在windows环境下开发,所以下载windows的版本

点击红色方框处
下载源码
解压后

实验

直接使用不确定因素太多,所以先实验后再使用,这里先使用Eclipse和Visual Studio来实验bsdiff是否可以完成拆分和合并的工作,并熟悉流程。

准备工作

生成新旧两个版本的APK。
旧版的APK需要完成差异包和旧版APK合并的工作,所以需要用到NDK开发,这一块放在后面详细介绍。
这里使用最普通的APK,先介绍如何生成差异包。

生成差异包

  1. 新建Eclipse工程 BsDiffTest
    该工程用来调用C/C++的代码。核心代码是C/C++。此处主要是Jni的调用。

  2. 新建Visual Studio的C++工程 BsDiffTest。
    该工程是用来生成dll动态库(windows环境)或者so动态库(linux环境)。

  3. 复制bsdiff的源码到VS工程的工程目录下。并删除bspatch.cpp
    该文件是用于合并的,在生成差异包的时候使用的是bsdiff.cpp


    bspatch.cpp
  4. 复制 jni.h和jni_md.h到VS工程的工程目录下。

  5. Eclipse工程中新建类

     package com.relengxing;
     public class BsDiff {
     static{
       System.loadLibrary("BsDiffTest");
     }
     /**
      * 调用本地方法生成差异包
      * @param oldfile
      * @param newfile
      * @param patchfile
      */
     public native static void diff(String oldfile,String newfile,String patchfile);
     }
    

在main方法中调用

    package com.relengxing;

    public class BsDiffTest {

    public static final String OLD_APK_PATH = "D:/apk/app_old.apk";

    public static final String NEW_APK_PATH = "D:/apk/app_new.apk";

    public static final String PATCH_PATH = "D:/apk/apk.patch";

    public static void main(String[] args) {
    
        BsDiff.diff(OLD_APK_PATH, NEW_APK_PATH, PATCH_PATH);
    
        }
    }
  1. 使用javah生成BsDiff类的头文件。如果此处不会请参考Jni系列,里面有详细说明。
    把生成的头文件复制到VS的工程目录
    修改#include<jni.h>为#include“jni.h”
    把头文件中定义的方法赋值到bsdiff.cpp的最后并实现。

     JNIEXPORT void JNICALL Java_com_relengxing_BsDiff_diff
     (JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr) {
         int argc = 4;
         char *argv[4];
    
         char *oldfile = (char *)env->GetStringUTFChars(oldfile_jstr, NULL);
         char *newfile = (char *)env->GetStringUTFChars(newfile_jstr, NULL);
         char *patchfile = (char *)env->GetStringUTFChars(patchfile_jstr, NULL);
    
         argv[0] = "bsdiff";
         argv[1] = oldfile;
         argv[2] = newfile;
         argv[3] = patchfile;
    
         bsdiff_main(argc, argv);
    
         env->ReleaseStringUTFChars(oldfile_jstr, oldfile);
         env->ReleaseStringUTFChars(newfile_jstr, newfile);
         env->ReleaseStringUTFChars(patchfile_jstr, patchfile);
     }
    
  2. 修改bsdiff.cpp中的main方法为bsdiff_main
    在Java_com_relengxing_BsDiff_diff中调用bsdiff_main()方法,
    该方法需要传递两个参数

     int bsdiff_main(int argc,char *argv[])
    

int argc,char *argv[];
在源码中可以找到以下代码,意思就是argc必须等于4,否则就报错

    if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);在源码中依次可以找到如下注释

而在argv[]中第一个参数是无用的,随便填什么都可以,第二个参数是旧版Apk的地址,第三个参数是新版Apk的地址,第四个参数是生成的差分包的地址。

    /* Allocate oldsize+1 bytes instead of oldsize bytes to ensure
    that we never try to malloc(0) and get a NULL pointer */
    //org:
    //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]);
    //new:
    //Read in chunks, don't rely on read always returns full data!
        
    /* Allocate newsize+1 bytes instead of newsize bytes to ensure
    that we never try to malloc(0) and get a NULL pointer */
    //org:
    //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]);
    //new:
    //Read in chunks, don't rely on read always returns full data!

    /* Create the patch file */
    //org:
    //if ((pf = fopen(argv[3], "w")) == NULL)     
    //new:
    //if((fd=open(argv[3],O_CREAT|O_TRUNC|O_WRONLY|O_BINARY|O_NOINHERIT,0666))<0)
  1. 工程中有很多错误,我们依次来解决。
    根据自己的环境选择生成dll动态库
    编译工程


    错误1

    当遇到以上错误时,在解决方案的属性其他选项中添加
    -D _CRT_SECURE_NO_WARNINGS


    解决方案1

    然后再编译:
    错误2

    当遇到以上错误时,在解决方案的属性其他选项中添加
    -D _CRT_NONSTDC_NO_DEPRECATE
    解决方案2

    再编译:


    错误3

    遇到以上错误时把SDL检查选为否
    解决方案3

    再编译:成功生成dll动态库
    dll动态库
  2. 自行修改环境变量,在jni系列有详述。
    然后在eclipse中运行。运行需要一段时间,运行完成后在我们设置的文件夹下可以查找到一下文件图中apk.patch就是差异包


    差异包

合并差分包(windows端)

合并差分包是在手机端做的事,这里先在windows端实验一遍,合并和生成差分包有相似之处,这里就简单的写了。

  1. 同样新建Eclipse工程BsPatchTest。
  2. 新建VS工程BsPatchTest。
  3. 复制bsdiff的源码到VS工程的工程目录下。并删除bsdiff.cpp该文件是用于合并的,在生成差异包的时候使用的是bspatch.cpp
  4. 复制 jni.h和jni_md.h到VS工程的工程目录下。
  5. Eclipse工程中新建类
  6. 使用javah生成BsDiff类的头文件
  7. 修改bspatch.cpp中的main方法为bspatch_main
    在Java_com_relengxing_BsDiff_diff中调用bsdiff_main()方法,
  8. 工程中有很多错误,我们依次来解决。
  9. 自行修改环境变量,在jni系列有详述。

头文件的代码复制到bspatch.cpp中实现:

JNIEXPORT void JNICALL Java_com_relengxing_BsPatch_patch
(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr) {
    int argc = 4;
    char* oldfile = (char*)env->GetStringUTFChars(oldfile_jstr, NULL);
    char* newfile = (char*)env->GetStringUTFChars(newfile_jstr, NULL);
    char* patchfile = (char*)env->GetStringUTFChars(patchfile_jstr, NULL);

    //参数(第一个参数无效)
    char *argv[4];
    argv[0] = "bspatch";
    argv[1] = oldfile;
    argv[2] = newfile;
    argv[3] = patchfile;

    bspatch_main(argc, argv);

    env->ReleaseStringUTFChars(oldfile_jstr, oldfile);
    env->ReleaseStringUTFChars(newfile_jstr, newfile);
    env->ReleaseStringUTFChars(patchfile_jstr, patchfile);

}

把之前的文件名修改一下

运行前

运行eclipse工程

运行后

经过比较,生成的文件和之前的文件大小一样。
所以使用bsdiff来做增量更新是可以实现的。

实际使用

服务器端

服务器端有各种语言,使用bsdiff的话,核心代码还是C/C++,最后就是和java一样调用C/C++。由于我不懂服务器端的技术。。所以这里使用一个最偷懒的办法,手动生成差分包,然后把差分包放在tomcat里,这样就可以在局域网内访问了。

客户端

Android客户端需要实现几个功能

  • 下载差分包
  • 合并差分包
  • 安装合并后的APK

这里我是使用Android Studio 2.2 Bate2开发的

新建工程

如图勾选此处


支持C++
开发界面

关于CMakeLists.txt这个文件我也不是很懂,反正前面是#的就是注释掉的。

这一行和版本号有关系

cmake_minimum_required(VERSION 3.4.1)

这一段大概意思就是创建并命名一个库,设置它是STATIC或者SHARED,并提供了其源代码的相对路径,你可以定义多个库,CMake会为你构建,Gradle自动打包共享库给你的apk。
这里可以把下面这段的native-lib和src/main/cpp/native-lib.cpp改成自己的

    add_library( # Sets the name of the library.
         native-lib

         # Sets the library as a shared library.
         SHARED

         # Provides a relative path to your source file(s).
         # Associated headers in the same location as their source
         # file are automatically included.
         src/main/cpp/native-lib.cpp )

后面还有两段这里先不研究了,以后如果有研究再写下来。

代码搬运工

Android是Linux的。
所以bsdiff要下载linux版本的

bsdiff
解压后

这里只有两个文件是有用的,就是两个点C文件。
bsdiff用来生成差分包,bspatch用来合并差分包。
但是只有这个.c文件是不够的,这个项目还依赖bzip2,所以再去把bzip2下载下来,这里把bspatch.c复制到工程的Cpp目录下。删除之前的native-lib.cpp。
把下载下来的bzip2源码解压出来,把所有的.c和.h文件复制粘贴到cpp目录下新建一个bzip2文件夹中。
修改CMakeLists.txt中下面两段,同步一下。


Cmakelists.txt
Cmakelists.txt

修改要加载的动态库


加载动态库

在bspatch.c中添加如下文件,至于为什么是这些文件,听说是多编译几次就知道了。

#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"

动态库源码的搬运工作到这里就结束了,剩下的就得自己写代码了。

写调用jni的类

新建一个类BsPatch

BsPatch

patch的方法名是红色的,是因为定义了该方法,但是在本地的方法中却找不到,这时按Alt+Enter快捷键

按Alt+Enter快捷键
jni\bspatch.c

这里重新生成了一个jni的文件夹,在jni的文件夹下又生成了一个bspatch.c的文件。
最上面那行提示代码的意思是这个文件没有在这个工程中,加我们添加到工程里面去......不理他,只复制我们要的东西。把这个里面的东西复制到cpp\bapatch.c的最后。和之前一样修改main为bspatch_main,在本地方法中调用

调用

至于怎么下载,怎么安装这块就不详细说了,具体的到时候看Git的代码好了。核心的代码就是合并这块,合并就是调用刚刚写的这个函数,然后把三个地址传递进来。

推荐阅读更多精彩内容

  • 最近就是在练习ndk开发,刚好遇到android增量更新的话题,主要是工具的运用,略带使用第三方so库的流程~~~...
    红黑军团号阅读 208评论 0 3
  • 上一节我们学习了NDK来处理文件的拆分和合并操作,那时候我们纯手工来敲C语言的代码,今天我们来用C语言代码搞搞ND...
    Lypop阅读 70评论 0 0
  • 前言 有关APK更新的技术比较多,例如:增量更新,插件式开发,热修复,RN静默安装。下面简单介绍一下: 增量更新:...
    小楠总阅读 1,005评论 2 8
  • 版权声明:本文为LooperJing原创文章,转载请注明出处! 前言 如果要对一个 APP 进行更新,你会怎么做呢...
    LooperJing阅读 1,776评论 0 29
  • 注:首发地址 0. 前言 如果只学理论,不做实践,不踩踩坑,一般很难发现真正实践项目中的问题的,也比较难以加深对技...
    cfanr阅读 5,560评论 4 47