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 升级功能的主要思路就是这样。至于更详情的逻辑处理,就要大家自行根据业务需求来实现了。