如何实现 APK 的升级功能?

2019-09-18

关键字:APK后台升级、APK自动升级


 

最近在磕一个APK,要做一个热更新的功能。笔者以前从来没有做过类似的功能,但没吃过猪肉总是见过猪跑步的,想了一下,要实现一个纯粹的升级功能似乎也不难。

 

1、升级原理

 

一个纯粹的升级功能只需要三个模块。

1、服务器;

2、升级包与升级信息;

3、具备升级功能的应用程序。

 

1、服务器

如果仅是想实现一个纯粹的升级功能的话,那服务器这边没啥特殊的要求。搭建一个最普通的 HTTP 访问型的站点即可。

 

2、升级包与升级信息

升级包就是新版本的 APK 程序安装包。但由于升级包通常来讲不具备信息描述能力,所以我们要再额外准备一个升级描述信息。升级策略就是将当前升级包的相关信息记录在升级信息中,客户端通过解析、判断升级信息来决定升级与否。换句话说,服务器端对外只暴露出一个升级信息出来。真实的升级程序地址,是需要解析升级信息来来获取的。

 

3、具备升级功能的应用程序

至于客户端,职责就清晰且单一了。程序启动后,在后台开一个线程定时访问升级服务器,拿到升级信息以后再做解析判断。当有更新版本时,下载然后提示升级。仅此而已。

 

 

2、实现

 

1、服务器搭建

服务器端很简单,使用 Ngix 搭建即可。然后开辟一个目录专门用于存放应用升级的文件。

 

 

2、升级包与升级信息准备

升级包就不说了,直接编译生成 APK 文件即可。

 

至于升级信息,看实际需求咯。比如笔者这边的升级需求就非常纯粹。仅有一个版本号与升级文件名。

[root@localhost umlocator]# cat upgradeinfo.txt 
version=0
app_name=umlocator_alpha3_ver3.apk
[root@localhost umlocator]#

客户端在拿到这个升级信息时,首先检查 version 字段记载的版本号。如果该版本号大于应用当前的版本号,则表明有新版本,需要下载新版本的程序。

 

app_name 字段记载的就是该新版本的安装包的名称,同时也是路径,因为笔者设定的是升级包与升级信息于同一目录下,所以客户端在拿到新版本的程序名以后,将自行将网址替换一下即可。

 

显然这份升级描述信息是极其简陋的。连个 md5 检验都没有,但笔者前面说了,只是想实现一个纯粹的升级功能而已。业务上的合理性不重要。

 

3、客户端升级处理逻辑

笔者这边应用程序的升级总体逻辑很简单:程序在登录成功以后通过一个后台子线程,定时向服务器查询升级信息。当检查到有新版本时,立即开始下载,下载完成以后即弹出提示进行应用升级。

 

后台线程通过一个 Service 来创建。

<service
    android:name=".UpgradeService"
    android:enabled="true"
    android:exported="false" />

因为后台线程服务是不可能开放给外部应用调用的,所以将 exported 设置为 false。同时,创建升级检测子线程不一定非得通过 Serivce 完成,大家可以按自己的实际业务需求来抉择。

 

子线程的实现如下所示:

    private Thread checkThread = new Thread(){

        @Override
        public void run() {
            while(isContinueCheck) {
                Logger.v(TAG, "Upgrade:" + counter);

                if(isDownloading) {
                    Logger.i(TAG, "Downloading new version apk:" + progress + "%");
                }else{
                    // 180s.
                    if(counter++ >= UPGRADE_CHECK_INTERVAL) {
                        counter = 0;// reset.
                        checkUpdate();
                    }
                }

                try {
                    Thread.sleep(10000); // 10s.
                } catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Logger.i(TAG, "The upgrade check thread was stopped.");
        }

    };

这个检测线程是通过一个标志位 isContinueCheck 来判断是否持续检测的。这个 while 语句每 10 秒执行一次,但是仍然有一个计数器 counter 来决定实际连接服务器检测升级信息的时间间隔。笔者这里设置成 counter 达到 18 ,即每 180 秒连接一次服务器查询升级信息。这个检测频率算是很高的了,检测频率高不是说会担心消耗手机端的资源,而是要担心服务器的资源消耗情况。当应用的用户量足够大时,这种检测频率,对服务提供方来讲是很烧钱的。但还是那句话,笔者仅用于演示一个纯粹的升级功能,所以这些都不重要。

 

那我们要如何来启动这个子线程呢?

 

笔者是将升级功能封装在 Serivce 中的。Service 有个特性:在应用运行期间,通过 start 的方式启动 Service 仅会实例化一份 Service 类。

 

所以笔者将控制子线程循环运行的标志位 isContinueCheck 设为实例变量。然后通过外部的 Intent 来携带当前是要“启动”还是“停止”线程的检测。在 MainActivity 的 onResume() 方法中,发送启动线程的命令。在 onPause() 方法中发送停止的命令。

@Override
protected void onResume() {
    super.onResume();
    //Start the upgrade check daemon.
    Intent intent = new Intent(this, UpgradeService.class);
    intent.putExtra("isContinueCheck", true);
    startService(intent);
}

@Override
protected void onPause() {
    super.onPause();
    //stop the upgrade check thread.
    Intent intent = new Intent(this, UpgradeService.class);
    intent.putExtra("isContinueCheck", false);
    startService(intent);
}

 

同时,为了提升体验,应该在程序启动之初就检查一次升级信息。因此要在启动线程前将计数器 counter 的值 设为 17。

private void startThread() {
    Logger.d(TAG, "checkThread is alive:" + checkThread.isAlive());
    if(checkThread.isAlive()) {
        if(isContinueCheck) {
            Logger.d(TAG, "upgrade check thread already running...");
        }else{
            Logger.d(TAG, "stopping the upgrade check thread...");
        }
    }else{
        /*
         * 升级策略:每 180 秒检查一次升级包,线程每 10 秒执行一次循环。
         * 刚打开程序时立即检查是否有新升级包。
         * 所以 counter 的默认值设为 17。
         * */
        counter = UPGRADE_CHECK_INTERVAL - 1; // counter = 17;
        checkThread.start();
        Logger.d(TAG, "started the upgrade check thread.");
    }
}

 

关于 HTTP 通信,笔者就直接使用 OkHttpUtils 开源库来实现了。当成功得到服务器的返回信息以后即开始解析了,解析算法根据自己的升级信息来写。笔者的实现如下图所示:

 

 

有个地方要注意的就是查询应用当前的版本号。Android SDK 有接口可以直接查

getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;

 

然后就是下载文件了。下载仍然是使用 OkHttpUtils 开源库来实现。这里强烈建议在 sdcard 中创建一个专用目录用于保存下载文件。至于本地文件的管理逻辑还不那么简单,就不再赘述了。OkHttpUtils 有一个回调类是专用于下载文件的(FileCallBack 类),照着下图所示的代码来写即可:

 

 

如果想监听下载进度,FileCallBack 类也有一个方法,重写该方法即可:

 

在这个 inProgress() 方法中,progress 是百分比形式的当前下载进度。文件下载完成时它的值为 1.000000

 

在应用中可以通过 startActivity 的方式来实现打开某 APK 安装引导程序的界面,它的实现代码为:

Uri uri = Uri.fromFile(apk);
Intent intent = new Intent();
intent.setClassName("com.android.packageinstaller", "com.android.packageinstaller.PackageInstallerActivity");
intent.setData(uri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

上面加红标粗的是 File 类的实例,它就是你下载好的 APK 文件。

 

 

一个纯粹的 APK 升级功能的主要思路就是这样。至于更详情的逻辑处理,就要大家自行根据业务需求来实现了。

 


 

posted @ 2019-09-18 11:58  大窟窿  阅读(3000)  评论(0编辑  收藏  举报