满堂花醉三千客,一剑霜寒十四州。这篇文章主要讲述MVP+多线程+断电续传 实现app在线升级库 (手把手教你打造自己的lib)相关的知识,希望能为你提供帮助。
作者:
夏至 欢迎转载,
也请保留这份申明,
谢谢。
http://blog.csdn.net/u011418943/article/details/705625801、需求分析 App 在线升级是比较传统的修复bug的一种方式, 一般添加新功能或者说修改一下比较严重的bug的时候, 我们都是会升级apk来实现我们的目的; 当然, 其实一些紧急的bug的其实是用 热修复 的方法, 毕竟有时候只是一行代码出了问题, 而你却要升级一整个apk, 下载安装等等, 除了代价有点高, 也会影响口碑的。
等等, 你都说成这样, 还学习这个干吗? 你根本不是老司机。。。。
别急, 假如你不是修复一行代码或者少数改动, 也是添加了很多东西, 诸如动画或者说重构等等, 那么这个时候, 在线升级就显得非常有必要了。
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 , 没有则添加添加:
文章图片
我们可以使用 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() {
}
});
模仿某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);
}
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);
//管理起来,
方便判断下载完成或者暂停等等
}
}
如果对多线程不熟悉, 可以看以前的文章, 并练练手: http://blog.csdn.net/u011418943/article/details/56675652
然后, 我们用到了线程池去管理我们的线程, 这样可以减少线程启动和销毁的时间。
至于数据库的保存, 我还是使用郭霖大神的 litepal, 毕竟确实很方便, 当然也可以自己写, 只是麻烦了点。
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();
}
}
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();
}
}
}
【MVP+多线程+断电续传 实现app在线升级库 (手把手教你打造自己的lib)】这样, 我们比较核心的内容就讲完了, 我们可以随便定制我们的UI, 直接使用我们自己的lib, 是不是感觉不错呢? 当然, 我这里封装的, 肯定不是很全面的, 很多东西没测试过了, 主要是帮助大家, 用MVP+ 多线程+ 数据库断点续传 的方式去编写我们自己的库; 希望能帮到大家。
推荐阅读
- AutoMapper实际项目运用
- Android应用层View绘制流程之measure,layout,draw三步曲
- Android自定义控件水波加速球
- Eclipse导入Android工程报错 Invalid project description(转载)
- JAVA Eclipse如何开发Android的多页面程序
- 安卓改变窗体的大小
- JAVA Eclipse开发Android如何让超出界面的部分自动显示滚动条
- 解决Android下元素滑动问题
- 黎活明8天快速掌握android视频教程--24_网络通信之网页源码查看器