Android开发之增量更新

一、使用场景

apk升级,节省服务器和用户的流量


二、原理

自从 Android 4.1 开始, Google Play 引入了应用程序的增量更新功能,App使用该升级方式,可节省约2/3的流量。

Android开发之增量更新_第1张图片

现在国内主流的应用市场也都支持应用的增量更新了,最常见的应用宝省流量更新。

增量更新的原理,就是将手机上已安装apk与服务器端最新apk进行二进制对比,得到差分包(即两个版本的差异文件),用户更新程序时,只需要下载差分包,并在本地使用差分包与已安装apk,合成新版apk。

例如,当前手机中已安装微博V1,大小为12.8MB,现在微博发布了最新版V2,大小为15.4MB,我们对两个版本的apk文件差分比对之后,发现差异只有3M(可能更小,因为得到差异文件后内部还会使用压缩),那么用户就只需要要下载一个3M的差分包,使用旧版apk与这个差分包,合成得到一个新版本apk,提醒用户安装即可,不需要整包下载15.4M的微博V2版apk。


三、过程

此过程需要服务器和客户端写作完成

服务器:拿到最新版的apk,called new.apk,旧版本的apk,called old.apk,通过增量更新技术得到差分文件,called patch.patch。

客户端:通过网络操作去服务器下载已准备好的差分文件patch.patch,找到data目录下当前版本的old.apk,通过增量更新技术合并这两个文件,得到new.apk。


四、实例讲解

上述过程需要用到服务器得到差分文件,但是尴尬了,我不会,所以我使用投机取巧的方式,新建一个工程通过NDK去得到差分文件,然后通过在另外一个项目中通过NDK去合并,当然无论是服务器还是通过模拟我们最终是需要知道是怎么的过程具体实现。

4.1 准备工作

1.NDK开发技术,还不会的,或者会的欢迎查看我NDK相关文章:点击进入

2.我们自己需要准备两个apk,一个old.apk,一个new.apk,用于模拟服务器进行差分,old.apk只是用了TextView显示当前为1.0的旧版本,在old.apk的基础上new.apk在当前目录下的res/drawable-hdpi目录下增加了一些图片以便模拟容量扩从,并将版本号修改为2,TextView显示为2.0新版本,这样就简单的得到了2个新旧版本。当然,这个操作非常简单,因此我们提供了资源下载:old.apk与new.apk下载。

3.用于差分new.apk与old.apk以及合并old.apk与patch.patch的bsdiff文件,又因为bsdiff依赖bzip2,所以我们还需要用到 bzip2
bsdiff中,bsdiff.c 用于生成差分包,bspatch.c 用于合成文件。这些文件都是c语言写的,所以需要使用NDK技术: 本地下载

4.2 模拟服务器得到差分文件patch.patch

注以下开发环境都是eclipse。

1.新建Android项目,随后new一个source folder 命名为jni,并做好此项目的NDK配置,这里不讲解,

2.然后将准备工作下载的bsdiff中的bsdiff.c,bsdiff.h,bzip2文件夹拷贝到jni目录下,

3.新建类,类名当然随意,用于加载动态库以及生成native方法,如下:

package com.example.serverpatch;

public class PatchAPK {
    public native static int patchAPK(String oldFile,String newFile,String patchFile);
    static{
        System.loadLibrary("server_patch");
    }
}

当然这里的server_patch必须与后面Android.mk文件中的so库名字一致。并且patchAPK方法需要3个参数,分别为旧apk的路径,新apk的路径,差分文件的路径。

4.通过cmd定位到此项目的src目录下,输入javah +3步骤中创建的类包名,例如:

javah com.example.serverpatch.PatchAPK

然后刷新项目,可以看到:
这里写图片描述

将其也复制到jni目录下

5.将com.example.serverpatch.PatchAPK.h中需要实现的方法复制到bsdiff.c中实现:
Android开发之增量更新_第2张图片
当然这里需要给这些JNIEnv,变量命名久不用说了吧。

然后我们看看具体怎么实现:

JNIEXPORT jint JNICALL Java_com_example_serverpatch_PatchAPK_patchAPK
(JNIEnv *env, jclass cls, jstring oldFile, jstring newFile, jstring patchFile){
    int argc=4;
    char *argv[argc];
    argv[0] = "bsdiff";
    argv[1] = (char*) ((*env)->GetStringUTFChars(env, oldFile, 0));
    argv[2] = (char*) ((*env)->GetStringUTFChars(env, newFile, 0));
    argv[3] = (char*) ((*env)->GetStringUTFChars(env, patchFile, 0));

    printf("old apk = %s \n", argv[1]);
    printf("new apk = %s \n", argv[2]);
    printf("patch = %s \n", argv[3]);

    int ret = diff_main(argc, argv);

    printf("diff_main result = %d ", ret);

    (*env)->ReleaseStringUTFChars(env, oldFile, argv[1]);
    (*env)->ReleaseStringUTFChars(env, newFile, argv[2]);
    (*env)->ReleaseStringUTFChars(env, patchFile, argv[3]);

    return ret;
}

生成差分文件我们需要用到是bsdiff.c中的main方法,因为我将其修改为diff_main,所以大家不必郁闷哪里的diff_main()函数,main函数需要两个参数,第一个固定为4,第二个为 char* 数据,但是我们传入的是Java中的string,所以我们首先通过NDK中的GetStringUTFChars将其转为 char* ,因为java中有GC,但是C语言必须自己释放通过ReleaseStringUTFChars释放。

6.将bsdiff.c中的 < bzlib.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"

因为此时的bzlib在本地,所以使用双引号。

6.编写Android.mk以及Application.mk。这里不贴出。只要记住生成的.so和加载的.so名称一致。

最终的项目样式如下:
Android开发之增量更新_第3张图片

首先在MainActivity不做任何事情,通过模拟器运行一把项目,我是通过模拟器。真机可以不用运行,因为我只是想将old.apk和new.apk放到模拟器的根目录下,不行拿不到它的根目录了啊,运行完成后将准备好的old.apk,与new.apk放到SDcard目录下,如我的模拟器:
Android开发之增量更新_第4张图片

可以看到new.apk有5M,old.apk有1M,如果不使用增量更新,现在我们需要使用5M的流量更新。

好的,现在在MainActivity中调用PatchAPK 类中的navtive方法:

package com.example.serverpatch;

import java.io.File;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getPatchAPK();
    }

    private void getPatchAPK() {
         if (Environment.getExternalStorageState() != null) {
             File sdFile=Environment.getExternalStorageDirectory();
             String sdString=sdFile.getAbsolutePath();
             PatchAPK.patchAPK(sdString+"/old.apk", sdString+"/new.apk", sdString+"/patch.patch");
         }

    }
}

我们找到刚才 放置于SD根目录下的new.apk与old.apk的目录,并添加差分文件目录,当然调用
PatchAPK.patchAPK(sdString+”/old.apk”, sdString+”/new.apk”, sdString+”/patch.patch”);
需要在子线程中,这里没有演示,主要理解原理。最后运行项目。

可以看到SD的根目录下多了一个patch.patch文件:
Android开发之增量更新_第5张图片

可以看到只有3M多点,到了这里我们模拟服务器拿到差分文件已经完成。

4.3 客户端合差分文件

到了这里离成功已经特别近了,所以我们不能气馁!

1.新建客户端项目,随之后面的操作和刚才模拟模拟服务器制作差分文件的步骤几乎是一致,所以我们不用担心代码量,可以一直复制,毕竟这是程序员必备技能。与模拟服务器中不同的是:

  • 将bspatch.c,bspatch.h放到jni目录下,而不是bsdiff系列,当然bzip2文件
    夹仍保留
  • 新建类hebingAPK,当然我这里命名不规范,别介意,关键看原理:
package com.example.patch;

public class hebingAPK {
    public native static int hbAPK(String oldFile,String newFile,String patchFile);
    static{
        System.loadLibrary("client_patch");
    }
}

通过javah命令生成头文件,在bspatch.c中实现具体合并,

JNIEXPORT jint JNICALL Java_com_example_patch_hebingAPK_hbAPK
(JNIEnv *env, jclass cls,jstring old, jstring new, jstring patch){
    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));

        printf("old apk = %s \n", argv[1]);
        printf("patch = %s \n", argv[3]);
        printf("new apk = %s \n", argv[2]);

        int ret = hb_main(argc, argv);

        printf("patch result = %d ", ret);

        (*env)->ReleaseStringUTFChars(env, old, argv[1]);
        (*env)->ReleaseStringUTFChars(env, new, argv[2]);
        (*env)->ReleaseStringUTFChars(env, patch, argv[3]);
        return ret;
}

可以看到合并和差分的代码NDK代码几乎一致,只是这里使用的main函数为bspatch.c中的main,将其修改为hb_main函数,最后当然也需要将将bsdiff.c中的 include < bzlib.h >导包修改。

最终项目目录样式:
Android开发之增量更新_第6张图片

最后在MainActivity中调用native代码:

public class MainActivity extends Activity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);
        getPatchAPK();
    }

    private void getPatchAPK() {
         if (Environment.getExternalStorageState() != null) {
             File sdFile=Environment.getExternalStorageDirectory();
             String sdString=sdFile.getAbsolutePath();
             hebingAPK.hbAPK(sdString+"/old.apk", sdString+"/new2.apk", sdString+"/patch.patch");
         }

    }
}

当然,这里合并和差分一样也是需要在子线程中进行,这里也不演示了,只要看原理。

等会我们就需要看生成的new2.apk是否可以安装并且大小是否和new.apk是否一样,如果有必要可以通过校验新合成的apk的MD5或SHA1是否正确,如正确,则引导用户安装,当然我们这里不演示这个。

最后运行,模拟器根目录如下:
Android开发之增量更新_第7张图片

可以看到new2.apk成功生成了,好的到此我们的差分和合并全部完成,觉得可以的点个赞好吗,谢谢。

4.3 安装new2.apk

当然这一部是最简单的,通过adb命令去安装。

首先将new2.apk pull到电脑中,打开CMD,定位到new2.apk目录下,通过adb install new2.apk将其安装:
Android开发之增量更新_第8张图片

可以看到安装成功,打开模拟器也是可以使用,说明这一套的差分和合并是成功的。


最后提供一下两个过程的源码:gitHub下载

贴个学习路径

你可能感兴趣的:(Android知识汇总)