Android 应用升级方案

转:http://www.jianshu.com/p/abbaddcf449f

http://blog.csdn.net/u013718120/article/details/53018801


一、全量升级

全量升级Demo

现在很多的App中都会有一个检查版本的功能。例如斗鱼TV App的设置界面下:

                 

当我们点击检查更新的时候,就会向服务器发起版本检测的请求。一般的处理方式是:服务器返回的App版本与当前手机安装的版本号进行对比。

(1)如果服务器所返回的版本号大于当前App版本号那么此时手机所安装的App不是最新版。可以提示用户升级。

(2)如果不大于当前版本号,可以提示用户为最新版本:

               

版本升级,也分为两种处理方式:

(1)跳转到App某市场(例如:360手机助手),然后根据包名在市场定位到该App,通过市场下载更新安装。

(2)在本App中实现Apk下载,下载完成后更新安装。

本篇博客的内容将围绕如何实现Apk下载,下载完成后更新安装来展开。

下面我将内容大致分为以下几个部分:

(1)App版本检测

(2)Apk下载

(3)Apk更新安装

(4)对以上功能进行封装

基于以上4部分,我们逐一展开。

1.App版本检测:

要实现App的更新下载,我们上面介绍了,前提是服务器要保存一个App的版本号(通常的方式是保存versionCode,当然你要对比versionName也没关系)。当用户去手动检测版本,或者进入首页自动检测时,第一步是需要请求服务器的版本号,拿到版本号之后与当前App版本号(当前版本号可通过PackageInfo获取)进行对比。服务器返回的版本号大于当前App版本号,证明App已经有更新,那么进入第2步。

2.Apk下载

Apk文件是保存在服务器的。我们可以通过Http流将其下载到本地手机,然后更新安装。Android中下载的方式很多种:HttpUrlConnection,Retrofit,okHttp,以及Android原生的下载工具类DownLoadManager 等等。我们采用的方式是Google推荐的下载工具类DownLoadManager。关于DownLoadManager的使用其实很简单,简单概括如下:

(1)通过getSystemService获取DownLoadManager。

(2)初始化DownLoadManager的Request,构建下载请求。

(3)调用DownLoadManager的enqueue异步发起请求,该方法返回值为标识当前下载任务的id,即downloadId。

(4)当下载完成后,系统会发出条件为android.intent.action.DOWNLOAD_COMPLETE的广播,我们可以自定义广播接受器,然后在onReceive中处理下载完成的逻辑即可。

详细使用方式大家可以参考网上的教程,此处就不再赘述。

上面通过下载啰嗦了一堆。此时我们要想一个问题:当我们下载完成后,并没有安装。当用户再次进入App时该如何操作?

有朋友会说,那就再去下载一次,然后继续执行更新安装呀!哈哈,这种方式是没有错误的,但是如果用户恶意行为,每次下载完成都不安装,那我们岂不是每次都要去下载100次,1000次。。(然后手机boom!!!)这种方式肯定是不能采用的。那么我们该如何解决呢?

很简单,当我们在下载之前,先去指定的文件夹下查看有木有已经下载好的Apk,并且该Apk的版本是高于本App的版本,此时我们就去执行安装操作。如果上面条件不成立,此时再去执行下载操作。

3.Apk更新安装

相信大家对于如何安装一个Apk都比较熟悉吧,原理也是比较简单的。

(1)通过downloadId获取下载的Uri。

(2)将Uri设置到Itent的setDataAndType作为启动条件。

(3)调用startActivity启动对应Intent即可。

以上3步,即可完成App的更新功能。

整体的流程很清晰:

版本检测 → Apk下载 (检查是否存在未安装的Apk) → Apk安装 → 完成更新

下面,通过代码来具体分析整个流程:

关于App版本检测其实就是一个Http请求,不再多说。我们从Apk下载开始:

上面我们提到,在下载之前需要去检测是否存在已经下载的Apk。通过什么获取呢?没错,肯定是downloadId了。

1> 如果存在downloadId,那么我们通过downloadId获取当前下载的状态status。status分为成功,失败两种状态。

(1)当status为成功状态时,即已经下载完成,我们就通过downloadId获取下载文件的Uri。然后可以通过Uri获取PackageInfo,与当前App进行包名和版本号的对比,当包名相同,并且当前版本号是小于下载的Apk版本号两个条件同时成立时,直接执行安装操作。否则,执行remove,通过downloadId删除下载任务以及文件,继续执行下载。

(2)当status为失败状态时,即下载未完成,我们就直接执行重新下载即可。

2> 如果不存在downloadId,即没有下载过Apk,执行下载即可。

核心代码如下:

下载完成后,系统会发出广播,在广播中,我们对比downloadId是否相同,相同情况下,直接通过downloadId获取Uri,然后跳转到安装界面,提示用户安装即可:


所以,别忘了在下载之前要先将该大喇叭(广播接受器)注册。

最后,当我们安装完成后,再次进入App,就将其已下载的Apk文件进行删除(将该方法放在onCreate生命周期中即可):


上面通过downloadApk获取下载文件的地址。downloadApk地址是在下载完成后广播接收器中保存的。

通过上面的步骤,我们就完成了App更新下载安装的全部工作。相信大家也有了更深的认识和理解。


二、以下为增量升级

增量升级原理

首先,两句话简单概括增量升级原理:

  1. 服务端通过比对最新升级包,和当前应用包,生成差分升级包;
  2. 客户端将差分升级包和当前应用包合并,生成最新升级包。

接下来,简单介绍下增量升级的原理:

  1. 首先,客户端获取当前应用的Version Code和应用APK文件的MD5值,发送给服务器;
  2. 服务器根据既定策略,给用户返回更新包信息。
    1. 通过MD5值没有查询到旧有APK应用信息,返回全量升级包网址,全量升级包MD5值;
    2. 需要返回patch包,但还没有生成patch包时,后台去生成patch包,并返回全量升级包网址,全量升级包MD5值;
    3. 需要返回patch包,并且已经生成patch包时,返回patch包网址,patch包MD5值,全量升级包网址,全量升级包MD5值;
    4. 不需要返回patch包,则返回全量升级包网址,全量升级包MD5值;
  3. 客户端根据返回信息进行更新操作。
    1. 如果只有全量升级包相关信息,则下载全量升级包,并在校验MD5值后,安装更新包;
    2. 如果有差分升级包(patch包),则下载差分升级包。校验差分升级包的MD5值。如果校验失败,走上面一个步骤。如果校验成功,则将差分升级包和当前版本的APK进行合并操作,生成新的应用包。校验新的应用包MD5值。校验通过,这安装这个生成的新应用包。如果校验失败,则走上面一个步骤。

以上只是简单介绍了增量升级的原理,实际应用中还需要细化,考虑更多的场景。

注意: 下载过程中,必须支持断点续传策略。防止网络不畅时,不断重试,造成的流量浪费。

增量升级方案

增量升级方案的核心就是使用Diff/Patch算法来对新旧APK进行diff/patch操作。
目前主流的Diff/Patch算法是bsdiff/bspatch,来自:http://www.daemonology.net/bsdiff/

另外,我了解到,国内有人开源了另外一种Diff/Patch算法,名字叫做HDiffPatch,来自:https://github.com/sisong/HDiffPatch

据说,比bsdiff/bspatch更高效呢?详见《HDiff1.0和BSDiff4.3的对比测试》

我将bsdiff/bspatch和HDiffPatch,使用jni封装成so库,供android调用。项目地址:https://github.com/snowdream/android-diffpatch
在封装HDiffPatch过程中遇到问题,得到作者@sisong的支持和帮助,在此表示感谢。

bsdiff/bspatch和HDiffPatch算法都是开源的,服务端可以根据源文件来进行编译集成。
这里我主要在Android客户端的角度,介绍下bsdiff/bspatch和HDiffPatch怎么使用。

BsDiffPatch

  1. 在build.gradle文件中自定义jnilib目录
    sourceSets {
     main {
         jniLibs.srcDirs = ['libs']
     }
    }
  2. 将 app/libs/armeabi-v7a/libbsdiffpatch.so 拷贝到你的工程对应目录下。
  3. 将 app/src/main/java/com/github/snowdream/bsdiffpatch 和 app/src/main/java/com/github/snowdream/diffpatch 拷贝到你的工程对应目录下,包名和文件名都不能改变。
  4. 在Java文件中参考以下代码进行调用。
    IDiffPatch bsDiffPatch = new BSDiffPatch();
    bsDiffPatch.init(getApplicationContext()); 
    //diff
    bsDiffPatch.diff(oldFilePath, newFilePath, diffFilePath);
    //patch
    bsDiffPatch.patch(oldFilePath, diffFilePath, gennewFilePath);

HDiffPatch

  1. 在build.gradle文件中自定义jnilib目录
    sourceSets {
     main {
         jniLibs.srcDirs = ['libs']
     }
    }
  2. 将 app/libs/armeabi-v7a/libhdiffpatch.so 拷贝到你的工程对应目录下。
  3. app/src/main/java/com/github/snowdream/hdiffpatch 和 app/src/main/java/com/github/snowdream/diffpatch 拷贝到你的工程对应目录下,包名和文件名都不能改变。
  4. 在Java文件中参考以下代码进行调用。
    IDiffPatch hDiffPatch = new HDiffPatch();
    hDiffPatch.init(getApplicationContext()); 
    //diff
    hDiffPatch.diff(oldFilePath, newFilePath, diffFilePath);
    //patch
    hDiffPatch.patch(oldFilePath, diffFilePath, gennewFilePath);

BsDiffPatch vs HDiffPatch

这里我选择高德地图Android客户端的两个版本来进行测试。

  • 测试来源:http://www.autonavi.com/
  • 测试版本: Amap_Android_V7.7.4.2128_GuanWang.apk 和 Amap_Android_V7.3.0.2036_GuanWang.apk (注:版本跨度大,差异大)
  • 对比算法: BsDiffPatch vs HDiffPatch
  • 测试结果:(详见下图)
    1. BsDiffPatch生成的patch包稍小。
    2. 两者的diff操作都非常耗资源,耗时间,无法忍受。(当然diff操作一般在服务端进行)
    3. 两者的patch操作都比较快。通过大概五次测试,BsDiffPatch的patch操作需要13s左右,而HDiffPatch的patch操作仅仅需要1s左右。

以上结果仅供参考。

  • 测试结论:
    1. BsDiffPatch应用更广泛
    2. HDiffPatch看起来更高效

test.png

扩展

以上算是比较成熟的增量升级方案了,但是仔细想想,可能还存在一些问题:

  1. 由于多渠道,多版本造成非常多Patch包
  2. bs diff/patch算法性能和内存开销太高
    第一个问题可以通过服务器策略进行限制。比如,只有最新版5个版本内的升级采用增量升级,其他的仍然采用全量升级。
    据说,还有一种更强大的算法,可以解决以上问题。大家有兴趣的话,可以自己去了解。


猜你喜欢

转载自blog.csdn.net/lzpdz/article/details/62423625