Android 多线程文件断点下载器实现(造轮子系列)(二)

本文介绍如何实现Android的多线程文件断点下载器,利用并发包中的线程池进行任务调度,详细阐述了任务抽象、DownloadManager的设计以及运行效果和问题解决策略,包括使用单例模式、回调更新UI以及解决界面频繁更新的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.开始

如果对断点续传没有了解的,可以看看我的上一篇博文——断点续传实现
上次完成了断点下载相关的功能,这次开始进行任务并行相关的扩展。
任务并行需要完成的功能:一定数量的任务并行下载,超过额定值的任务暂停等待。有一个很好的方法能完成这种要求,那就是concurrent包下提供的线程池。

2.任务抽象

用到线程池,那么就要用到runnable并进行相关的调用,为了提高抽象等级,把下载任务相关的属性和方法抽象成一个抽象类,供下载任务类实现。可以看到,基类是要求子类实现Runnable接口的。

abstract public class TransferTask implements Runnable
{
    protected long taskSize;
    protected long completedSize;

    protected String url;

    protected String fileName;
    protected String saveDirPath;
    protected OkHttpClient client;
    //下载的文件
    RandomAccessFile file;
    //任务状态
    int state = LoadState.PREPARE;

    public long getTaskSize()
    {
        return taskSize;
    }

    public long getCompletedSize()
    {
        return completedSize;
    }

    public String getFileName()
    {
        return fileName;
    }

    public void setFileName(String fileName)
    {
        this.fileName = fileName;
    }

    public void setSaveDirPath(String saveDirPath)
    {
        this.saveDirPath = saveDirPath;
    }

    public int getState()
    {
        return state;
    }

    public void setState(int state)
    {
        this.state = state;
    }

    public String getUrl()
    {
        return url;
    }

    public void setUrl(String url)
    {
        this.url = url;
    }

    public String getSaveDirPath()
    {
        return saveDirPath;
    }

    @Override
    abstract public void run();
}

具体实现类的run()方法,使用上次写下的代码就可以了。

3.DownloadManager

完成了简单的抽象,就有了可供调度的线程。
考虑到使用线程池,那么就需要一个专门的类管理线程池,并对相关的下载任务进行分配和需要的操作。
于是定义一个DownlaodManager,因为线程池无法获取线程的状态和改变线程内的变量,所以Manager出了维护一个线程池外,还要维护一个任务的列表,在线程执行完毕或取消后,将对应的任务移出列表。设置的回调如下

interface CompletedListener
{
    void isFinished(String url);
}

除了这些,还要考虑到后台线程和前台交互,用来显示进度或者提示相关信息。于是也需要维护一个UI线程的Handler,并用回调更新界面。

这样的应用,比较适合使用单例模式。具体分析写在注释里

/**
 * Created by pxh on 2016/2/15.
 * 管理下载任务
 */
public class DownloadManager implements DownloadTask.CompletedListener
{
    static DownloadManager mManager;
    Context context;

    //数据库相关的操作类和实体类
    static private DaoMaster daoMaster;
    static private DaoSession daoSession;
    private DownloadEntityDao downloadDao;

    //下载路径
    String downLoadPath = "";

    //可并行线程数
    private int nThread;

    //Activity或fragment实现的接口,方法为OnUIUpdate(),是在UI线程运行的方法
    DownloadUpdateListener mDownloadUpdate;

    private Handler mHandler;

    //维护的任务相关队列,保存未完成的任务
    LinkedList<TransferTask> taskList;

    //用于调度的线程池
    ExecutorService executorService;

    //私有构造方法
    private DownloadManager(Context context, int nThread)
    {
        this.context = context;
        this.nThread = nThread;

        //得到UI线程的Handler,用于更新UI
        mHandler = new Handler(Looper.getMainLooper());
        //初始化nThread大小的线程池,超过nThread的任务挂起
        executorService = Executors.newFixedThreadPool(this.nThread);
        taskList = new LinkedList<>();
        //将数据库中的未完成任务读取出来,并存入到taskList中
        getDownloadTask();
        downloadDao = getDaoSession(context).getDownloadEntityDao();
    }

    //获得实例前先进行一次init,因为下载任务在后台执行,为了防止内存泄漏,Context最好为Application的Context而不是Activity的
    static public void init(Context context)
    {
        if (mManager == null)
            synchronized (DownloadManager.class) {
                if (mManager == null)
                    mManager = new DownloadManager(context, 3);
            }
    }

    static public void init(Context context, int nThread)
    {
        if (mManager == null)
            synchronized (DownloadManager.class) {
                if (mManager == null)
                    mManager = new DownloadManager(context, nThread);
            }
    }

    static public DownloadManager getInstance()
    {
        if (mManager == null)
            throw new NullPointerException();
        return mManager;
    }

    public void addTask(String url, String fileName)
    {
        //DownloadTask为TransferTask的实现类
        DownloadTask task = new DownloadTask(fileName, url, SDCardUtils.getSDCardPath() + downLoadPath, downloadDao);
        //注册完成事件,方便任务完成后将实例移出实例集合
        task.setCompletedListener(this);
        //将任务加入队列,并在线程池中执行
        taskList.add(task);
        executorService.execute(task);
    }

    /**
     * 可以获得当前为下载完成的任务列表及其相关信息
     *
     * @return
     */
    public LinkedList<TransferTask> getTaskList()
    {
        return taskList;
    }

    //可以根据url获取相应的任务
    private DownloadTask getTask(String url)
    {
        for (TransferTask task : taskList) {
            DownloadTask dTask = (DownloadTask) task;
            if (dTask.getUrl().equals(url)) {
                return dTask;
            }
        }
        return null;
    }

    //下载完成,将相应的任务移出taskList
    @Override
    public void isFinished(String url)
    {
        Log.v("task finished", "task : " + url + " download completed");
        DownloadTask task = getTask(url);
        if (task != null)
            taskList.remove(task);
        else
            Log.e("isFinished", "task=null");
    }

    //使用弱引用,防止内存泄漏
    public void setUpdateListener(DownloadUpdateListener updateListener)
    {
        WeakReference<DownloadUpdateListener> reference = new WeakReference<>(updateListener);//prevent memory leak
        this.mDownloadUpdate = reference.get();
    }

    /**
     * 获取DaoMaster
     *
     * @param context
     * @return
     */
    public static DaoMaster getDaoMaster(Context context)
    {
        if (daoMaster == null) {
            DaoMaster.OpenHelper helper = new DaoMaster.DevOpenHelper(context, "downloadDB", null);
            daoMaster = new DaoMaster(helper.getWritableDatabase());
        }
        return daoMaster;
    }

    /**
     * 获取 DaoSession
     *
     * @param context
     * @return
     */
    public static DaoSession getDaoSession(Context context)
    {
        if (daoSession == null) {
            if (daoMaster == null) {
                daoMaster = getDaoMaster(context);
            }
            daoSession = daoMaster.newSession();
        }
        return daoSession;
    }

    //将数据库中的未完成任务读取出来,并存入到taskList中
    private void getDownloadTask()
    {
        DownloadEntityDao downloadEntityDao = getDaoSession(context).getDownloadEntityDao();
        List<DownloadEntity> entityList = downloadEntityDao.loadAll();
        for (DownloadEntity entity : entityList) {
            Log.e("dao", entity.toString());
            if (entity.getCompletedSize().equals(entity.getTaskSize())) {
                //handle already downloaded files
            } else
                taskList.add(new DownloadTask(downloadEntityDao, entity));
        }

    }

    public interface DownloadUpdateListener
    {
        void OnUIUpdate();
    }
}
4.简单使用

在Activity中,只需要如下代码就可以得到Manager的实例

DownloadManager.init(this.getApplicationContext());
downloadManager = DownloadManager.getInstance();

一般情况下,都是使用ListView或者RecyclerView显示下载的信息,这时候就可以通过

downloadManager.getTaskList()

获得任务列表的引用
并且可以在OnUIUpdate()方法中随任务下载更新列表

@Override
public void OnUIUpdate()
{
    adapter.notifyDataSetChanged();
}

这里写了一个例子,Activity如下

public class MainActivity extends AppCompatActivity implements TaskConfirmDialog.InputCompletedListener,
        DownloadManager.DownloadUpdateListener
{
    private ListView listView;
    DownloadManager downloadManager;
    protected Adapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        listView = (ListView) findViewById(R.id.listView);
        DownloadManager.init(this.getApplicationContext());
        downloadManager = DownloadManager.getInstance();
        downloadManager.setUpdateListener(this);
        setListViewAdapter();
        verifyStoragePermissions(this);
    }

    private static void deleteFilesByDirectory(File directory)
    {
        if (directory != null && directory.exists() && directory.isDirectory()) {
            for (File item : directory.listFiles()) {
                item.delete();
            }
        }
    }

    void setListViewAdapter()
    {
        adapter = new Adapter(this, downloadManager.getTaskList());
        listView.setAdapter(adapter);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        switch (item.getItemId()) {
            case R.id.action_add_task:
                TaskConfirmDialog dialogFragment = new TaskConfirmDialog();
                android.app.FragmentManager manager = getFragmentManager();
                dialogFragment.show(manager, "");
                break;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void inputCompleted(String url, String fileName)
    {

        url = "http://apk.hiapk.com/web/api.do?qt=8051&id=716";
        String url1 = "https://github.com/nebulae-pan/OkHttpDownloadManager/archive/master.zip";
        String url2 = "https://github.com/bxiaopeng/AndroidStudio/archive/master.zip";
        String url3 = "https://github.com/romannurik/AndroidAssetStudio/archive/master.zip";
        String url4 = "https://github.com/facebook/fresco/archive/master.zip";
        String url5 = "https://github.com/bacy/volley/archive/master.zip";
        downloadManager.addTask(url, "123.apk");
        downloadManager.addTask(url1, "1.zip");
        downloadManager.addTask(url2, "2.zip");
        downloadManager.addTask(url3, "3.zip");
        downloadManager.addTask(url4, "4.zip");
        downloadManager.addTask(url5, "5.zip");
    }

    @Override
    public void OnUIUpdate()
    {
        adapter.notifyDataSetChanged();
    }

    /**
     * just sample
     */
    static class Adapter extends BaseAdapter
    {
        LinkedList<TransferTask> data;
        Context context;

        public Adapter(Context context, LinkedList<TransferTask> data)
        {
            this.data = data;
            this.context = context;
        }

        @Override
        public int getCount()
        {
            return data.size();
        }

        @Override
        public Object getItem(int position)
        {
            return data.get(position);
        }

        @Override
        public long getItemId(int position)
        {
            return 0;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent)
        {
            if (convertView == null) {
                convertView = ((Activity) context).getLayoutInflater().inflate(R.layout.item_download, parent, false);
            }
            final TransferTask tf = data.get(position);
            //if taskSize isn't initial complete,post to getView
            ((TextView) convertView.findViewById(R.id.title)).setText(tf.getFileName());
            ((ProgressBar) convertView.findViewById(R.id.progressBar)).setProgress((int) (tf.getTaskSize() > 0 ? 100
                    * tf.getCompletedSize() / tf.getTaskSize() : 0));
            if (tf.getState() == LoadState.PREPARE) {
                (convertView.findViewById(R.id.operation)).setEnabled(false);
                ((Button) convertView.findViewById(R.id.operation)).setText("connecting");
            }
            if (tf.getState() == LoadState.PAUSE) {
                ((Button) convertView.findViewById(R.id.operation)).setText("start");
            }
            if (tf.getState() == LoadState.DOWNLOADING) {
                (convertView.findViewById(R.id.operation)).setEnabled(true);
                ((Button) convertView.findViewById(R.id.operation)).setText("pause");
            }
            (convertView.findViewById(R.id.operation)).setOnClickListener(new View.OnClickListener()
            {
                @Override
                public void onClick(View v)
                {
                    if (tf.getState() == LoadState.DOWNLOADING)
                        DownloadManager.getInstance().pauseTask(position);
                    else if (tf.getState() == LoadState.PAUSE)
                        DownloadManager.getInstance().restartTask(position);
                }
            });
            return convertView;
        }
    }
}
5.运行效果与问题解决

具体运行效果如下
第一个截图是同时有三个任务可以下载,其余的任务等待
6个任务下载

如果前面的任务暂停,后面的任务开始进行
暂停

不过在更新界面的时候发现了一个问题,在每接受一个数据块后更新,导致多任务下更新速率极快,大于了界面刷新速度。
想到的解决办法有两个

  • 接收数据块后挂起一段时间,降低更新速率
  • 格外开启一个更新线程,固定的时间想UI线程发送更新信息

第一种方法实现简单,但会导致加载变慢。
这里放下我想到的第二种方法

//在DownloadManager下加入这两个属性
final Object updateLock = new Object();//更新界面进程的互斥锁
boolean isUpdating = false; //当前是否更新

/**
 * 需判断状态,全部暂停后停止更新界面
 */
Runnable updateUIByOneSecond = new Runnable()
{
    @Override
    public void run()
    {
        synchronized (updateLock) {
            if (isUpdating) {
                mHandler.post(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        if (mDownloadUpdate == null) return;
                        mDownloadUpdate.OnUIUpdate();
                    }
                });
                ifNeedStopUpdateUI();
                mHandler.postDelayed(this, 1000);
            }
        }
    }
};

protected void startUpdateUI()
{
    synchronized (updateLock) {
        if (!isUpdating) {
            isUpdating = true;
            new Thread(updateUIByOneSecond).start();
        }
    }
}

protected void stopUpdateUI()
{
    synchronized (updateLock) {
        isUpdating = false;
    }
}

//检查是否需要停止更新
protected void ifNeedStopUpdateUI()
{
    for (TransferTask task : taskList) {
        if (task.getState() == LoadState.DOWNLOADING || task.getState() == LoadState.PREPARE )
            return;
    }
    stopUpdateUI();
}

实现思路就是,在addTask(),reStartTask()这些会需要界面更新的任务中加入startUpdateUI()器界面更新线程。
界面更新线程updateUIByOneSecond()每秒执行一次,会判断当前状态是否需要执行更新,如果需要,向UI线程的Handler发送更新信息,并在发送完毕后,检查所有任务的状态,如果不存在正在下载,或正在连接准备中的任务,就结束更新。这样就能实现统一的列表更新。

6.结束

这次实现了多任务并行下载,之后就要向最后一步:多线程下载,发起挑战了。
如果有大神,希望能看看我的博文或在github上的代码,给我提出一写建议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值