MVP+多线程+断点续传 实现app在线升级库 (手把手教你打造自己的lib)

作者: 夏至 欢迎转载,也请保留这份申明,谢谢。

http://blog.csdn.net/u011418943/article/details/70562580

1、需求分析

App 在线升级是比较传统的修复bug的一种方式,一般添加新功能或者说修改一下比较严重的bug的时候,我们都是会升级apk来实现我们的目的;当然,其实在一些紧急的bug是用 热修复 的方法,毕竟有时候只是一行代码出了问题,而你却要升级一整个apk,下载安装等等,除了代价有点高,也会影响口碑的。
等等,你都说成这样,还学习这个干吗?你根本不是老司机。。。。

别急,假如你不是修复一行代码或者少数改动,也是添加了很多东西,诸如动画或者说重构等等,那么这个时候,在线升级就显得非常有必要了。
先附上github源码 ,需要的同学可以下载看看。
demo地址: https://github.com/LillteZheng/AppUpdateDemo

首先先上效果图:


这里写图片描述

所以,这里,我将用 MVP 设计模式带你实现 App 在线升级lib 的代码编写。首先,先上思维导图:


这里写图片描述

可以看到,我们正式下载的时候才用到MVP的设计模式,也是官网说的,我们不要为了MVP而去MVP;

接在我们再看一下,我们lib的目录结构:


这里写图片描述

可以看到,我们的lib MVP模式还是比较清晰的,而我们的module 也只有一个 activity 就实现我们了我们的在线更新。
当然,如果你对 MVP 模式不熟悉,欢迎查看我的上一篇文章:

MVP 设计模式,实战理解MVP

由于我们使用了数据库去保存数据,即断点续传功能,我们需要在application中添加:

android:name="com.rachel.updatelib.UpdateLibAppLication"

或者让你的 application 继承 UpdateLibAppLication:

public class MyApplication extends UpdateLibAppLication

2、怎么实现

ok,进入正题,首先第一步就是检测版本,再我们的androidmanifest.xml ,有两个参数,versioncode和versionname,如果没有则自己添加:

这里写图片描述

在线升级的,其实就是对比versioncode就行了。
而我们可以使用 packagemanager 来获取我们本地的版本

/**
     * 获取本地版本
     * @param context
     * @return
     */
    public LocalInfo getLocalInfo(Context context){
        PackageInfo packageInfo = null;
        try {
            packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
                    PackageManager.GET_CONFIGURATIONS);
            LocalInfo localInfo = new LocalInfo();
            localInfo.setVersioncode(packageInfo.versionCode);
            localInfo.setVersionname(packageInfo.versionName);
            return localInfo;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return nul

我们在升级的时候,最常用的就是 versioncode 和versionname,所以,LocalInfo 封装了它们两个:

public class LocalInfo {
    private int versioncode;
    private String versionname;
...

接着就是检测服务器的json文件了,这里很多人都跟我一样,不会搭服务器啊!!!!不过没关系,我们先看一下南方周末的json文件
整理之后格式如下:


这里写图片描述

可以看到,我们需要的就那几个参数,所以,我们完成自己写吗,至于apk的url,就随便找一个apk就可以了,我用的是简书的,如果对简书造成困扰,请告诉我,我马上删掉;(心虚。。。)

    private String filename;
    private int versioncode;
    private String versionname;
    private String versionmsg;
    private String fileurl;
    private long filesize;
    private File FileDir;
...

我们把解析出来的 versioncode 大于本地的,就可以更新,然后把上面的数据放到view去;比如我的:

FileInfo fileInfo = new FileInfo.Builder()
                .setFileName("小白点")
                .setVersionCode(2)
                .setVersionName("1.1")
                .setVersionMsg("1、添加builder模式\n2、添加任务删除方法")
                .setFileUrl("http://downloads.jianshu.io/apps/haruki/jianShu-release-2.1.3-JianShu.apk")
                .builder();
        VersionManager.getInstance(this).checkUpdateUseFileInfo(fileInfo, new VersionCallback() {
            @Override
            public void success(FileInfo fileInfo, LocalInfo localInfo) {
                log.d("success");
                showPopupWindow(MainActivity.this,rootview,fileInfo);
            }
            @Override
            public void lastest() {
            }
        });

可以看到,我用到了 Builder 的设计模式去加载,看起来是不是舒服了很多了呢?如果你也想这种高逼格的初始化模式,可以看我的这一片文章:
模仿常用框架Builder初始化数据,如何优雅地装逼

ok,数据初始化,模仿某app的更新界面之后,效果如下:


这里写图片描述

但,这并不是我想讲的;我想讲的是,如何用 MVP 的方式去封装我们的lib、

3、使用MVP 模式封装下载库;

model:

我们先来看一下model的机构图:


这里写图片描述

首先,思考一下,我们按了确定键之后,就是开始下载的,在下载的工程中,我们还实现了暂停,继续和删除的功能,所以,接口函数如下:

public interface IUpdateModel {
    void download(FileInfo fileInfo);
    void onDestroy();
    void pause();
    void restart();
    void delete();
}

至于download则是实现下载的方法,而一般下载我们是通过线程去下载,所以我们通过一个服务去实现我们的下载任务:

@Override
    public void download(FileInfo fileInfo) {
        Intent intent = new Intent(mContext,DownloadService.class);
        intent.putExtra("fileinfo",fileInfo);
        mContext.bindService(intent,conn,Context.BIND_AUTO_CREATE);
        Timer timer = new Timer(); //绑定需要时间,隔500ms后再连接
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                mBinder.startDownload(mIUpdatePresenter);
            }
        },500);
    }

而且我们是通过binder去绑定我们的服务的,这样,我们就是和service相互通信了,比如暂停,重新开始等都是service的方法,用binder则可以在其他界面,使用这几个方法。。

3.1 多线程下载原理

文件在下载中,我们常用的是用单线程下载,这样的好处在于好控制,能够监控这个文件的下载进度等等。缺点在于,没有完全利用cpu的利用率,而且如果是大文件,下载的速度较慢。所以,我们可以通过多线程的方式,去下载文件。
实现原理是什么呢?就是把一个文件给切分几块来下载。比如一个11M的文件,我们把它分成5个部分来下载;那么它的计算公司就为

 blocksize = 11%5 == 0? 11/5:11%5+1;
//每一个线程要下载的大小
 blocksize = filesize%threadcount == 0? filesize/threadcount : filesize/threadcount+1
这里写图片描述

需要注意的一下是,我们最后一个一般是除不尽的,所以,我们用文件大小来代替,我们我们多线程的代码为:

 public void startDownload(FileInfo fileInfo, IUpdatePresenter callback) {
        mFileInfo = fileInfo;
        mIUpdatePresenter = callback;
        // 通过url 来判断数据库是否已经存在线程信息了
        List<ThreadInfo> threadInfos = DataSupport.where("url = ?",fileInfo.getFileurl()).find(ThreadInfo.class);
        if (threadInfos.size() == 0){ //此时数据库中并没有存在任何信息
            for (int i = 0; i < THREADCOUNT; i++) {
                int blocksize = (int)fileInfo.getFilesize()/THREADCOUNT;//每个线程分配的大小
                ThreadInfo threadInfo = new ThreadInfo(i,fileInfo.getFileurl(),0,blocksize*i,blocksize*(i+1)-1);
                if (i == THREADCOUNT -1){ //最后一个除不尽,则用文件的总大小填进去
                    threadInfo.setEndpos((int)fileInfo.getFilesize());
                }
                threadInfo.save();//保存到数据库中
                threadInfos.add(threadInfo);
            }
        }
        mDownloadTaskThreadList = new ArrayList<>();
        for (ThreadInfo threadInfo : threadInfos){
            DownloadTaskThread downloadTaskThread = new DownloadTaskThread(threadInfo);
            mExecutorService.execute(downloadTaskThread);
            log.d("thread: "+(threadInfo.getStartpos()+threadInfo.getThreadfinished())+" "+threadInfo.getEndpos());
            mDownloadTaskThreadList.add(downloadTaskThread); //管理起来,方便判断下载完成或者暂停等等
        }
    }

如果对多线程不熟悉,可以看我以前的文章,并练练手: 多线程下载文件

然后,我们用到了线程池去管理我们的线程,这样可以减少线程启动和销毁的时间。
至于数据库的保存,我还是使用郭霖大神的 litepal,毕竟确实很方便,当然也可以自己写,只是麻烦了点。
项目中也用到比较多的单例模式,如果你对单例不熟悉,可以看这篇文章:
Android 三种单例模式,简单明了

View :

既然逻辑部分我们已经搞定了,那么接下里就是 View 的实现了,想一下,我们要更新的东西有什么?一个下载任务进度的显示,首先是进度,然后是下载的大小,就足够了,考虑到时app升级,这里我们没提示下载速度了,其实也简单,就是进度/时间就可以了,注意这个时间是1s,别搞错了,当然,还有成功和失败的接口。所以,我们view 的接口就很清晰了:

public interface IDownloadView {
    //提供给view更新UI的接口
    void setDownloadProgress(int progress); //更新进度
    void setDownloadSize(String downloadSize); //更新下载的大小
    void setFileSize(String fileSize); //文件总大小
    void downloadSuccess(); //文件下载成功
    void downloadFail(String errorMsg); //文件下载失败
}

让你的view继承它,然后更新方法即可:

@Override
    public void setDownloadSize(String downloadSize) {
        mDownloadSize.setText(downloadSize);
    }
    @Override
    public void setFileSize(String fileSize) {
        mFileSize.setText(fileSize);
    }
    @Override
    public void downloadSuccess() {
        if (mPopupWindow != null){
            mPopupWindow.dismiss();
        }
    }
    @Override
    public void downloadFail(String errorMsg) {
        if (mPopupWindow != null){
          //  mPopupWindow.dismiss();
            Toast.makeText(this, "下载失败", Toast.LENGTH_SHORT).show();
        }
    }

presenter:

这个是我们 view 与model 的纽带,所以,这里我们还是要考虑好的,首先,view 的更新上,progress,下载大小和文件大小,我们可以用实体类封装起来,然后就是开始下载,暂停,删除等等,view 和model有的接口,它都得有,所以,我们的接口如下:

public interface IUpdatePresenter {
    void getDownloadInfo(DownloadInfo downloadInfo); //下载进度更新
    void errorToast(String errorMsg); //失败
    void success(String path); //成功
    void onDestroy();
    void pause();
    void restart();
    void delete();
}

实现方法如下:

public class UpdatePresenter implements IUpdatePresenter{
    private Context mContext = UpdateLibAppLication.getContext();
    private IUpdateModel mUpdateModel;
    private IDownloadView mIDownloadView;
    public static String mApkPath = null;
    private Handler mHandler = new Handler(Looper.getMainLooper());
    private UpdatePresenter(IDownloadView iDownloadView){
        mIDownloadView = iDownloadView;
        mUpdateModel = new UpdateModel(this);
    };

    //单例模式
    private volatile static UpdatePresenter sUpdatePresenter;
    public static UpdatePresenter getInstance(IDownloadView iDownloadView){
        if (sUpdatePresenter == null){
            synchronized (UpdatePresenter.class){
                if (sUpdatePresenter == null){
                    sUpdatePresenter = new UpdatePresenter(iDownloadView);
                }
            }
        }
        return sUpdatePresenter;
    }
    /**
     * 开始下载
     * @param fileInfo
     */
    public void startDownload(FileInfo fileInfo){
        mUpdateModel.download(fileInfo);
    }
    /**
     * 更新UI
     * @param downloadInfo
     */
    @Override
    public void getDownloadInfo(final DownloadInfo downloadInfo) {
        //log.d("downloadInfo: "+downloadInfo);
        mHandler.post(new Runnable() { //由于数据是在线程返回,这里更新到UI,需要用主线程更新
            @Override
            public void run() {
                log.d("presenter: "+downloadInfo.getDwonloadSize());
                mIDownloadView.setDownloadProgress(downloadInfo.getProgress());
                mIDownloadView.setDownloadSize(downloadInfo.getDwonloadSize());
                mIDownloadView.setFileSize(downloadInfo.getFileSize());
            }
        });
    }
    @Override
    public void errorToast(final String errorMsg) {
        log.d("errorMsg: "+errorMsg);
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mIDownloadView.downloadFail(errorMsg);
            }
        });
    }
    /**
     * 下载完成自动安装
     * @param path
     */
    @Override
    public void success(String path) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mIDownloadView.downloadSuccess();
            }
        });
        mApkPath = path;
        File file = new File(path);
        if (!file.exists()){
            return ;
        }
        mUpdateModel.onDestroy(); //当下载完成就取消绑定service,防止出错
      //  log.d("path: "+path);
        chmod(path); //需要修改权限,不然packageinstallactivity无法解析其他包下的apk
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); //表明不是未知来源
        intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
        UpdateLibAppLication.getContext().startActivity(intent);
    }
    /**
     * 添加权限
     * @param path
     */
    private void chmod(String path){
        if (path.contains("/data")){  //data下才需要改权限
            String[] paths = path.split("/");
            String splitpath = "/";
            for (String p : paths){
                if (!p.equals("")){
                    splitpath += p +"/";
                    String commond = "chmod 777 "+splitpath;
                    try {
                        Runtime.getRuntime().exec(commond);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            String commond = "chmod 777 "+path;
            try {
                Runtime.getRuntime().exec(commond);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public void onDestroy(){
        if (mUpdateModel != null){
            mUpdateModel.onDestroy();
        }
    }
    @Override
    public void pause() {
        mUpdateModel.pause();
    }
    @Override
    public void restart() {
        mUpdateModel.restart();
    }
    @Override
    public void delete() {
        mUpdateModel.delete();
    }
}

代码比较长,但是都是比较好理解的,只要你对MVP有个大概的轮廓。

3.2 /data/data 目录安装失败的问题

上面中,当软件下载完成,我们就直接安装,如果你手机又内存卡还好,如果没有,我们是安装在 /data/data 目录下,但是这样一来,packageinstallactivity 是没有权限去解析其他包名下的软件的;如果没有权限,则用:

Intent intent = new Intent();
        intent.setAction(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); //表明不是未知来源
        intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
        UpdateLibAppLication.getContext().startActivity(intent);

会提示解析失败,安装不了的。
所以,我们需要把所有路径下的权限都改一下,我们可以使用 runntime 方法:

 /**
     * 添加权限
     * @param path
     */
    private void chmod(String path){
        if (path.contains("/data")){  //data下才需要改权限
            String[] paths = path.split("/");
            String splitpath = "/";
            for (String p : paths){  //改变所有路径下的权限
                if (!p.equals("")){
                    splitpath += p +"/";
                    String commond = "chmod 777 "+splitpath;
                    try {
                        Runtime.getRuntime().exec(commond);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            String commond = "chmod 777 "+path;
            try {
                Runtime.getRuntime().exec(commond); //最后改apk的权限
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

这样,我们比较核心的内容就讲完了,我们可以随便定制我们的UI,直接使用我们自己的lib,是不是感觉不错呢?当然,我这里封装的,肯定不是很全面的,很多东西没测试过了,主要是帮助大家,用MVP+多线程+数据库断点续传 的方式去编写我们自己的库;希望能帮到大家。

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

推荐阅读更多精彩内容