工作找完了,玩也玩完了,该好好学习了,最近我把《Java并发编程的艺术》这本书给读完了,对于并发编程以及线程池的使用还是不娴熟,我就在imooc上找到一个项目“Android-Service系列之断点续传下载“,这是我对这个项目在编写的时候记录。

涉及知识点

UI界面编写

数据库

Service

广播传递数据

多线程以及Handler

网络

这些应该是Android的基础,我就不累述了,到时候在代码中遇到了再进行解释。

这个项目主要的流程是:

Android 断点续传上传文件_SQL

一切的操作的开始是基于Activity的,但是我们的下载任务肯定是不能在Activity中进行的,因为假如我们的Activity切换成后台进程就有可能会被销毁(进程的优先级:前台,可见,服务,后台,空),所以我们将下载放在Service中是比较好的,但是Service和Activity一样是主线程,是不能进行数据的操作的,所以我们要利用到Thread或者是线程池,如果我们要可见下载进度的话,我们就需要通过广播的消息传递来更新UI上的进度,对于断点我们就需要实时将下载到的文件位置存储下来,所以我们利用数据库(稳定)存储进度。下载完成以后再将下载信息删除。

基础布局

这部分就不讲了,特别简单的一个布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tvFileName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <ProgressBar
        android:id="@+id/pbProgress"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/tvFileName"
        android:layout_below="@+id/tvFileName"/>

    <Button
        android:id="@+id/btStop"
        style="?android:buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_below="@id/pbProgress"
        android:text="停止"/>

    <Button
        android:id="@+id/btStart"
        style="?android:buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toLeftOf="@id/btStop"
        android:layout_below="@id/pbProgress"
        android:text="下载"/>


</RelativeLayout>

实体类

在这个项目里面我们需要定义两个实体类来进操作,一个是文件信息的实体类,一个是线程信息的实体类

文件信息

主要的文件相关信息

private int id;//文件id
private String url;//文件的下载url
private String fileName;//文件名
private int length;//文件长度
private int finished;//文件下载完成度
线程相关信息
private int id;//文件id
private String url;//文件下载url
private int start;//线程从哪里开始下载
private int end;//线程到哪里结束下载
private int finished;//完成多少

因为我们需要将文件信息进行储存以及传递,所以我们需要实现序列化接口

public class FileInfo implements Serializable

Activity

我们在Activity中需要对控件进行监听,如何通过Intent将信息进行传递。 当然这是最基础的,在之后我们有多个下载任务或者是下载完成我们就需要使用ListView去完成这个效果。先暂时这样写。

package com.gin.xjh.download_demo;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.gin.xjh.download_demo.entities.FileInfo;
import com.gin.xjh.download_demo.services.DownloadService;

public class MainActivity extends AppCompatActivity {

    private TextView mTvFileName;
    private ProgressBar mPbProgress;
    private Button mBtStop, mBtStart;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initEvent();
    }

    private void initEvent() {
        final FileInfo fileInfo = new FileInfo(0, "http://music.163.com/" +
                "song/media/outer/url?id=557581647.mp3", "一眼一生", 0, 0);
        mBtStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, DownloadService.class);
                intent.setAction(DownloadService.ACTION_START);
                intent.putExtra("fileInfo", fileInfo);
                startService(intent);
            }
        });
        mBtStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, DownloadService.class);
                intent.setAction(DownloadService.ACTION_STOP);
                intent.putExtra("fileInfo", fileInfo);
                startService(intent);
            }
        });
    }

    private void initView() {
        mTvFileName = findViewById(R.id.tvFileName);
        mPbProgress = findViewById(R.id.pbProgress);
        mBtStop = findViewById(R.id.btStop);
        mBtStart = findViewById(R.id.btStart);
    }
}

Service

当然我们将消息传递到了Service中,并且我们是通过Start方法启动的Service,所以我们需要在onStartCommand方法中对Intent传递的消息进行判断,我们就需要重写onStartCommand方法,但是我们需要进行网络下载,所以我们需要新建一个Thread去完成这个耗时操作。

package com.gin.xjh.download_demo.services;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;

import com.gin.xjh.download_demo.entities.FileInfo;

import java.net.HttpURLConnection;

public class DownloadService extends Service {

    public static final String DOWNLOAD_PATH =
            Environment.getExternalStorageDirectory().getAbsolutePath() +
                    "/downloads/";
    public static final String ACTION_START = "ACTION_START";
    public static final String ACTION_STOP = "ACTION_STOP";

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //从Activity中传来的数据
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
        if (ACTION_START.equals(intent.getAction())) {
            Log.i("test", "Start:" + fileInfo.toString());
        } else if (ACTION_STOP.equals(intent.getAction())) {
            Log.i("test", "Stop:" + fileInfo.toString());
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    class InitThread extends Thread {
        private FileInfo mFileInfo = null;

        public InitThread(FileInfo mFileInfo) {
            this.mFileInfo = mFileInfo;
        }

        @Override
        public void run() {
            //网络下载操作
        }
    }
}

网络下载初始化

因为我们是最基础的下载,所以我们使用的还是HttpURLConnection进行网络的相关操作,并且因为我们是从服务器中下载文件,所以我们选择的是GET方法来获取数据。 我们根据url获取到文件的相关数据,对长度进行初始化,以及创建下载文件相关(检查路径是否存在,如果没有则进行创建) PS:网络链接,以及流文件使用完后进行关闭,防止内存泄漏。

@Override
@Override
public void run() {
    HttpURLConnection conn = null;
    RandomAccessFile raf = null;
    try {
        //连接网络文件
        URL url = new URL(mFileInfo.getUrl());
        conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(3000);
        conn.setRequestMethod("GET");
        int length = -1;
        if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
            //获得文件长度
            length = conn.getContentLength();
        }
        if (length <= 0) {
            return;
        }
        //在本地创建文件
        File dir = new File(DOWNLOAD_PATH);//验证下载地址
        if (!dir.exists()) {
            dir.mkdir();
        }
        File file = new File(dir, mFileInfo.getFileName());
        raf = new RandomAccessFile(file, "rwd");//r:读权限,w:写权限,d:删除权限
        //设置文件长度
        raf.setLength(length);
        mFileInfo.setLength(length);
        mHandler.obtainMessage(MSG_INIT, mFileInfo).sendToTarget();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        conn.disconnect();
        try {
            raf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

数据库初始化

我们要将线程的相关信息存入数据库中,这样我们才能做到断点续传,因为,没有记录的话,下一次的下载就不知道从哪里开始了,所以我们需要使用数据库(关于数据库的基本操作可以看我的另一篇博客:传送门)。在这里我们需要几个操作:

定义数据库帮助类

这里我们继承SQLiteOpenHelper,并且将数据库的创建以及更新重写好。

package com.gin.xjh.download_demo.db;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DBHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "download.db";
    private static final int VERSION = 1;
    private static final String SQL_CREATE = "create table thread_info(_id integer primary key autoincrement," +
            "thread_id integer,url text,start integer,ends integer,finished integer)";
    private static final String SQL_DROP = "drop table if exists thread_info";

    public DBHelper(Context context) {
        super(context, DB_NAME, null, VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int i, int i1) {
        db.execSQL(SQL_DROP);
        db.execSQL(SQL_CREATE);
    }
}
定义数据访问接口

虽然我们直接在帮助类中写增删改查的操作也是可以的,但是使用接口的话就可以定义一个规范,并且在之后我们进行接口的实现也可以使得代码的耦合度降到最低。

package com.gin.xjh.download_demo.db;

import com.gin.xjh.download_demo.entities.ThreadInfo;

import java.util.List;

/**
 * 数据访问接口
 */

public interface ThreadDAO {

    /**
     * 插入线程信息
     */
    void insertThread(ThreadInfo threadInfo);

    /**
     * 删除线程信息
     */
    void deleteThread(String url, int thread_id);

    /**
     * 更新线程信息
     */
    void updateThread(String url, int thread_id, int finished);

    /**
     * 查询文件的线程信息
     */
    List<ThreadInfo> getThreads(String url);

    /**
     * 线程信息是否存在
     */
    boolean isExists(String url, int thread_id);
}
实现数据访问接口

就是简单的增删改查的操作

package com.gin.xjh.download_demo.db;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.gin.xjh.download_demo.entities.ThreadInfo;

import java.util.ArrayList;
import java.util.List;

/**
 * 数据访问接口的实现
 */

public class ThreadDAOImpl implements ThreadDAO {

    private DBHelper mHelper = null;

    public ThreadDAOImpl(Context context) {
        mHelper = new DBHelper(context);
    }


    @Override
    public void insertThread(ThreadInfo threadInfo) {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        db.execSQL(
                "insert into thread_info(thread_id,url,start,ends,finished) values(?,?,?,?,?)",
                new Object[]{threadInfo.getId(), threadInfo.getUrl(), threadInfo.getStart(),
                        threadInfo.getEnds(), threadInfo.getFinished()});
        db.close();
    }

    @Override
    public void deleteThread(String url, int thread_id) {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        db.execSQL(
                "delete from thread_info where url = ? and thread_id = ?",
                new Object[]{url, thread_id});
        db.close();
    }

    @Override
    public void updateThread(String url, int thread_id, int finished) {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        db.execSQL(
                "update thread_info set finished = ? where url = ? and thread_id = ?",
                new Object[]{finished, url, thread_id});
        db.close();
    }

    @Override
    public List<ThreadInfo> getThreads(String url) {
        List<ThreadInfo> list = new ArrayList<>();
        SQLiteDatabase db = mHelper.getReadableDatabase();
        Cursor cursor = db.rawQuery("select * from thread_info where url = ?",
                new String[]{url});
        while (cursor.moveToNext()) {
            ThreadInfo thread = new ThreadInfo();
            thread.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
            thread.setUrl(cursor.getString(cursor.getColumnIndex("url")));
            thread.setStart(cursor.getInt(cursor.getColumnIndex("start")));
            thread.setEnds(cursor.getInt(cursor.getColumnIndex("ends")));
            thread.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));
            list.add(thread);
        }
        cursor.close();
        db.close();
        return list;
    }

    @Override
    public boolean isExists(String url, int thread_id) {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from thread_info where url = ? and thread_id = ?",
                new String[]{url, thread_id + ""});
        boolean exists = cursor.moveToNext();
        cursor.close();
        db.close();
        return exists;
    }
}

任务下载类

我们需要定义一个任务下载类,在里面启动线程下载我们需要的文件。

在文件的下载中我们使用的是RandomAccessFile,这个类的seek方法可以在读写的时候根据我们的需要在我们设定的位置开始读写。 然后我们对于暂停按钮的监听,定义一个标值位来判断是否点击暂停,并且在每500毫秒刷新一次进度,并且保存线程下载信息,以防程序奔溃,数据没有记录。

package com.gin.xjh.download_demo.services;

import android.content.Context;
import android.content.Intent;
import android.util.Log;

import com.gin.xjh.download_demo.db.ThreadDAO;
import com.gin.xjh.download_demo.db.ThreadDAOImpl;
import com.gin.xjh.download_demo.entities.FileInfo;
import com.gin.xjh.download_demo.entities.ThreadInfo;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

/**
 * 下载任务类
 */

public class DownloadTask {

    private Context mContext = null;
    private FileInfo mFileInfo = null;
    private ThreadDAO mDao = null;
    private int mFinished;
    public boolean isPause = false;

    public DownloadTask(Context mContext, FileInfo mFileInfo) {
        this.mContext = mContext;
        this.mFileInfo = mFileInfo;
        mDao = new ThreadDAOImpl(mContext);
    }

    public void download() {
        //读取数据库中的线程信息
        List<ThreadInfo> threadInfos = mDao.getThreads(mFileInfo.getUrl());
        ThreadInfo threadInfo = null;
        if (threadInfos.size() == 0) {
            //初始化线程信息对象
            threadInfo = new ThreadInfo(0, mFileInfo.getUrl(), 0, mFileInfo.getLength(), 0);
        } else {
            threadInfo = threadInfos.get(0);
        }
        //创建子线程进行下载
        new DownloadThread(threadInfo).start();
    }

    /**
     * 下载线程
     */
    class DownloadThread extends Thread {
        private ThreadInfo mThreadInfo = null;

        public DownloadThread(ThreadInfo mThreadInfo) {
            this.mThreadInfo = mThreadInfo;
        }

        @Override
        public void run() {
            //向数据库插入线程信息
            if (!mDao.isExists(mThreadInfo.getUrl(), mThreadInfo.getId())) {
                mDao.insertThread(mThreadInfo);
            }
            //设置下载位置
            HttpURLConnection conn = null;
            RandomAccessFile raf = null;
            InputStream input = null;
            try {
                URL url = new URL(mThreadInfo.getUrl());
                conn = (HttpURLConnection) url.openConnection();
                conn.setConnectTimeout(3000);
                conn.setRequestMethod("GET");
                //设置下载位置
				int start = mThreadInfo.getStart() + mThreadInfo.getFinished();
				conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.getEnds());
				//设置文件写入位置
				File file = new File(DownloadService.DOWNLOAD_PATH, mFileInfo.getFileName());
				raf = new RandomAccessFile(file, "rwd");
				raf.seek(start);
				//开始下载
				mFinished += mThreadInfo.getFinished();
                Intent intent = new Intent(DownloadService.ACTION_UPDATE);
                if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
                    //读取数据
                    input = conn.getInputStream();
                    byte[] buffer = new byte[1024 * 4];
                    int len = -1;
                    long time = System.currentTimeMillis();
                    while ((len = input.read(buffer)) != -1) {
                        //写入文件
                        raf.write(buffer, 0, len);
                        //把下载进度发送广播给Activity
                        mFinished += len;
                        if (System.currentTimeMillis() - time > 500) {
                            time = System.currentTimeMillis();
                            //保存下载进度
                            mDao.updateThread(mFileInfo.getUrl(), mThreadInfo.getId(), mFinished);
                            intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());
                            mContext.sendBroadcast(intent);
                            if(isPause){
                                return;
                            }
                        }
                        //在暂停时保存下载进度
                        if (isPause) {
                            mDao.updateThread(mFileInfo.getUrl(), mThreadInfo.getId(), mFinished);
                            return;
                        }
                    }
                    intent.putExtra("finished", 100);
                    mContext.sendBroadcast(intent);
                    //删除线程信息
                    mDao.deleteThread(mFileInfo.getUrl(), mFileInfo.getId());
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                conn.disconnect();
                try {
                    raf.close();
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

之后在启动这个下载任务就好了。

mTask = new DownloadTask(DownloadService.this,fileInfo);
mTask.download();

广播接收器

因为我们是通过广播的形式来更新主线程的进度条,所以我们要编写一个广播接收器,并且注册广播接收器。当我们获取到线程中的广播以后,根据广播中传递的消息对ProgressBar的进度进行更新。

/**
 * 更新进度条的广播接收器
 */
BroadcastReceiver mReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if(DownloadService.ACTION_UPDATE.equals(intent.getAction())){
            int finished = intent.getIntExtra("finished",0);
            mPbProgress.setProgress(finished);
        }
    }
};

对于广播接收器的注册我采用的是动态注册的方法,这里我就不多说了。 这样我们这个单线程的断点续传功能就已经完成了。 多线程下载的功能:因为是在这个代码上进行修改的,所有不太好写,之后会在GitHub上更新,直接看更新细节就能看到我有哪些是进行修改的。 多线程的主要思想:通过将文件分段的方法进行下载,主要是要注意方法的同步,因为涉及到多线程的访问。