工作找完了,玩也玩完了,该好好学习了,最近我把《Java并发编程的艺术》这本书给读完了,对于并发编程以及线程池的使用还是不娴熟,我就在imooc上找到一个项目“Android-Service系列之断点续传下载“,这是我对这个项目在编写的时候记录。
这些应该是Android的基础,我就不累述了,到时候在代码中遇到了再进行解释。
这个项目主要的流程是:
一切的操作的开始是基于Activity的,但是我们的下载任务肯定是不能在Activity中进行的,因为假如我们的Activity切换成后台进程就有可能会被销毁(进程的优先级:前台,可见,服务,后台,空),所以我们将下载放在Service中是比较好的,但是Service和Activity一样是主线程,是不能进行数据的操作的,所以我们要利用到Thread或者是线程池,如果我们要可见下载进度的话,我们就需要通过广播的消息传递来更新UI上的进度,对于断点我们就需要实时将下载到的文件位置存储下来,所以我们利用数据库(稳定)存储进度。下载完成以后再将下载信息删除。
这部分就不讲了,特别简单的一个布局
<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中需要对控件进行监听,如何通过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中,并且我们是通过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上更新,直接看更新细节就能看到我有哪些是进行修改的。
多线程的主要思想:通过将文件分段的方法进行下载,主要是要注意方法的同步,因为涉及到多线程的访问。
GitHub传送门:戳这里