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

来源:互联网 发布:js中的延时函数 编辑:程序博客网 时间:2024/06/04 17:40

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()方法中随任务下载更新列表

@Overridepublic 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上的代码,给我提出一写建议。

0 0
原创粉丝点击