美文网首页Android开发android基础知识Android开发
使用Android基础知识编写一个多任务多线程断点下载示例

使用Android基础知识编写一个多任务多线程断点下载示例

作者: Luckychuan | 来源:发表于2018-07-18 18:23 被阅读29次

    前言

    在学习完《第一行代码》的下载最佳实践后,打算利用Android的基础知识将此完善成一个多任务,断点,离线保存,支持后台的下载示例。适合刚把Android基础知识学完不知道怎么组合运用的初学者。用到的
    Android知识有:
    1.OkHttp断点下载;
    2.Activity与Service通信;
    3.权限获取;
    4.RecyclerView和Adapter实现列表;
    5.SQLite保存数据;
    6.AsyncTask异步下载。

    效果演示

    下载、暂停、后台下载

    功能点

    1.能多条任务同时下载;
    2.支持暂停,继续下载;
    3.finish掉Activity后能在后台下载;
    4.stopService退出程序时,使用数据库保存进度。

    结构

    1.结构图

    结构图.PNG

    2.结构分析

    (1)UI部分

    MainActivity中通过调用notifyDataSetChanged()等方法刷新RecyclerView的数据;当用户点击了RecyclerView中的StartButton和CancelButton,通过OnItemButtonClickListener回调MainActivity。

    (2)Activity与Service通信

    由于下载是个耗时操作,我们要使用Service来做数据和逻辑操作。使用Binder和OnTaskDataChangeListener实现Activity与Service通信。

    (3)Service调用SQLite

    在Service中调用DatabaseManager将当前的下载数据保存。

    (4)DownloadManager

    使用DownloadManager管理下载任务。Service调用DownloadManager做下载操作,DownloadManager通过DownloadView回调Service做数据更新。

    (5)DownloadAsyncTask异步下载

    DownloadManager用HashMap管理DownloadAsyncTask做多任务下载,DownloadAsyncTask用DownLoadListener将每一个任务的下载状态回调给DownloadManager。

    具体实现

    1.获取权限

    实现下载功能我们需要“网络权限” 和 “存储读写权限”。

    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
    

    Android 6.0 以上还要在Activity请求权限。较为基础没什么多说的,直接上代码:

    MainActivity.java

        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            //获取权限
            int readStorageCheck = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE);
            if (readStorageCheck == PackageManager.PERMISSION_GRANTED) {
                initUI();
            } else {
                ActivityCompat.requestPermissions(MainActivity.this,
                        new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
            }
        }
        
            @Override
        public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    
            //当权限获得时
            if (grantResults.length > 0 &&
                    grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                initUI();
            } else {
                if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
                    //弹出对话框提示用户接收权限
                    AlertDialog.Builder dialog = new AlertDialog.Builder(this);
                    dialog.setMessage("程序要获得权限后才能运行");
                    dialog.setCancelable(false);
                    dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            //请求读取手机存储的权限
                            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
                        }
                    });
                    dialog.create().show();
                }
            }
        }
    

    2.Activity与Service通信

    (1)创建DownloadBinder

    Activity需要Service做的事情有:
    1.添加新任务;
    2.点击了“开始”按钮;
    3.点击了“暂停”按钮;
    4.点击了“取消”按钮;
    5.点击stopService退出程序时,使用数据库保存进度。
    因此在DownloadBinder定义5个方法,使用url做参数,可以确定当前是哪一个下载任务被用户操作。

    DownloadService.java

        class DownloadBinder extends Binder {
    
            //点击了开始按钮
            public void startDownload(String url) {        
            
            }
    
            //点击了暂停按钮
            public void pauseDownload(String url) {
    
            }
    
            //点击了取消按钮
            public void cancelDownload(String url) {
    
            }
    
            //新建任务
            public void newTask(String url) {
    
            }
    
            //stopService退出程序
            public void saveProgress() {
    
            }
    
        }
    

    (2)创建OnTaskDataChangeListener

    使用OnTaskDataChangeListener让Service回调Activity

    DownloadService.java

        interface OnTaskDataChangeListener {
        
            //应用打开时,Activity初始化数据
            void onInitFinish(List<Task> list);
    
            //添加了新任务
            void onDataInsert(int position);
    
            //任务的状态发送了变化,例如:更新下载进度,开始和暂停转换
            void onDataChange(int position);
    
            //任务被取消
            void onDataRemove(int position);
        }
    

    (3)绑定Service

    当Activity启动时,绑定Service。同时调用mServiceBinder.setListener(MainActivity.this);将MainActivity实现的OnTaskDataChangeListener接口传到Service。这时Service就知道Activity启动了。这时候在setListener()方法中调用mListener.onInitFinish(mTasks)给Actvity初始化数据界面。

    MainActivity.java

        //绑定Service,实现Activity和Service通信
        private DownloadService.DownloadBinder mServiceBinder;
        private ServiceConnection mConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mServiceBinder = (DownloadService.DownloadBinder) service;
                mServiceBinder.setListener(MainActivity.this);
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
                mServiceBinder = null;
            }
        };
        
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            //绑定Service
            Intent serviceIntent = new Intent(this, DownloadService.class);
            startService(serviceIntent);
            bindService(serviceIntent, mConnection, BIND_AUTO_CREATE);
        }
    
        @Override
        protected void onDestroy() {
            unbindService(mConnection);
            super.onDestroy();
        }
    

    DownloadService.java

        private ArrayList<Task> mTasks;
        private OnTaskDataChangeListener mListener;
        private DatabaseManager mDataBaseManager;
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return new DownloadBinder();
        }
    
        @Override
        public boolean onUnbind(Intent intent) {
            mListener = null;
            return super.onUnbind(intent);
        }   
    
         class DownloadBinder extends Binder {
    
            public void setListener(OnTaskDataChangeListener listener) {
                mListener = listener;
    
                if (mTasks == null) {
                    mTasks = new ArrayList<>();
                    //在数据库中取得数据
                    mTasks.addAll(mDataBaseManager.query());
                }
    
                mListener.onInitFinish(mTasks);
    
            }
        }
    

    3.Service中的逻辑操作

    (1)创建数据java bean

    Task.java

    public class Task implements Serializable {
    
            private String url;
            private String name;
            private int progress;
    
            private boolean isDownloading;
    
            public Task(String url, String name) {
                this.url = url;
                this.name = name;
                isDownloading = true;
            }
    
            public Task(String url, String name, int progress) {
                this.url = url;
                this.name = name;
                this.progress = progress;
                this.isDownloading = false;
            }
    
        }
    

    (2)定义Service中的成员变量

    由于我们要实现Activity被销毁后还能继续在后台下载,因此将数据列表ArrayList<Task> mTasks的变化放在Service中,而不是在Activity中。使用DatabaseManager mDataBaseManager;做数据库操作。使用DownloadManager mDownloadManager;做下载操作。

    DownloadService.java

        private  ArrayList<Task> mTasks;
        private OnTaskDataChangeListener mListener;
        private DatabaseManager mDataBaseManager;
        private DownloadManager mDownloadManager;
        
         @Override
        public void onCreate() {
            super.onCreate();
            Log.d(TAG, "onCreate: ");
            if (mDownloadManager == null) {
                Log.d(TAG, "onCreate: init mDownloadManager");
                mDownloadManager = new DownloadManager(this);
            }
    
            if (mDataBaseManager == null) {
                Log.d(TAG, "onCreate: init mDataBaseManager");
                mDataBaseManager = DatabaseManager.getInstance(getApplicationContext());
            }
    
            //后台下载
            Notification.Builder builder = new Notification.Builder(getApplicationContext());
            builder.setSmallIcon(R.mipmap.ic_launcher);
            builder.setContentText("下载");
            builder.setContentTitle("下载");
            Notification notification = builder.build();
    //        notification.flags = Notification.FLAG_FOREGROUND_SERVICE;
            startForeground(0, notification);
    
            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            manager.notify(0, notification);
    
        }
    
        @Override
        public void onDestroy() {
            Log.d(TAG, "onDestroy: ");
            stopForeground(true);
            super.onDestroy();
        }
    

    (3)实现DownloadBinder的方法

    DownloadService.java

        class DownloadBinder extends Binder {
    
            public void startDownload(String url) {
                mDownloadManager.addDownloadTask(url);
            }
    
            public void pauseDownload(String url) {
                mDownloadManager.pauseDownload(url);
            }
    
            public void cancelDownload(String url) {
                mDownloadManager.cancelDownload(url);
            }
    
            public void newTask(String url) {
                //判断任务是否已经存在
                for (Task t : mTasks) {
                    if (t.getUrl().equals(url)) {
                        Toast.makeText(getApplicationContext(), "任务已经存在", Toast.LENGTH_SHORT).show();
                        return;
                    }
                }
    
                String name = url.substring(url.lastIndexOf("/") + 1);
                Task task = new Task(url, name);
                mTasks.add(task);
    
                if (mListener != null) {
                    mListener.onDataInsert(mTasks.size() - 1);
                }
    
                startDownload(url);
            }
    
            public void saveProgress() {
                for (Task task : mTasks) {
                    Log.d(TAG, "saveProgress: "+task.toString());
    
                    //停止下载
                    if (task.getProgress() < 100) {
                        if(task.isDownloading()){
                            mDownloadManager.pauseDownload(task.getUrl());
                        }
                    }
    
                    //保存到数据库
                    if (mDataBaseManager.query(task.getUrl()) == null) {
                        mDataBaseManager.insert(task);
                    }else{
                        mDataBaseManager.update(task.getUrl(), task.getProgress());
                    }
                }
            }
    
        }
    

    (4)用DownloadView作DownloadManager下载状态的回调

    DownloadView.java

        public interface DownloadView {
    
        void onDownloadPause(String url);
    
        void updateProgress(String url, int progress);
    
        void onFail(String url);
    
        void onCancel(String url);
        
        }
    

    让Service实现DownloadView的方法,当DownloadManager的下载状态变化时,回调Service更新task数据。

    DownloadService.java

        private  ArrayList<Task> mTasks;
        private OnTaskDataChangeListener mListener;
        private DatabaseManager mDataBaseManager;
        private DownloadManager mDownloadManager;
        
        @Override
        public void onDownloadPause(String url) {
            int position = getPosition(url);
            Task task = mTasks.get(position);
            task.setDownloading(false);
    
            if (mListener != null) {
                mListener.onDataChange(position);
            }
        }
    
        @Override
        public void updateProgress(String url, int progress) {
            int position = getPosition(url);
            Task task = mTasks.get(position);
            task.setProgress(progress);
    
            if (progress < 100) {
                task.setDownloading(true);
            } else {
                task.setDownloading(false);
            }
    
            if (mListener != null) {
                mListener.onDataChange(position);
            }
    
            Log.d(TAG, "updateProgress: " + progress);
        }
    
        @Override
        public void onFail(String url) {
            int position = getPosition(url);
            Task task = mTasks.get(position);
            task.setProgress(-1);
            task.setDownloading(false);
    
            if (mListener != null) {
                mListener.onDataChange(position);
            }
        }
    
        @Override
        public void onCancel(String url) {
            int position = getPosition(url);
            Task task = mTasks.get(position);
            mTasks.remove(position);
    
            if (mDataBaseManager.query(task.getUrl()) != null) {
                mDataBaseManager.delete(url);
            }
    
            if (mListener != null) {
                mListener.onDataRemove(position);
            }
        }
    
        /**
         * 通过url找到当前task在list的position
         *
         * @param url
         * @return
         */
        private int getPosition(String url) {
            for (int i = 0; i < mTasks.size(); i++) {
                Task task = mTasks.get(i);
                if (url.equals(task.getUrl())) {
                    return i;
                }
            }
            return -1;
        }
    

    4.DownloadManager管理下载任务

    在DownloadManager使用HashMap管理DownloadAsyncTask。
    1.在事件“新建任务”,“开始下载”中,我们都要新建DownloadAsyncTask;
    2.当正在下载时,对于事件“暂停”和“取消”,我们只要根据url在map中找到当前asyncTask,将其暂停和取消。由于asyncTask的生命周期已经完成了,我们要将其在map中remove。
    3.当用户在暂停状态下点击了取消按钮,由于当前任务已经在map中remove,要重新新建DownloadAsyncTask,才能将其取消。

    根据以上分析,可以抽象出以下方法,并让DownloadManager继承并实现。

    DownloadModel.java

        public abstract class DownloadModel {
            abstract void addDownloadTask(String url);
            abstract void pauseDownload(String url);
            abstract void cancelDownload(String url);
        }
    

    DownloadManager.java

        public class DownloadManager extends DownloadModel {
    
            private HashMap<String, DownloadAsyncTask> mMap = new HashMap<>();
            private DownloadView mView;
    
            public DownloadManager(DownloadView view) {
                mView = view;
            }
    
    
            @Override
            public void addDownloadTask(final String url) {
                DownloadAsyncTask asyncTask = new DownloadAsyncTask(new DownloadAsyncTask.DownLoadListener() {
    
                    @Override
                    public void onDownloadPause() {
                        mMap.remove(url);
                        mView.onDownloadPause(url);
                    }
    
                    @Override
                    public void updateProgress(int progress) {
                        mView.updateProgress(url, progress);
                    }
    
                    @Override
                    public void onFail() {
                        mMap.remove(url);
                        mView.onFail(url);
                    }
    
                    @Override
                    public void onCancel() {
                        mMap.remove(url);
                        mView.onCancel(url);
    
                    }
    
                    @Override
                    public void onFinish() {
                        mMap.remove(url);
                    }
                });
                mMap.put(url, asyncTask);
                //实现多任务下载,开始任务
                asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url);
            }
    
            @Override
            public void pauseDownload(String url) {
                DownloadAsyncTask task = mMap.get(url);
                task.setPause();
                mMap.remove(task);
            }
    
            @Override
            public void cancelDownload(String url) {
                DownloadAsyncTask task = mMap.get(url);
                //当未下载时点击取消,要新建AsyncTask
                if (task == null) {
                    addDownloadTask(url);
                }
                task.setCancel();
                mMap.remove(task);
            }
    
        }
    

    同时我们还需要在DownloadAsyncTask创建DownloadListener接口回调将当前AsyncTask的下载状态状态返回给DownloadManager,并由DownloadManager再通过url返回给Service。

    DownloadAsyncTask.java

        public interface DownLoadListener {
            void onDownloadPause();
            void updateProgress(int progress);
            void onFail();
            void onCancel();
            void onFinish();
        }
    

    5.DownloadAsyncTask实现下载

    (1)创建类 DownloadAsyncTask

    DownloadAsyncTask extends AsyncTask<String, Integer, Integer>,其中String为下载的url,第一个Integer为下载进度,第二个Integer为下载状态。

    (2)下载的状态标识。

    DownloadAsyncTask.java

        private static final int STATUS_SUCCEED = 1;
        private static final int STATUS_PAUSED = 2;
        private static final int STATUS_CANCELED = 3;
        private static final int STATUS_FAILED = 4;
    

    当AsyncTask正在doInBackground()时,用户点击暂停或取消时,使用isPause或isCancel中断任务

    DownloadAsyncTask.java

        private boolean isPause = false;
        private boolean isCancel = false;
        
         public void setPause() {
            isPause = true;
        }
    
        public void setCancel() {
            isCancel = true;
        }
    

    (3)重写doInBackground方法

    下载要用到OkHttp,引入闭包

        compile 'com.squareup.okhttp3:okhttp:3.4.1'
    

    首先得到File文件。当新建AsyncTask时,有可能是“下载”,也有可能是在暂停状态下“取消”。若是取消,将file文件删除,并结束任务,返回状态STATUS_CANCELED。若是下载,通过file.exists()判断是重头下载还是继续下载,并得到已下载进度downloadedLength。然后通过OkHttp获取内容的大小contentLength。若contentLength为0,则无法下载;若contentLength == downloadedLength则表示下载完成。

    DownloadAsyncTask.java

         @Override
        protected Integer doInBackground(String... params) {
            String url = params[0];
            String name = url.substring(url.lastIndexOf("/"));
    
            long downloadedLength = 0;
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
            File file = new File(directory + name);
    
            //判断任务是开始还是取消
            if(isCancel){
                if(file.exists()){
                    file.delete();
                }
                return STATUS_CANCELED;
            }
    
            if(file.exists()){
                downloadedLength = file.length();
            }
    
            long contentLength = getContentLength(url);
            //无法下载
            if (contentLength == 0) {
                return STATUS_FAILED;
            } else if (contentLength == downloadedLength) {
                return STATUS_SUCCEED;
            }
    
            //开始下载
            isPause = false;
            isCancel = false;
    
            InputStream is = null;
            RandomAccessFile saveFile = null;
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder().
                    addHeader("RANGE", "bytes=" + downloadedLength + "-") //指定从哪一个字节下载
                    .url(url).build();
            try {
                Response response = client.newCall(request).execute();
                //写入到本地
                if (response != null) {
                    Log.d(TAG, "doInBackground: response not null");
                    is = response.body().byteStream();
                    saveFile = new RandomAccessFile(file, "rw");
                    saveFile.seek(downloadedLength);
                    int len;
                    byte[] buffer = new byte[1024];
                    while ((len = is.read(buffer)) != -1) {
                        if (isPause) {
                            return STATUS_PAUSED;
                        } else if (isCancel) {
                            if(file.exists()){
                                file.delete();
                            }
                            return STATUS_CANCELED;
                        }
                        //获取已下载的进度
                        saveFile.write(buffer, 0, len);
                        downloadedLength += len;
                        int progress = (int) (downloadedLength * 100 / contentLength);
                        publishProgress(progress);
                    }
    
                    response.body().close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                    if (saveFile != null) {
                        saveFile.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
    
                }
            }
    
            if (contentLength == downloadedLength) {
                return STATUS_SUCCEED;
            }
            return STATUS_FAILED;
        }
        
        
        private long getContentLength(String url) {
            long contentLength = 0;
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder().url(url).build();
            Response response = null;
            try {
                response = client.newCall(request).execute();
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (response != null && response.isSuccessful()) {
                contentLength = response.body().contentLength();
                response.close();
            } else {
                Log.d(TAG, "getContentLength: response null");
            }
            return contentLength;
        }
    

    源代码

    https://github.com/Luckychuan/MultiThreadDownloadDemo

    ·

    相关文章

      网友评论

        本文标题:使用Android基础知识编写一个多任务多线程断点下载示例

        本文链接:https://www.haomeiwen.com/subject/yhxtpftx.html