博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
(Android小应用)在Android中实现多线程断点下载(连载二)
阅读量:2227 次
发布时间:2019-05-09

本文共 33529 字,大约阅读时间需要 111 分钟。

前面已经讲了通过多线程下载网络文件的一部分核心代码,下面继续做我们的项目

当然如果我们做小一点的Android项目的时候可以先做界面,然后根据需要来扩展所需要的功能,当然有的时候我们宁愿先实现业务,业务功能做好了再实现界面,因为界面里面基本不含有技术,况且我们只是为了学习,界面不需要多美观的,能用就行,OK,继续

先做数据库这层

关于建Android项目方法的截图就省略了,我用的模拟器版本是2.3.3,至今为至最新的吧

首先建一个类DBOpenHelper继续自SQLiteOpenHelper

package   
com.studio.service;
import
android.content.Context;
import
android.database.sqlite.SQLiteDatabase;
import
android.database.sqlite.SQLiteOpenHelper;
public
class
DBOpenHelper
extends
SQLiteOpenHelper {
private
static
final
String DBNAME
=
"
download.db
"
;
private
static
final
int
VERSION
=
1
;
public
DBOpenHelper(Context context) {
super
(context, DBNAME,
null
, VERSION); } @Override
public
void
onCreate(SQLiteDatabase db) { db.execSQL(
"
CREATE TABLE IF NOT EXISTS filedownlog (id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, downlength INTEGER)
"
); } @Override
public
void
onUpgrade(SQLiteDatabase db,
int
oldVersion,
int
newVersion) { db.execSQL(
"
DROP TABLE IF EXISTS filedownlog
"
); onCreate(db); }}

这里定义的数据名字叫download.db,版本号为1,表名filedownlog,里面有三个字段,一个id,主键,下载路径downpath,线程ID和一个下载长度downlength

然后建一个FileService类

package   
com.studio.service;
import
java.util.HashMap;
import
java.util.Map;
import
android.content.Context;
import
android.database.Cursor;
import
android.database.sqlite.SQLiteDatabase;
/**
* 业务bean *
*/
public
class
FileService {
private
DBOpenHelper openHelper;
public
FileService(Context context) { openHelper
=
new
DBOpenHelper(context); }
/**
* 获取每条线程已经下载的文件长度 * *
@param
path *
@return
*/
public
Map
<
Integer, Integer
>
getData(String path) { SQLiteDatabase db
=
openHelper.getReadableDatabase(); Cursor cursor
=
db .rawQuery(
"
select threadid, downlength from filedownlog where downpath=?
"
,
new
String[] { path }); Map
<
Integer, Integer
>
data
=
new
HashMap
<
Integer, Integer
>
();
while
(cursor.moveToNext()) { data.put(cursor.getInt(
0
), cursor.getInt(
1
)); } cursor.close(); db.close();
return
data; }
/**
* 保存每条线程已经下载的文件长度 * *
@param
path *
@param
map
*/
public
void
save(String path, Map
<
Integer, Integer
>
map) {
//
int threadid,
//
int position
SQLiteDatabase db
=
openHelper.getWritableDatabase(); db.beginTransaction();
try
{
for
(Map.Entry
<
Integer, Integer
>
entry : map.entrySet()) { db.execSQL(
"
insert into filedownlog(downpath, threadid, downlength) values(?,?,?)
"
,
new
Object[] { path, entry.getKey(), entry.getValue() }); } db.setTransactionSuccessful(); }
finally
{ db.endTransaction(); } db.close(); }
/**
* 实时更新每条线程已经下载的文件长度 * *
@param
path *
@param
map
*/
public
void
update(String path, Map
<
Integer, Integer
>
map) { SQLiteDatabase db
=
openHelper.getWritableDatabase(); db.beginTransaction();
try
{
for
(Map.Entry
<
Integer, Integer
>
entry : map.entrySet()) { db.execSQL(
"
update filedownlog set downlength=? where downpath=? and threadid=?
"
,
new
Object[] { entry.getValue(), path, entry.getKey() }); } db.setTransactionSuccessful(); }
finally
{ db.endTransaction(); } db.close(); }
/**
* 当文件下载完成后,删除对应的下载记录 * *
@param
path
*/
public
void
delete(String path) { SQLiteDatabase db
=
openHelper.getWritableDatabase(); db.execSQL(
"
delete from filedownlog where downpath=?
"
,
new
Object[] { path }); db.close(); }}

类中都有相应的注释的,看不懂可以慢慢看……

因为要用到进度条,所以先把进度条建了

package   
com.studio.net.download;
public
interface
DownloadProgressListener {
public
void
onDownloadSize(
int
size);}
这里我们定义了一个进度条监听接口,由实现此接口的类来调用onDownloadSize()方法

新建一个类DownloadThread继承自Thread

package   
com.studio.net.download;
import
java.io.File;
import
java.io.InputStream;
import
java.io.RandomAccessFile;
import
java.net.HttpURLConnection;
import
java.net.URL;
import
android.util.Log;
public
class
DownloadThread
extends
Thread {
private
static
final
String TAG
=
"
DownloadThread
"
;
private
File saveFile;
private
URL downUrl;
private
int
block;
/*
下载开始位置
*/
private
int
threadId
=
-
1
;
private
int
downLength;
private
boolean
finish
=
false
;
private
FileDownloader downloader;
public
DownloadThread(FileDownloader downloader, URL downUrl, File saveFile,
int
block,
int
downLength,
int
threadId) {
this
.downUrl
=
downUrl;
this
.saveFile
=
saveFile;
this
.block
=
block;
this
.downloader
=
downloader;
this
.threadId
=
threadId;
this
.downLength
=
downLength; } @Override
public
void
run() {
if
(downLength
<
block) {
//
未下载完成
try
{ HttpURLConnection http
=
(HttpURLConnection) downUrl .openConnection(); http.setConnectTimeout(
5
*
1000
); http.setRequestMethod(
"
GET
"
); http.setRequestProperty(
"
Accept
"
,
"
image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
"
); http.setRequestProperty(
"
Accept-Language
"
,
"
zh-CN
"
); http.setRequestProperty(
"
Referer
"
, downUrl.toString()); http.setRequestProperty(
"
Charset
"
,
"
UTF-8
"
);
int
startPos
=
block
*
(threadId
-
1
)
+
downLength;
//
开始位置
int
endPos
=
block
*
threadId
-
1
;
//
结束位置
http.setRequestProperty(
"
Range
"
,
"
bytes=
"
+
startPos
+
"
-
"
+
endPos);
//
设置获取实体数据的范围
http.setRequestProperty(
"
User-Agent
"
,
"
Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
"
); http.setRequestProperty(
"
Connection
"
,
"
Keep-Alive
"
); InputStream inStream
=
http.getInputStream();
byte
[] buffer
=
new
byte
[
1024
];
int
offset
=
0
; print(
"
Thread
"
+
this
.threadId
+
"
start download from position
"
+
startPos); RandomAccessFile threadfile
=
new
RandomAccessFile(
this
.saveFile,
"
rwd
"
); threadfile.seek(startPos);
while
((offset
=
inStream.read(buffer,
0
,
1024
))
!=
-
1
) { threadfile.write(buffer,
0
, offset); downLength
+=
offset; downloader.update(
this
.threadId, downLength); downloader.append(offset); } threadfile.close(); inStream.close(); print(
"
Thread
"
+
this
.threadId
+
"
download finish
"
);
this
.finish
=
true
; }
catch
(Exception e) {
this
.downLength
=
-
1
; print(
"
Thread
"
+
this
.threadId
+
"
:
"
+
e); } } }
private
static
void
print(String msg) { Log.i(TAG, msg); }
/**
* 下载是否完成 * *
@return
*/
public
boolean
isFinish() {
return
finish; }
/**
* 已经下载的内容大小 * *
@return
如果返回值为-1,代表下载失败
*/
public
long
getDownLength() {
return
downLength; }}

再建一个文件下载器FileDownloader类

package   
com.studio.net.download;
import
java.io.File;
import
java.io.RandomAccessFile;
import
java.net.HttpURLConnection;
import
java.net.URL;
import
java.util.LinkedHashMap;
import
java.util.Map;
import
java.util.UUID;
import
java.util.concurrent.ConcurrentHashMap;
import
java.util.regex.Matcher;
import
java.util.regex.Pattern;
import
com.studio.service.FileService;
import
android.content.Context;
import
android.util.Log;
public
class
FileDownloader {
private
static
final
String TAG
=
"
FileDownloader
"
;
private
Context context;
private
FileService fileService;
/*
已下载文件长度
*/
private
int
downloadSize
=
0
;
/*
原始文件长度
*/
private
int
fileSize
=
0
;
/*
线程数
*/
private
DownloadThread[] threads;
/*
本地保存文件
*/
private
File saveFile;
/*
缓存各线程下载的长度
*/
private
Map
<
Integer, Integer
>
data
=
new
ConcurrentHashMap
<
Integer, Integer
>
();
/*
每条线程下载的长度
*/
private
int
block;
/*
下载路径
*/
private
String downloadUrl;
/**
* 获取线程数
*/
public
int
getThreadSize() {
return
threads.length; }
/**
* 获取文件大小 * *
@return
*/
public
int
getFileSize() {
return
fileSize; }
/**
* 累计已下载大小 * *
@param
size
*/
protected
synchronized
void
append(
int
size) { downloadSize
+=
size; }
/**
* 更新指定线程最后下载的位置 * *
@param
threadId * 线程id *
@param
pos * 最后下载的位置
*/
protected
synchronized
void
update(
int
threadId,
int
pos) {
this
.data.put(threadId, pos);
this
.fileService.update(
this
.downloadUrl,
this
.data); }
/**
* 构建文件下载器 * *
@param
downloadUrl * 下载路径 *
@param
fileSaveDir * 文件保存目录 *
@param
threadNum * 下载线程数
*/
public
FileDownloader(Context context, String downloadUrl, File fileSaveDir,
int
threadNum) {
try
{
this
.context
=
context;
this
.downloadUrl
=
downloadUrl; fileService
=
new
FileService(
this
.context); URL url
=
new
URL(
this
.downloadUrl);
if
(
!
fileSaveDir.exists()) fileSaveDir.mkdirs();
this
.threads
=
new
DownloadThread[threadNum]; HttpURLConnection conn
=
(HttpURLConnection) url.openConnection(); conn.setConnectTimeout(
5
*
1000
); conn.setRequestMethod(
"
GET
"
); conn.setRequestProperty(
"
Accept
"
,
"
image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
"
); conn.setRequestProperty(
"
Accept-Language
"
,
"
zh-CN
"
); conn.setRequestProperty(
"
Referer
"
, downloadUrl); conn.setRequestProperty(
"
Charset
"
,
"
UTF-8
"
); conn.setRequestProperty(
"
User-Agent
"
,
"
Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
"
); conn.setRequestProperty(
"
Connection
"
,
"
Keep-Alive
"
); conn.connect(); printResponseHeader(conn);
if
(conn.getResponseCode()
==
200
) {
this
.fileSize
=
conn.getContentLength();
//
根据响应获取文件大小
if
(
this
.fileSize
<=
0
)
throw
new
RuntimeException(
"
Unkown file size
"
); String filename
=
getFileName(conn);
//
获取文件名称
this
.saveFile
=
new
File(fileSaveDir, filename);
//
构建保存文件
Map
<
Integer, Integer
>
logdata
=
fileService .getData(downloadUrl);
//
获取下载记录
if
(logdata.size()
>
0
) {
//
如果存在下载记录
for
(Map.Entry
<
Integer, Integer
>
entry : logdata.entrySet()) data.put(entry.getKey(), entry.getValue());
//
把各条线程已经下载的数据长度放入data中
}
if
(
this
.data.size()
==
this
.threads.length) {
//
下面计算所有线程已经下载的数据长度
for
(
int
i
=
0
; i
<
this
.threads.length; i
++
) {
this
.downloadSize
+=
this
.data.get(i
+
1
); } print(
"
已经下载的长度
"
+
this
.downloadSize); }
//
计算每条线程下载的数据长度
this
.block
=
(
this
.fileSize
%
this
.threads.length)
==
0
?
this
.fileSize
/
this
.threads.length :
this
.fileSize
/
this
.threads.length
+
1
; }
else
{
throw
new
RuntimeException(
"
server no response
"
); } }
catch
(Exception e) { print(e.toString());
throw
new
RuntimeException(
"
don't connection this url
"
); } }
/**
* 获取文件名
*/
private
String getFileName(HttpURLConnection conn) { String filename
=
this
.downloadUrl.substring(
this
.downloadUrl .lastIndexOf(
'
/
'
)
+
1
);
if
(filename
==
null
||
""
.equals(filename.trim())) {
//
如果获取不到文件名称
for
(
int
i
=
0
;; i
++
) { String mine
=
conn.getHeaderField(i);
if
(mine
==
null
)
break
;
if
(
"
content-disposition
"
.equals(conn.getHeaderFieldKey(i) .toLowerCase())) { Matcher m
=
Pattern.compile(
"
.*filename=(.*)
"
).matcher( mine.toLowerCase());
if
(m.find())
return
m.group(
1
); } } filename
=
UUID.randomUUID()
+
"
.tmp
"
;
//
默认取一个文件名
}
return
filename; }
/**
* 开始下载文件 * *
@param
listener * 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null *
@return
已下载文件大小 *
@throws
Exception
*/
public
int
download(DownloadProgressListener listener)
throws
Exception {
try
{ RandomAccessFile randOut
=
new
RandomAccessFile(
this
.saveFile,
"
rw
"
);
if
(
this
.fileSize
>
0
) randOut.setLength(
this
.fileSize); randOut.close(); URL url
=
new
URL(
this
.downloadUrl);
if
(
this
.data.size()
!=
this
.threads.length) {
this
.data.clear();
for
(
int
i
=
0
; i
<
this
.threads.length; i
++
) {
this
.data.put(i
+
1
,
0
);
//
初始化每条线程已经下载的数据长度为0
} }
for
(
int
i
=
0
; i
<
this
.threads.length; i
++
) {
//
开启线程进行下载
int
downLength
=
this
.data.get(i
+
1
);
if
(downLength
<
this
.block
&&
this
.downloadSize
<
this
.fileSize) {
//
判断线程是否已经完成下载,否则继续下载
this
.threads[i]
=
new
DownloadThread(
this
, url,
this
.saveFile,
this
.block,
this
.data.get(i
+
1
), i
+
1
);
this
.threads[i].setPriority(
7
);
this
.threads[i].start(); }
else
{
this
.threads[i]
=
null
; } }
this
.fileService.save(
this
.downloadUrl,
this
.data);
boolean
notFinish
=
true
;
//
下载未完成
while
(notFinish) {
//
循环判断所有线程是否完成下载
Thread.sleep(
900
); notFinish
=
false
;
//
假定全部线程下载完成
for
(
int
i
=
0
; i
<
this
.threads.length; i
++
) {
if
(
this
.threads[i]
!=
null
&&
!
this
.threads[i].isFinish()) {
//
如果发现线程未完成下载
notFinish
=
true
;
//
设置标志为下载没有完成
if
(
this
.threads[i].getDownLength()
==
-
1
) {
//
如果下载失败,再重新下载
this
.threads[i]
=
new
DownloadThread(
this
, url,
this
.saveFile,
this
.block,
this
.data.get(i
+
1
), i
+
1
);
this
.threads[i].setPriority(
7
);
this
.threads[i].start(); } } }
if
(listener
!=
null
) listener.onDownloadSize(
this
.downloadSize);
//
通知目前已经下载完成的数据长度
} fileService.delete(
this
.downloadUrl); }
catch
(Exception e) { print(e.toString());
throw
new
Exception(
"
file download fail
"
); }
return
this
.downloadSize; }
/**
* 获取Http响应头字段 * *
@param
http *
@return
*/
public
static
Map
<
String, String
>
getHttpResponseHeader( HttpURLConnection http) { Map
<
String, String
>
header
=
new
LinkedHashMap
<
String, String
>
();
for
(
int
i
=
0
;; i
++
) { String mine
=
http.getHeaderField(i);
if
(mine
==
null
)
break
; header.put(http.getHeaderFieldKey(i), mine); }
return
header; }
/**
* 打印Http头字段 * *
@param
http
*/
public
static
void
printResponseHeader(HttpURLConnection http) { Map
<
String, String
>
header
=
getHttpResponseHeader(http);
for
(Map.Entry
<
String, String
>
entry : header.entrySet()) { String key
=
entry.getKey()
!=
null
?
entry.getKey()
+
"
:
"
:
""
; print(key
+
entry.getValue()); } }
private
static
void
print(String msg) { Log.i(TAG, msg); }}

OK至此业务写完了

现在来设计界面UI,由于要使用到String类型的字符串,所以我们先在String.xml中定义一下

添加

<   
string
name
="path"
>
下载路径
<
string
name
="button"
>
下载
<
string
name
="suceess"
>
下载完成
<
string
name
="error"
>
下载失败
<
string
name
="sdcarderror"
>
SDCard不存在或写保护

界面main.xml

<
LinearLayout
xmlns:android
="http://schemas.android.com/apk/res/android"
android:orientation
="vertical"
android:layout_width
="fill_parent"
android:layout_height
="fill_parent"
>
<
TextView
android:layout_width
="fill_parent"
android:layout_height
="wrap_content"
android:text
="@string/path"
/>
<
EditText
android:layout_width
="fill_parent"
android:layout_height
="wrap_content"
android:text
="http://www.mp3upload.ca/upload/42981-fdt.mp3"
android:id
="@+id/path"
/>
<
Button
android:layout_width
="wrap_content"
android:layout_height
="wrap_content"
android:text
="@string/button"
android:id
="@+id/button"
/>
<
ProgressBar
android:layout_width
="fill_parent"
android:layout_height
="20px"
style
="?android:attr/progressBarStyleHorizontal"
android:id
="@+id/downloadbar"
/>
<
TextView
android:layout_width
="fill_parent"
android:layout_height
="wrap_content"
android:gravity
="center"
android:id
="@+id/result"
/>

主类MainActivity

package   
com.studio.download;
import
java.io.File;
import
com.studio.net.download.DownloadProgressListener;
import
com.studio.net.download.FileDownloader;
import
android.app.Activity;
import
android.os.Bundle;
import
android.os.Environment;
import
android.os.Handler;
import
android.os.Message;
import
android.view.View;
import
android.widget.Button;
import
android.widget.EditText;
import
android.widget.ProgressBar;
import
android.widget.TextView;
import
android.widget.Toast;
public
class
MainActivity
extends
Activity {
private
EditText pathText;
private
ProgressBar downloadbar;
private
TextView resultView;
//
它用于往消息队列发送消息,当Handler被创建时会自动绑定到Handler被创建时所在的线程所绑定的消息队列
private
Handler handler
=
new
Handler() { @Override
public
void
handleMessage(Message msg) {
switch
(msg.what) {
case
1
: downloadbar.setProgress(msg.getData().getInt(
"
size
"
));
//
把当前已经下载的数据长度设置为进度条的当前刻度
float
num
=
(
float
) downloadbar.getProgress()
/
(
float
) downloadbar.getMax();
int
result
=
(
int
) (num
*
100
); resultView.setText(result
+
"
%
"
);
if
(downloadbar.getProgress()
==
downloadbar.getMax()) { Toast.makeText(MainActivity.
this
, R.string.suceess,
1
) .show(); }
break
;
case
-
1
: Toast.makeText(MainActivity.
this
, R.string.error,
1
).show();
break
; } } }; @Override
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState); setContentView(R.layout.main); pathText
=
(EditText) findViewById(R.id.path); downloadbar
=
(ProgressBar) findViewById(R.id.downloadbar); resultView
=
(TextView) findViewById(R.id.result); Button button
=
(Button) findViewById(R.id.button); button.setOnClickListener(
new
View.OnClickListener() { @Override
public
void
onClick(View v) {
if
(Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { String path
=
pathText.getText().toString(); download(path, Environment.getExternalStorageDirectory()); }
else
{ Toast.makeText(MainActivity.
this
, R.string.sdcarderror,
1
) .show(); } } }); }
//
对UI控件的更新只能由主线程(UI线程)负责,如果不在UI线程更新控件,更新后的值不会被重绘到屏幕上
private
void
download(
final
String path,
final
File saveDir) {
new
Thread(
new
Runnable() { @Override
public
void
run() {
try
{ FileDownloader loader
=
new
FileDownloader( MainActivity.
this
, path, saveDir,
3
); downloadbar.setMax(loader.getFileSize());
//
设置进度条的最大刻度为文件的大小
loader.download(
new
DownloadProgressListener() { @Override
public
void
onDownloadSize(
int
size) { Message msg
=
new
Message(); msg.what
=
1
; msg.getData().putInt(
"
size
"
, size); handler.sendMessage(msg);
//
msg.target = handler;
} }); }
catch
(Exception e) { Message msg
=
new
Message(); msg.what
=
-
1
; handler.sendMessage(msg); } } }).start(); }}

当然别忘了添加权限

<   
uses-permission
android:name
="android.permission.INTERNET"
>
<
uses-permission
android:name
="android.permission.WRITE_EXTERNAL_STORAGE"
>
<
uses-permission
android:name
="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
>
OK,搞定,运行一下

如图所示

由于这个mp3有点大,耐心等待

OK,终于下载完成了,看看sdcard吧,上面有我们刚下载的歌曲文件了,播放一下试试,呵呵

转载地址:http://gqhfb.baihongyu.com/

你可能感兴趣的文章
【LEETCODE】228-Summary Ranges
查看>>
【LEETCODE】27-Remove Element
查看>>
【LEETCODE】66-Plus One
查看>>
【LEETCODE】26-Remove Duplicates from Sorted Array
查看>>
【LEETCODE】118-Pascal's Triangle
查看>>
【LEETCODE】119-Pascal's Triangle II
查看>>
【LEETCODE】190-Reverse Bits
查看>>
【LEETCODE】67-Add Binary
查看>>
【LEETCODE】223-Rectangle Area
查看>>
【LEETCODE】12-Integer to Roman
查看>>
【学习方法】如何分析源代码
查看>>
【LEETCODE】61- Rotate List [Python]
查看>>
【算法】- 动态规划的编织艺术
查看>>
深度学习的主要应用举例
查看>>
word2vec 模型思想和代码实现
查看>>
怎样做情感分析
查看>>
用深度神经网络处理NER命名实体识别问题
查看>>
用 RNN 训练语言模型生成文本
查看>>
RNN与机器翻译
查看>>
用 Recursive Neural Networks 得到分析树
查看>>