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范围内。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容