MultiThreadDownloader下载库的使用详解

刚接触Android开发的时候,记得当时要实现一个下载功能的任务。鉴于当时对Android的掌握不够,实现起来感觉很有难度,只能参照当时其他项目里的下载代码,过后对这部分内容仍旧比较模糊,只知道执行的流程和实现的方式,想不到更合理的方式去优化。虽然这部分功能一直用着比较稳定,但是变更需求的时候去修改逻辑难度相当大。最近从网上找了些稳定易用的下载库,试着用到我的项目中。

这里先介绍一下MultiThreadDownloader,出自Aspsine。项目中给出的Demo相当简单,这里按照Demo对该库的使用进行分析。

首先看一下Demo的结构:

Demo的项目结构图

(BTW:使用简书插图时,直接将图片拖进来即可完成上传并生成链接。然而图片的尺寸不一定合适,如何定义图片的大小或比例,知乎上有一个讨论markdown中插入图片怎么定义图片的大小或比例?,简单总结一下三种方式:一、嵌入HTML代码(img标签),二、使用支持图片大小更改操作的Mou编辑器(图片链接后加" =100x100"),三、使用支持参数的图床,如七牛。使用过程中发现简书默认支持七牛的图床接口,简直太方便了!执行缩放图片操作只需配置/thumbnail/参数即可,具体可查看七牛的图片处理高级文档
 另:CloudApp也是相当好用,奈何被墙,在我已使用VPN的情况下在简书里书写图片的链接仍然无法显示,太遗憾了!)

言归正传,接着看项目结构,这里采用Module dependency方式依赖下载库MultiThreadDownloader,先啰嗦一下GitHub上的使用说明

  • Step 1: Add permission in 'AndroidManifest.xml'
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  • Step 2: Add "compile project(':library')" in your 'build.gradle' file.(Maven or jCenter support will be coming soon)

  • Step 3: Config it in your Application class

private void initDownloader() { 
  DownloadConfiguration configuration = new DownloadConfiguration(); 
  configuration.setMaxThreadNum(10); 
  configuration.setThreadNum(3); 
  DownloadManager.getInstance().init(getApplicationContext(), configuration);
}
  • Step 4: Just use it!
// first: build a DownloadRequest:
final DownloadRequest request = new DownloadRequest.Builder()
            .setTitle(appInfo.getName() + ".apk")
            .setUri(appInfo.getUrl())
            .setFolder(mDownloadDir)
            .build();

// download:
// the tag here, you can simply use download uri as your tag;
DownloadManager.getInstance().download(request, tag, new CallBack() {
    @Override
    public void onStarted() {

    }

    @Override
    public void onConnecting() {

    }

    @Override
    public void onConnected(long total, boolean isRangeSupport) {

    }

    @Override
    public void onProgress(long finished, long total, int progress) {

    }

    @Override
    public void onCompleted() {

    }

    @Override
    public void onDownloadPaused() {

    }

    @Override
    public void onDownloadCanceled() {

    }

    @Override
    public void onFailed(DownloadException e) {

    }
});

//pause
DownloadManager.getInstance().pause(tag);

//pause all
DownloadManager.getInstance().pauseAll();

//cancel
DownloadManager.getInstance().cancel(tag);

//cancel all
DownloadManager.getInstance().cancelAll();

完成这些之后就能够使用下载功能了,然而这种方式并不适用于我们通常的业务场景,下面我将对涉及ListView的下载业务场景中使用该库进行一些说明。

本文主要是针对下载业务做介绍,对于下载所用到的URL的集合,采用常量String[]形式放到类DataSource中,作为数据源,其他相关的NAMES、IMAGES也采用这种方式,并暴露获取数据的方法:

(现实情况中URL基本都要通过服务器交互来获取,有机会再去介绍这部分内容)

private static final String[] URLS = {
    "http://s1.music.126.net/download/android/CloudMusic_2.8.1_official_4.apk",
    "http://dl.m.cc.youku.com/android/phone/Youku_Phone_youkuweb.apk",
    "http://dldir1.qq.com/qqmi/TencentVideo_V4.1.0.8897_51.apk",
    "http://wap3.ucweb.com/files/UCBrowser/zh-cn/999/UCBrowser_V10.6.0.620_android_pf145_(Build150721222435).apk",
    "http://msoftdl.360.cn/mobilesafe/shouji360/360safesis/360MobileSafe_6.2.3.1060.apk",
    "http://www.51job.com/client/51job_51JOB_1_AND2.9.3.apk",
    "http://upgrade.m.tv.sohu.com/channels/hdv/5.0.0/SohuTV_5.0.0_47_201506112011.apk",
    "http://dldir1.qq.com/qqcontacts/100001_phonebook_4.0.0_3148.apk",
    "http://download.alicdn.com/wireless/taobao4android/latest/702757.apk",
    "http://apps.wandoujia.com/apps/com.jm.android.jumei/download",
    "http://download.3g.fang.com/soufun_android_30001_7.9.0.apk"
};

   ……
   ……
   ……

public List<AppInfo> getData() {
    List<AppInfo> appInfos = new ArrayList<AppInfo>();
    for (int i = 0; i < NAMES.length; i++) {
        AppInfo appInfo = new AppInfo(String.valueOf(i), NAMES[i], IMAGES[i], URLS[i]);
        appInfos.add(appInfo);
    }
    return appInfos;
}

如此一来,数据源的问题解决了,下面看一下Demo的效果(这时候看效果似乎有点晚了):


Demo效果图

 采用ListView必然会用到Adapter、ListView的Item对应的Entity等,这里每个Item显示的是一个应用的相关信息,对应的Entity类AppInfo为:

AppInfo类

这里定义的静态的下载状态字段值和成员变量status相关联,通过status字段我们灵活设置ListView中Item的下载状态,并能更新Item对应的可操作方式,若当前Item的statusSTATUS_NOT_DOWNLOAD(0),则Item对应的状态为未下载,该Item的可操作方式为可执行下载。AppInfo类提供了两个方法getStatusText()getButtonText()映射这种关系。

下面看一下UI中的ListViewFragment,主要成员变量为:

private List<AppInfo> mAppInfos;
private ListViewAdapter mAdapter;
private File mDownloadDir;
private DownloadReceiver mReceiver;

onCreate()中的操作:

mDownloadDir = new File(Environment.getExternalStorageDirectory(), "Download");
mAdapter = new ListViewAdapter();
mAdapter.setOnItemClickListener(this);
mAppInfos = DataSource.getInstance().getData();
for (AppInfo info : mAppInfos) {
    DownloadInfo downloadInfo = DownloadManager.getInstance().getDownloadProgress(info.getUrl());
    if (downloadInfo != null) {
        info.setProgress(downloadInfo.getProgress());
        info.setDownloadPerSize(Utils.getDownloadPerSize(downloadInfo.getFinished(), downloadInfo.getLength()));
        info.setStatus(AppInfo.STATUS_PAUSED);
    }
}

设定下载的路径 mDownloadDir,实例化mAdapter,绑定mAdapter的点击监听器,获取数据源mAppInfos, ** 最重要的一步:给数据源中每一项的下载状态赋值。**
 这里的 progress,downloadPersizestatus是由MultiThreadDownloader库为我们维护,只需要每一项的url值即可。

onActivityCreated()中完成ListView和Adapter的绑定及Adapter的数据填充。

每条Item下载状态的更新是通过ListViewFragment中的广播接收器 DownloadReceiver实现的,则一定存在发送广播的地方。的确,广播发送是在Service中执行的,Service的内容稍后介绍;发送时会将AppInfo对象一起发送(仔细观察可以发现AppInfo类实现了Serializable接口,就是为了将其通过广播的Intent传递)。 DownloadReceiver中可以获取Item的position信息,并获取AppInfo对象,并根据AppInfo的status字段,使用:
ListViewAdapter.ViewHolder holder = getViewHolder(position)的方式设置Item的显示状态和对应的内容。

BroadcastReceiver的使用需要注册和注销操作,分别在onResume()和onPause()方法中调用。

private void register() {
    mReceiver = new DownloadReceiver();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(DownloadService.ACTION_DOWNLOAD_BROAD_CAST);
    LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver, intentFilter);
}

private void unRegister() {
    if (mReceiver != null) {
        LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver);
    }
}

在项目结构中有一个listener包,这里面定义了一个接口:OnItemClickListener,它接收一个泛型参数,在后边的使用中会具体化成AppInfo。

public interface OnItemClickListener<T> {
    void onItemClick(View v, int position, T t);
}

ListViewFragment实现了OnItemClickListener接口,并将泛型参数具体化为AppInfo,所以在ListViewFragment中还要重写onItemClick()方法,这是为了处理Item的下载按钮点击处理下载操作的问题;ListViewAdapter中Item的下载按钮设置点击的监听器,并用OnItemClickListener的onItemClick()方法实现,最终实现对ListViewFragment中覆写的onItemClick()方法的调用(这部分的知识应该再看一下);这里只简单处理了下载和暂停操作。

@Override
public void onItemClick(View v, final int position, final AppInfo appInfo) {
    if (appInfo.getStatus() == AppInfo.STATUS_DOWNLOADING || appInfo.getStatus() == AppInfo.STATUS_CONNECTING) {
        pause(appInfo.getUrl());
    } else {
        download(position, appInfo.getUrl(), appInfo);
    }
}

对于下载和暂停的方法在ListViewFragment中实现,分别调用了DownloadService中的方法:

private void download(int position, String tag, AppInfo info) {
    DownloadService.intentDownload(getActivity(), position, tag, info);
}

private void pause(String tag) 
    DownloadService.intentPause(getActivity(), tag);
}

下面具体分析一下DownloadService中的内容。

DownloadService中提供了启动service的方法:intentDownload()、intentPause(),传递ACTION_DOWNLOAD、EXTRA_POSITION、EXTRA_POSITIONEXTRA_APP_INFO作为参数。

public static void intentDownload(Context context, int position, String tag, AppInfo info) {
    Intent intent = new Intent(context, DownloadService.class);
    intent.setAction(ACTION_DOWNLOAD);
    intent.putExtra(EXTRA_POSITION, position);
    intent.putExtra(EXTRA_POSITION, tag);
    intent.putExtra(EXTRA_APP_INFO, info);
    context.startService(intent);
}

public static void intentPause(Context context, String tag) {
    Intent intent = new Intent(context, DownloadService.class);
    intent.setAction(ACTION_PAUSE);
    intent.putExtra(EXTRA_TAG, tag);
    context.startService(intent);
}

onStartCommand()方法中判断传进来的action名字,调用对应的方法;下载操作的开始、暂停和取消都是通过DownloadManager处理,以下是下载方法:

private void download(final int position, final AppInfo appInfo, String tag) {
    final DownloadRequest request = new DownloadRequest.Builder()
            .setTitle(appInfo.getName() + ".apk")
            .setUri(appInfo.getUrl())
            .setFolder(mDownloadDir)
            .build();
    mDownloadManager.download(request, tag, new DownloadCallBack(position, appInfo, mNotificationManager, getApplicationContext()));
}

注意DownloadManager.download()方法中传入了一个匿名的DownloadCallBack对象,它是下载过程的方法回调,发送广播并更新Notification。
 通过DownloadService中监听下载的状态发送广播,并在ListViewFragment中通过DownloadReceiver接收广播消息,实现ListView每一项的状态更新。

在ListViewFragment中有两个方法:isCurrentListViewItemVisible()getViewHolder():

private boolean isCurrentListViewItemVisible(int position) {
    int first = listView.getFirstVisiblePosition();
    int last = listView.getLastVisiblePosition();
    return first <= position && position <= last;
}

private ListViewAdapter.ViewHolder getViewHolder(int position) {
    int childPosition = position - listView.getFirstVisiblePosition();
    View view = listView.getChildAt(childPosition);
    return (ListViewAdapter.ViewHolder) view.getTag();
}

ViewHolder的获取是通过position参数,获取ListView的该位置Item的View;在DownloadReceiver接收到广播消息更新View的状态时,首先要判断该View当前是否可见,传入position参数,看它是否在ListView当前所有可见Item的起止position范围内。

这是相当简洁的一个下载库,在我看来使用起来也较为清晰。这里所做的分析只是我在使用它的过程中的一些总结,理解有误或者总结不当的地方欢迎指正!

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

推荐阅读更多精彩内容