android版本更新策略

作者: laogui | 来源:发表于2016-06-28 08:19 被阅读10528次
屏幕快照 2016-06-28 02.00.08.png

开发中对版本进行检查并更新的需求基本是所有应用必须有的功能,可是在实际开发中有些朋友就容易忽略一些细节。

版本更新的基本流程:

一般是将本地版本告诉服务器,服务器经过相关处理会返回客户端相关信息,告诉客户端需不需要更新,如果需要更新是强制更新还是非强制更新。客户端得到服务器返回的相关信息后再进一步做逻辑处理。

强制更新:

一般的处理就是进入应用就弹窗通知用户有版本更新,弹窗可以没有取消按钮并不能取消。这样用户就只能选择更新或者关闭应用了,当然也可以添加取消按钮,但是如果用户选择取消则直接退出应用。

非强制更新

一般的处理是在应用的设置中添加版本检查的操作,如果用户主动检查版本则弹窗告知用户有版本更新。这时用户可以取消或者更新。

功能实际是比较简单清晰的,但之所以写这篇文章,是因为在我们公司的一个项目中,我把这个模块分给了一个有着4年工作经验的哥们编写,最后这哥们花了2个小时做完了。我还想这哥们写得挺快,效率很高嘛,结果一测试发现问题不少:

  1. 进入首页前关闭网络,进入后刷新界面发现强制更新提醒没有弹窗
  2. 再进入其它界面也没有任何更新提醒
  3. 在正常更新时点击确定更新,没有判断网络状态(wifi,移动网络)直接下载apk文件,如果用户在移动网络下将耗费非常多的流量,直接影响用户体验
  4. 下载过程在应用内没有进度条提醒,通知栏也没有进度提醒
  5. apk文件下载过程中,如果强制结束应用,下载被中断
  6. apk如果正常下载下来,弹出了安装界面,这时如果用户取消了安装回到应用,在需要强制更新的情况下并没有再次弹窗阻止用户进行任何其它操作,失去了强制更新的意义

首先声明下,我这丝毫没有吐槽的意思哟,只是想说作为一个合格的程序员大家最起码需要做到思维严谨这点,在有能力的情况下对用户体验能提点建议最好。自己写的代码一定要经过严格测试再交付,不要指望测试人员帮你测试再去修改,你要知道现在很多公司是没有专业的测试人员甚至是没有测试人员的哟。

针对以上问题出现的原因分析及解决方案如下:

  • 对于1,2问题
    很明显他把检查更新的工作只写在了应用的首页(比如MainActivity)中了,在其它任何界面并没有检查更新的操作

  • 解决方案
    每个界面都需要检查更新,当然咱们不能在每个Activity中都复制粘贴一样的代码。这时定义一个BaseActivity,所有其它Activity都从它继承就显得很有价值了。可以把检查更新的操作放到BaseActivity的相关方法中,比如放在onResume中,这样每当显示一个界面时都将执行检查更新的操作

  • 对于5问题,如果把下载的操作放在了Activity中进行,如果应用意外终止或者强制退出应用,则下载线程也将被终止

  • 解决方案
    可以将下载任务放到Service中执行,这样即使应用被终止Service一样有保活机制(startForeground)让Service的任务有很大的机会继续得以执行

  • 对于6问题,如果检查更新的操作没有在Activity的resume时再次执行,则回到Activity自然也就没有检查更新并弹窗了

  • 解决方案
    在Activity的onResume中继续检查更新,如果是强制更新则弹窗阻止用户进行其它操作

  • 对于3,4问题,我倒是觉得不是程序问题而是态度问题,实际加入非wifi和进度显示的功能非常简单

整体解决方案

  1. 定义Service类,比如VersionUpdateService.java。主要提供版本检查及文件下载操作
  2. 定义VersionUpdateHelper类,用来使用Service并提供和前台Activity的交互
    如果大家对Service的使用还有问题(需要频繁更新前台ui等),建议大家阅读android图片压缩上传系列-service篇这篇文章先做了解。

核心代码如下:

public class VersionUpdateService extends Service {
    private LocalBinder binder = new LocalBinder();

    private DownLoadListener downLoadListener;//下载任务监听回调接口
    private boolean downLoading;
    private int progress;

    private NotificationManager mNotificationManager;
    private NotificationUpdaterThread notificationUpdaterThread;
    private Notification.Builder notificationBuilder;
    private final int NOTIFICATION_ID = 100;

    private VersionUpdateModel versionUpdateModel;
    private CheckVersionCallBack checkVersionCallBack;//检查结果监听回调接口
    public interface DownLoadListener {
        void begain();
        void inProgress(float progress, long total);
        void downLoadLatestSuccess(File file);
        void downLoadLatestFailed();
    }

    public interface CheckVersionCallBack {
        void onSuccess();
        void onError();
    }
    ...
    private class NotificationUpdaterThread extends Thread {
        @Override
        public void run() {
            while (true) {
                notificationBuilder.setContentTitle("正在下载更新" + progress + "%"); // the label of the entry
                notificationBuilder.setProgress(100, progress, false);
                ...
            }
        }
    }
    private void starDownLoadForground() {
        //创建通知栏
        notificationBuilder = new Notification.Builder(this);
        ...
        Notification notification = notificationBuilder.getNotification();
        startForeground(NOTIFICATION_ID, notification);
    }
    private void stopDownLoadForground() {
        stopForeground(true);
    }
    //执行版本检查任务
    public void doCheckUpdateTask() {
        //获取本定版本号
        final int currentBuild = AppUtil.getVersionCode(this);
        //调用版本检查接口
      ApiManager.getInstance().versionApi.upgradeRecords(currentBuild, new RequestCallBack() {
            @Override
            public void onSuccess(Headers headers, String response) {
                    versionUpdateModel = JSON.parseObject(response, VersionUpdateModel.class);
                    ...
                    if (checkVersionCallBack != null)
                        checkVersionCallBack.onSuccess();
            }

            @Override
            public void onError(int code, String response) {
              ...
            }
        });
    }
    public void doDownLoadTask() {
        starDownLoadForground();
        //启动通知栏进度更新线程
        notificationUpdaterThread = new NotificationUpdaterThread();
        notificationUpdaterThread.start();
        //文件下载存放路径
        final File fileDir = FolderUtil.getDownloadCacheFolder();
        ...
        downLoading = true;
        if (downLoadListener != null) {
            downLoadListener.begain();
        }
        NetManager.getInstance().download(url, fileDir.getAbsolutePath(), new DownloadCallBack() {
            @Override
            public void inProgress(float progress_, long total) {
                ...
                //执行进度更新
               if (downLoadListener != null) 
                  downLoadListener.inProgress(progress_, total);
              }
            @Override
            public void onSuccess(Headers headers, String response) {
                //执行成功回调
                ...
                installApk(destFile, VersionUpdateService.this);
            }

            @Override
            public void onError(int code, String response) {
                ...
                //执行失败回调
            }
        });
    }

    //安装apk
    public void installApk(File file, Context context) {
        ...
    }
}
public class VersionUpdateHelper implements ServiceConnection {
    private Context context;
    private VersionUpdateService service;
    private AlertDialog waitForUpdateDialog;
    private ProgressDialog progressDialog;

    private static boolean isCanceled;

    private boolean showDialogOnStart;

    public static final int NEED_UPDATE = 2;
    public static final int DONOT_NEED_UPDATE = 1;
    public static final int CHECK_FAILD = -1;
    public static final int USER_CANCELED = 0;

    private CheckCallBack checkCallBack;

    public interface CheckCallBack{
        void callBack(int code);
    }

    public VersionUpdateHelper(Context context) {
        this.context = context;
    }

    public void startUpdateVersion() {
        if (isCanceled)
            return;
        if (isWaitForUpdate() || isWaitForDownload()) {
            return;
        }
        if (service == null && context != null) {
            context.bindService(new Intent(context, VersionUpdateService.class), this, Context.BIND_AUTO_CREATE);
        }
    }

    public void stopUpdateVersion() {
        unBindService();
    }

    private void cancel() {
        isCanceled = true;
        unBindService();
    }

    private void unBindService() {
        if (isWaitForUpdate() || isWaitForDownload()) {
            return;
        }
        if (service != null && !service.isDownLoading()) {
            context.unbindService(this);
            service = null;
        }
    }

    ...

    private void showNotWifiDownloadDialog() {
        final AlertDialog.Builder builer = new AlertDialog.Builder(context);
        builer.setTitle("下载新版本");
        builer.setMessage("检查到您的网络处于非wifi状态,下载新版本将消耗一定的流量,是否继续下载?");
        builer.setNegativeButton("以后再说", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ...
                //如果是强制更新 exit app
                if (mustUpdate) {
                    MainApplication.getInstance().exitApp();
                }
            }
        });
        builer.setPositiveButton("继续下载", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
                service.doDownLoadTask();
            }
        });
        ...
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
        service = ((VersionUpdateService.LocalBinder) binder).getService();
        service.setCheckVersionCallBack(new VersionUpdateService.CheckVersionCallBack() {
            @Override
            public void onSuccess() {
                VersionUpdateModel versionUpdateModel = service.getVersionUpdateModel();

                //EventBus控制更新红点提示
                EventBus.getDefault().postSticky(versionUpdateEvent);

                if (!versionUpdateModel.isNeedUpgrade()) {
                    if(checkCallBack != null){
                        checkCallBack.callBack(DONOT_NEED_UPDATE);
                    }
                    cancel();
                    return;
                }
                if (!versionUpdateModel.isMustUpgrade() && !showDialogOnStart) {
                    cancel();
                    return;
                }
                if(checkCallBack != null){
                    checkCallBack.callBack(NEED_UPDATE);
                }
                final AlertDialog.Builder builer = ...//更新提示对话框
                builer.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.cancel();
                        if (NetUtil.isWifi(context)) {
                            service.doDownLoadTask();
                        } else {
                            showNotWifiDownloadDialog();
                        }
                    }
                });

                //当点取消按钮时进行登录
                if (!versionUpdateModel.isMustUpgrade()) {
                    builer.setNegativeButton("稍后更新", new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.cancel();
                            cancel();
                            if(checkCallBack != null){
                                checkCallBack.callBack(USER_CANCELED);
                            }
                        }
                    });
                }
                builer.setCancelable(false);
                waitForUpdateDialog = builer.create();
                waitForUpdateDialog.show();
            }

            @Override
            public void onError() {
                unBindService();
                ...
            }
        });

        service.setDownLoadListener(new VersionUpdateService.DownLoadListener() {
            @Override
            public void begain() {
                VersionUpdateModel versionUpdateModel = service.getVersionUpdateModel();
                if (versionUpdateModel.isMustUpgrade()) {
                    progressDialog = ...//生成进度条对话框
                }
            }

            @Override
            public void inProgress(float progress, long total) {
                ...//更新进度条
            }

            @Override
            public void downLoadLatestSuccess(File file) {
                ...//执行成功处理
                unBindService();
            }

            @Override
            public void downLoadLatestFailed() {
                ...//执行失败处理
                unBindService();
            }
        });

        service.doCheckUpdateTask();
    }
    ...
}

最后,使用方式还是非常简单的。在BaseActivity中使用:

private VersionUpdateHelper versionUpdateHelper;
@Override
protected void onResume() {
    super.onResume();
    if(versionUpdateHelper == null)
        versionUpdateHelper = new VersionUpdateHelper(this);
    versionUpdateHelper.startUpdateVersion();
}

@Override
protected void onPause() {
    super.onPause();
    if(versionUpdateHelper != null)
        versionUpdateHelper.stopUpdateVersion();
}

保证在每进入一个界面和离开界面时都将检查更新(bindService)和取消检查(unBindService)。这时有些朋友可能认为这样做会不会浪费资源呢?没有!
1,如果应用是强制更新,那么在网络正常情况下进入应用就能检查出有新版本,这时弹窗后用户不能进入任何操作,没有机会进入别的界面,所有没有进行重复检查;如果进入应用主页由于网络问题,检查失败,这时虽然不会弹窗提示更新,但是如果用户的网络恢复后进入任何其它界面都将得到正常的版本更新检查并弹窗提示
2,如果应用是非强制更新时,在Helper代码里进行了如下的判断:

if (!versionUpdateModel.isMustUpgrade() && !showDialogOnStart) {
    cancel();
    return;
}

这里的showDialogOnStart默认为false,也就是说如果不是强制更新则检查成功后就当“取消”处理,并在cancel方法中将变量isCanceled修改为true,这样如果有新的请求想要执行startUpdateVersion()都将被忽略,注意isCanceled是static全局的。

如果想实现在设置中由用户手动检查更新,则只需执行类似如下代码:
SettingActivity.java

private VersionUpdateHelper versionUpdateHelper;

@OnClick(R.id.rl_version_update)
public void onClickVersionUpdate(View view) {
    if(updateTips.getVisibility() == View.VISIBLE){
        return;
    }
    VersionUpdateHelper.resetCancelFlag();//重置cancel标记
    if (versionUpdateHelper == null) {
        versionUpdateHelper = new VersionUpdateHelper(this);
        versionUpdateHelper.setShowDialogOnStart(true);
        versionUpdateHelper.setCheckCallBack(new VersionUpdateHelper.CheckCallBack() {
            @Override
            public void callBack(int code) {
                //EventBus发送消息通知红点消失
                VersionUpdateEvent versionUpdateEvent = new VersionUpdateEvent();
                versionUpdateEvent.setShowTips(false);
                EventBus.getDefault().postSticky(versionUpdateEvent);
            }
        });
    }
    versionUpdateHelper.startUpdateVersion();
}

写在最后

由于代码较多,且多数代码和ui相关,所以在文章中很多ui相关或者getter和setter方法等非核心代码并没有列出。演示代码中用了EventBus和OkHttp开源控件,具体使用方法望大家自己找相关资料学习。本人打算有空的时候写个EventBus系列文章,望大家多多关注。
文件下载也是使用的okHttp实现的,大家可以换成任何你熟悉的下载框架。VersionUpdateService.java和VersionUpdateHelper.java的完整代码可以到我的github上下载,由于时间关系并没有相关用法的完整案例还望见谅,等有时间一定奉上。
如果有任何问题可以在评论中加以提问,谢谢~~

相关文章

网友评论

  • jinchen_jianshu:我觉得在主页检查更新就行了,不用Base
  • 忆念成风:我觉得如果你在BaseActivity中放置检查更新的操作,那每个继承BaseActivity的页面都有必要去检查更新。你可以在启动页的时候进行检查或者在主页的时候检查,这样话服务器的压力会小很多的。
  • 1dadac33649b:在base中请求浪费资源了 ,可以在每个接口上传递一个版本号给服务器,让服务器来判断是不是过时的版本,要是就提示用户去更新
  • 程序员不务正业:每个界面取更新,没那个必要吧。不考虑网络问题吗
  • 乘风破浪的程序员:如果用户 下载了一半,杀掉了应用,下次进入下载是不是又得重新下载了
  • 嘉了个桀:第三个问题:用一个NotificationUpdaterThread线程while(true)操作高频率刷新notificationBuilder内容,会造成下拉通知栏的时候比较卡顿,刷新通知栏操作放在下载的inProgress中会比较好
    laogui:@嘉了个桀 嗯,谢谢提醒,这块确实有问题,其实可以忽略通知栏的ui更新,因为在应用内有进度显示,另外也可以用DownLoadManager来处理
  • 嘉了个桀:第一个问题:
    if (!versionUpdateModel.isMustUpgrade() && !showDialogOnStart) {
    cancel();
    return;
    }
    设置了这个,在非强制更新情况下第一次进activity也不会弹出提示更新对话框。

    第二个问题:isCanceled设为static的,点了稍后更新isCanceled设为true,在退出应用后,系统没那么快回收掉。很长时间下再重新进入应用也不会提示更新
  • 云佾风徽:太频繁了
  • 92f31811f2bc:请问,bindservice当activity销毁的时候也会销毁,正在下载不会中断吗?
    tianzy145:Demo有吗??大神
    laogui:@本心清明 不会,只要下载是在独立线程中执行就不会
  • tyh520life:可不可以发一下demo ,好多地方都报错,也猜不到是什么东西
    能不写代码么:@laogui 大神您的Demo呢
    tyh520life:@laogui 可以可以
    laogui:@tyh520life 我会尽快写个demo放github,请留意我的github更新
  • forevan:针对问题1,2的解决方案中写的在onResume中检查更新,这样是否过于频繁?用户锁屏、打开等操作都要去检测。可以考虑放在接口的返回code中,定义相关的code值来决定是否启动检测。不过这样一来也涉及到怎么样封装才能在每个接口的返回值中检测这个code以及下一步的操作?不知楼主在实际项目中是怎么处理的?
    forevan:@laogui 说的有道理~我说的那种方案就是当时为了要解决用户在使用过程中出现的更新,不过需要封装很多东西,受教啦~~~ :smile:
    laogui:@ministorm 你也可以通过跟服务器人员沟通,通过推送来实现更新提示,如果用的第三方推送,达率就要受第三方影响了!
    laogui:@ministorm 这个思路就是我在实际项目中的真实思路,我认为你担心的问题没有必要。如果是强制更新,检查后弹窗阻止其做其它操作,如果用户由于别的原因逃过一个更新检查,当他进去其它界面时还有机会继续检查更新!如果是非强制更新,则保存个全局的变量来表示下次可以不用检查更新了,这样即使在onpause或onresume里也不会再邦定服务了不影响性能!咱们这个思路唯一缺点是:如果用户进去应用检查出的是非强制更新,那么在用户使用过程中就不会再启动检查服务了,会造成用户在使用过程中如果有个强制更新了,用户是不知道的,只能等到他再次启动应用时才会检查到有强制更新!就看具体的项目需求了
  • 奔跑的笨鸟:进去首页前关闭网络,怎么检查是否需要更新?
    laogui:@NKming 没必要吧😄
    NKming:@laogui 那用不用写一个检测是否有网的后台service来检查更新
    laogui:@feimeng0530 检查不了,你可以在恢复或者用户主动刷新时再检查

本文标题:android版本更新策略

本文链接:https://www.haomeiwen.com/subject/lsxtjttx.html