APK 如何实现应用热更新功能?

2019-10-22

关键字:APK 通过代码安装程序、APK 更新、打开APK安装程序、APK的升级


 

这篇文章是基于笔者的另一篇 APK 升级相关的文章(如何实现APK的升级功能)的改进版本。

 

主要改进的点如下:

1、新增了兼容高低版本系统安装APK功能

2、改进了定时检测流程;

3、改进了提示流程;

4、改进了存储逻辑,兼容多种类型手机,避免出现权限不足的问题。

5、区分了普通升级与强制升级功能。

 

1、概述

 

所谓热更新就是 APK 自动升级的一种较好听一点的说法而已。一款APK若能摆脱手机系统应用商店的依赖而仅依靠自己来实现升级,就称为热更新。

 

APK 的升级由于需要在程序运行期间,有些甚至在程序在后台运行时都要进行。因此通常都会把升级功能做在一个后台服务子线程上。

 

而我个人目前写代码的理念崇尚“专人专事”,用专业的话来说就是“高内聚”。因此我将整个升级相关的功能代码都封装在同一个 Service 代码文件中。虽然它内部其实夹杂着不少不同类型的功能代码,但所幸整个代码文件也并不庞大,同时对于升级功能的管理也特别轻松。

 

对于APK的升级,我个人之前从未参考过其他人的实现方法。我自己对于升级的理解就是有一个后台线程在程序运行期间不间断地定时向升级服务器检索升级信息,当有新版本时,发出提示供用户选择下载与否。并且我认为升级应该至少设置两种升级策略:一是普通升级。二是强制升级。普通升级不必多说,强制升级就是当检测到有强制升级包时,立即在后台下载该更新包,下载完毕后即弹出软件安装界面,并缩短强制升级检测时间间隔。若用户取消安装强制升级包,则在一个较短的时间内再次弹出安装界面。通过这种“死皮赖脸”的方式来强迫用户安装新版本软件。也许可以实现直接通过代码来安装软件的功能,但我并没有去研究,所以就姑且不讨论这种方式的强制升级了。

 

2、升级服务

 

笔者这边直接通过一个 UpgradeService 来实现升级的所有功能。

 

由于一个 Service 里要实现多种功能,比如:启停检测、发出提示、下载、安装等,因此这个 Service 定义了多种类型,如下图:

 

笔者的 Service 仅支持以 start 的方式启动,因此要求在启动 Intent 中设置类型以便于 Service 区分不同的动作,onStartCommand() 方法的实现如下图所示:

 

整个 Service 的架构倒是简单,就根据不同的类型参数干不同的活而已。下面逐一分析下这些类型的“活”。

 

3、升级检测

 

在 Serivce 中关于启动类型有两种:1、TYPE_START_CHECK;2、TYPE_STOP_CHECK。笔者关于升级检测的开启策略是:当应用不处于前台时即停止升级检测,回到前台运行又会重新开启升级检测。

 

首先,关于定时功能,这里使用的是 Android 自带的 android.os.CountDownTimer 来实现。由于这个类是抽象类,因此笔者在 Service 内部封装了一个 Timer 内部类来实现 CountDownTimer,并根据业务需要定制了一些功能,整个 Timer 内部类的代码如下:

private static class Timer extends CountDownTimer {

    private int timing;
    private int interval;

    private OnTimeOutCallback callback;

    Timer(int timingSeconds, int intervalSeconds, OnTimeOutCallback callback) {
        super(timingSeconds * 1000, intervalSeconds * 1000);
        timing = timingSeconds;
        interval = intervalSeconds;
        this.callback = callback;
    }

    @Override
    public void onTick(long l) {
        Logger.d(TAG, "onTick:" + l + "," + this);
    }

    @Override
    public void onFinish() {
        boolean ret = false;
        if(callback != null) {
            ret = callback.onTimeOut(timing, interval);
        }

        if(!ret) {
            start(); // Restart timing...
        }
    }

    interface OnTimeOutCallback {
        /**@return true 表示已处理过Timer的生命周期,Timer不要再重启计数线程。。*/
        boolean onTimeOut(int timingSeconds, int countDownIntervalSeconds);
    }

} // class Timer -- end.

在这个 Timer 类中,定时完成事件是通过回调的方式来通知的,并且在默认情况下定时结束回调过后就会重启定时器进行二次定时计数。

 

然后是在 Service 类内部又封装了一个内部类专门负责普通升级检查工作。这个内部类仅提供一个 start() 方法与 stop() 方法,同时由于需要依赖于前面说到的 Timer 类来定时计数,因此为了接收定时结果,还得实现 Timer 类中的接口。源码框架如下:

private class NormalUpgradeThread implements Timer.OnTimeOutCallback {

    Timer timer;

    private void start(){
        // 根据实际业务需要开启定时器。
    }

    private void stop(){
        // 根据实际业务需要停止定时器
    }

    @Override
    public boolean onTimeOut(int timingSeconds, int countDownIntervalSeconds) {
        // 处理逻辑
        // 连接服务器检查升级

        return false / true;
    }
}

至此,Service 只需要持有这个 NormalUpgradeThread 类对象就可以控制后台升级子线程,或者说定时计数器的启动与停止了。

 

至于前面提到的强制升级,它的框架也和这个基本一样。

 

4、提示与触发升级

 

在连接服务器检测升级的功能中,笔者这边就直接使用 OkHttp 开源框架来实现了。

 

至于通信传参与结果解析及错误处理,就得各位自行处理了。

 

关于提示有新升级包的功能,笔者这边使用的是传统的在合适的地方以小红点提示的方式来指示。

 

当用户点击对应的小红点后会弹出提示对话框,经用户确定后会触发下载新升级包或直接打开系统安装程序的功能。

 

至于到底是下载还是打开安装界面取决于指定下载目录中是否已成功下载好该升级包软件。

 

5、下载与安装

 

关于下载的新升级包文件的保存位置,笔者这边采用的是 APK 应用安装目录下的 cache 目录,即

context.getCacheDir();

 

2020-02-01 更新:

关于选择下载应用的存储目录要慎重!Android 中应用私有目录,如 /data/data/com.xxx.xxx/ 目录真的是私有目录来的。如果你把下载的APK保存在那个地方,到时想调用系统的安装程序来安装时就会出现问题了,可能会提示诸如“解析程序包失败”等异常提示。其原因就是系统的应用安装程序对于你自己编写的应用来说也属于“外部应用”,一个外部应用是没有权限直接访问你的私有目录的,就相当于系统的应用安装程序拿不到你的安装包文件,所以当然要报错了。

 

解决的办法倒也简单,最简单的就是直接在 sdcard 里保存你下载的新版本APK即可。

 

在 Android 中,提供给 APK 存储文件用的位置主要有以下五个:

1、程序安装目录下的 cache 目录,即 /data 目录对应包名下的 cache 目录;

2、程序安装目录下的 files 目录,同样是 /data 目录对应包名下的 files 目录;

3、sdcard 目录;

4、sdcard 目录下对应包名的专属目录下的 cache 目录;

5、sdcard 目录下对应包名的专属目录下的 files 目录;

 

笔者所选择的,就是上述第 1 条所示的目录。

 

之所以选择这个目录,是为了避免因手机 ROM 对 APK 访问 sdcard 权限的默认策略的不同而引发的无法读写的问题。上述第 1、第 2 条默认都具有读写权限,使用它们是兼容性最好的。

 

当下载与校验通过后即自动打开程序安装应用以便安装。该功能也因系统版本不同会有不同的实现代码,核心代码如下:

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_ACTIVITY_NEW_TASK);
    Uri uri = FileProvider.getUriForFile(this, "com.unionman.locator.fileprovider", apk);
    intent.setDataAndType(uri, "application/vnd.android.package-archive");
}else{
    intent.setDataAndType(Uri.fromFile(apk),"application/vnd.android.package-archive");
}
try {
    startActivity(intent);
}catch(Exception e){
    e.printStackTrace();
}

对于 Android7.0 及以上的系统,它还需要一些额外的配置,关于这些配置请参阅笔者的另一篇博文:Android如何通过Java代码安装APK?

 

6、普通升级与强制升级

 

前面所述都是普通升级的流程。

 

但所谓强制升级它与普通升级也基本一致,唯一的不同就是检测到有新版本应用时会在后台自动下载。当然你其实也可以根据用户所使用的网络来选择是否自动下载,例如只当用户通过 WIFI 上网时才自动下载以避免给用户产生额外的流量费用。

 

当新版本应用下载好后即缩短检测周期,每个检测周期到来时,由于能检测到当前已下载好安装包,即会强制打开应用安装程序以供用户确认安装。

 

缩短检测周期的目的是为了避免用户拒绝安装的情况而准备的。当用户拒绝安装时,会回到应用继续使用,若此时仍安装原来的检测周期,则这个强制升级可以认为是没有一点“强制效果”的。只要用户不想升级,那用户只需要在每次弹出安装界面时返回掉就可以继续使用旧版软件了。因此,我们必须要缩短检测周期,并且缩的非常短。例如每 5 秒弹一次,这样一来,用户不升级就相当于无法使用了,就能一定程度上达到强制升级的目的。

 

至于强制升级的代码就不贴了。原理知道了,实现起来就很简单了。

 


 

posted @ 2019-10-22 15:18  大窟窿  阅读(3299)  评论(0编辑  收藏  举报