android版本更新策略

屏幕快照 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上下载,由于时间关系并没有相关用法的完整案例还望见谅,等有时间一定奉上。
如果有任何问题可以在评论中加以提问,谢谢~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,614评论 4 59
  • L7: 1.天猫开专卖店的材料和成本: 一、天猫商城专营店所需材料 1企业营业执照副本 2企业税务登记证 3组织机...
    a93d7dcda8b1阅读 155评论 0 0
  • 出国游一直是件麻烦事,除了收拾行李,还要规划,并且需要提前很长时间。如果你选择去美国,就更痛苦了,因为美签。美签不...
    小咪小不阅读 1,489评论 0 2