全屏浏览
缩小浏览
回到页首

android程序---->android多线程下载(一)

  多线程下载是加快下载速度的一种方式,通过开启多个线程去执行一个任务,可以使任务的执行速度变快。多线程的任务下载时常都会使用得到断点续传下载,就是我们在一次下载未结束时退出下载,第二次下载时会接着第一次下载的进度继续下载。对于android中的下载,我想分多个部分去讲解分析。今天,我们就首先开始android中下载断点续传代码的实现。源码下载:java多线程断点续传(一) 。关于多线程下载单个文件的实现,请参见博客:android程序---->android多线程下载(二)

 

目录导航

  1.   android中断点续传的思路
  2.   android断点续传基本的UI
  3.   android断点续传的工具类
  4.   下载暂停取消的具体流程
  5.   友情链接

android中断点续传的思路

一、 断点续传的实现步骤:

第一步: 我们要获得下载资源的的长度,用http请求中HttpURLConnection的getContentLength()方法

第二步:在本地创建一个文件,设计其长度。File file = new File()

第三步:从数据库中获得上次下载的进度,当暂停下载时,存储下载的状态,用到数据库的知识

第四步:从上次下载的位置下载数据,同时保存进度到数据库:RandomAccessFile的seek方法与HttpURLConnection的setRequestProperty方法

第五步:将下载进度回传到Activity,可以通过Intent将数据广播到Activity中

第六步:下载完成后删除下载信息,在数据库中删除相应的信息

 

二、 断点续传实现的流程图:

 

android断点续传基本的UI编写

明白了上述的实现流程,现在我们开始一个android项目,开始断点续传代码的编写,项目结构如下:

运行的截图如下:

一、 编写基本的UI,三个TextView,分别显示文件名、下载进度和下载速度,一个ProgressBar。二个Button,分别用于开始下载、暂停下载和取消下载。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.linux.continuedownload.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:layout_marginLeft="80dp"
        android:id="@+id/progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:layout_marginLeft="80dp"
        android:id="@+id/speed"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    </LinearLayout>
    <ProgressBar
        android:visibility="invisible"
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/start"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="开始下载" />

        <Button
            android:layout_marginLeft="20dp"
            android:id="@+id/stop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="暂停下载" />

        <Button
            android:layout_marginLeft="20dp"
            android:id="@+id/cancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="取消下载" />
    </LinearLayout>

</LinearLayout>

 

二、 在MainActivity中初始化一些组件,绑定按钮的事件:

在onCreate方法中初始化一些组件:

// 初始化组件
textView = (TextView) findViewById(R.id.textView);
progressView = (TextView) findViewById(R.id.progress);
speedView = (TextView) findViewById(R.id.speed);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
progressBar.setMax(100);
startButton = (Button) findViewById(R.id.start);
stopButton = (Button) findViewById(R.id.stop);
cancelButton = (Button) findViewById(R.id.cancel);
// 创建一个文件信息对象
final FileInfo fileInfo = new FileInfo(0, fileUrl, "huhx.apk", 0, 0);

在onCreate方法中绑定开始下载按钮事件:点击start按钮,设置进度条可见,并且设置start的Action,启动服务。

startButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        textView.setText(fileInfo.getFileName());
        progressBar.setVisibility(View.VISIBLE);
        // 通过Intent传递参数给service
        Intent intent = new Intent(MainActivity.this, DownloadService.class);
        intent.setAction(DownloadService.ACTION_START);
        intent.putExtra("fileInfo", fileInfo);
        startService(intent);
    }
});

在onCreate方法中绑定暂停下载按钮事件:点击stop按钮,设置stop的Action,启动服务。

stopButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 通过Intent传递参数给service
        Intent intent = new Intent(MainActivity.this, DownloadService.class);
        intent.setAction(DownloadService.ACTION_STOP);
        intent.putExtra("fileInfo", fileInfo);
        startService(intent);
    }
});

在onCreate方法中绑定取消下载按钮事件:点击cancel按钮,设置cancel的Action,启动服务,之后更新UI。

cancelButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 通过Intent传递参数给service
        Intent intent = new Intent(MainActivity.this, DownloadService.class);
        intent.setAction(DownloadService.ACTION_CANCEL);
        intent.putExtra("fileInfo", fileInfo);
        startService(intent);

        // 更新textView和progressBar的显示UI
        textView.setText("");
        progressBar.setVisibility(View.INVISIBLE);
        progressView.setText("");
        speedView.setText("");
    }
});

注册广播,用于Service向Activity传递一些下载进度信息:

// 静态注册广播
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadService.ACTION_UPDATE);
registerReceiver(broadcastReceiver, intentFilter);

/**
 * 更新UI
 */
BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) {
            int finished = intent.getIntExtra("finished", 0);
            int speed = intent.getIntExtra("speed", 0);

            Log.i("Main", finished + "");
            progressBar.setProgress(finished);
            progressView.setText(finished + "%");
            speedView.setText(speed + "KB/s");
        }
    }
};

 

三、 在AndroidManifest.xm文件中声明权限,定义服务

<service android:name="com.huhx.services.DownloadService" android:exported="true" />

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

 

android断点续传的工具类

二、 我们定义一些实体类,用于断点续传过程的信息的良好封装:

下载文件信息: 省略了get和set方法,以及toString和构造方法

public class FileInfo implements Serializable{
    // 文件Id,用于标识文件
    private int fileId;
    // 文件的下载地址
    private String url;
    // 文件的名称
    private String fileName;
    // 文件的长度,也就是大小
    private int length;
    // 文件已经的下载量
    private int finished;

}

下载资源的线程信息:省略同上

public class ThreadInfo {
    // 线程ID
    private int threadId;
    // 下载资源的地址
    private String url;
    //下载资源的开始处
    private int start;
    //下载资源的结束处
    private int end;
    //资源已经的下载量
    private int finished;
}

 

三、 我们开始数据库方面的编写,它用于存储更新线程的下载的进度信息

首先我们要创建一个数据库的工具类:

package com.huhx.util;

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

/**
 * Created by huhx on 2016/4/9.
 */
public class SqliteDBHelper extends SQLiteOpenHelper {
    private static final String DB_NAME = "download.db";
    private static final int version = 1;

    private static final String CREATE_THREADINFO = "create table thread_info(_id integer primary key autoincrement, " +
            "thread_id integer, url text, start integer, end integer, finished integer)";
    private static final String DROP_THREADINFO = "drop table if exists thread_info";

    public SqliteDBHelper(Context context) {
        super(context, DB_NAME, null, version);
    }

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

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL(DROP_THREADINFO);
        db.execSQL(CREATE_THREADINFO);
    }
}

 定义一个Dao接口,用于数据库对线程信息的CRUD操作:

/**
 * Created by Linux on 2016/4/9.
 */
public interface ThreadDao {
    // 插入线程信息
    public void insertThread(ThreadInfo threadInfo);

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

    // 删除所有关于这个url的线程
    public void deleteThread(String url);

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

    // 查询线程信息
    public List<ThreadInfo> queryThread(String url);

    // 线程信息是否存在
    public boolean isThreadInfoExist(String url, int threadId);
}

具体实现上述Dao的Impl类:

package com.huhx.util;

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

import com.huhx.model.ThreadInfo;

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

/**
 * Created by huhx on 2016/4/9.
 */
public class ThreadDaoImpl implements ThreadDao {

    private SqliteDBHelper sqliteDBHelper;

    public ThreadDaoImpl(Context context) {
        sqliteDBHelper = new SqliteDBHelper(context);
    }

    @Override
    public void insertThread(ThreadInfo threadInfo) {
        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
        Object[] objects = new Object[]{
                threadInfo.getThreadId(), threadInfo.getUrl(), threadInfo.getStart(), threadInfo.getEnd(), threadInfo.getFinished()
        };
        database.execSQL("insert into thread_info(thread_id, url, start, end, finished) values(?,?,?,?,?)", objects);
        database.close();

    }

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

    @Override
    public void deleteThread(String url) {
        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
        Object[] objects = new Object[]{
                url
        };
        database.execSQL("delete from thread_info where url = ?", objects);
        database.close();
    }

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

    @Override
    public List<ThreadInfo> queryThread(String url) {
        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
        List<ThreadInfo> threadInfos = new ArrayList<>();
        Cursor cursor = database.rawQuery("select * from thread_info where url = ?", new String[]{url});
        while (cursor.moveToNext()) {
            ThreadInfo threadInfo = new ThreadInfo();
            threadInfo.setThreadId(cursor.getInt(cursor.getColumnIndex("thread_id")));
            threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));
            threadInfo.setStart(cursor.getInt(cursor.getColumnIndex("start")));
            threadInfo.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
            threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));

            threadInfos.add(threadInfo);
        }
        cursor.close();
        database.close();
        return threadInfos;
    }

    @Override
    public boolean isThreadInfoExist(String url, int threadId) {
        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
        Cursor cursor = database.rawQuery("select * from thread_info where url = ? and thread_id = ?", new String[]{url, threadId+""});

        boolean isExist = cursor.moveToNext();
        cursor.close();
        database.close();
        return isExist;
    }
}

 

下载暂停取消的具体流程

四、 最后我们开始最重要的Service以及核心的下载代码的编写,我们按照上述的开始、暂停、取消的顺序,来讲解断点续传的实现过程。

我们在DownloadService中onStartCommand方法中接收的Intent,关于Service的使用请参见:android基础---->service的生命周期

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // 获得Activity传过来的参数
    if (ACTION_START.equals(intent.getAction())) {
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
        // 启动初始化线程
        new InitThread(fileInfo).start();
    } else if (ACTION_STOP.equals(intent.getAction())) {
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
        if (downloadTask != null) {
            downloadTask.isPause = true;
        }
    } else if (ACTION_CANCEL.equals(intent.getAction())) {
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
        if (downloadTask != null) {
            downloadTask.isPause = true;
        }
        // 删除本地文件
        File file = new File(DOWNLOAD_PATH, fileInfo.getFileName());
        if (file.exists()) {
            file.delete();
        }
        handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget();
    }
    return super.onStartCommand(intent, flags, startId);
}

 

五、 文件的开始下载流程:

开始下载时,启动一个初始化线程,并把文件信息传递给线程,该线程通过Http请求得到文件的长度,在本地创建下载文件的载体,设置大小并发送下载的消息给Handler:

/**
 * 初始化子线程
 */
class InitThread extends Thread {
    private FileInfo fileInfo = null;

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

    @Override
    public void run() {
        // 连接网络文件
        HttpURLConnection connection = null;
        RandomAccessFile randomAccessFile = null;
        try {
            URL url = new URL(fileInfo.getUrl());
            connection = (HttpURLConnection) url.openConnection();
            connection.setConnectTimeout(3000);
            connection.setRequestMethod("GET");

            connection.connect();

            int length = -1;
            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                // 获取文件的长度
                length = connection.getContentLength();
            }
            if (length <= 0) {
                return;
            }
            // 在本地创建文件
            File dir = new File(DOWNLOAD_PATH);
            if (dir.exists()) {
                dir.mkdir();
            }
            File file = new File(dir, fileInfo.getFileName());
            // 设置文件长度
            randomAccessFile = new RandomAccessFile(file, "rwd");
            randomAccessFile.setLength(length);

            fileInfo.setLength(length);
            handler.obtainMessage(DOWNLOAD_MESSAGE, fileInfo).sendToTarget();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                randomAccessFile.close();
                connection.disconnect();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

handler接收消息,并加以处理:注意这里有两种消息,我们暂时只考虑DOWNLOAD_MESSAGE消息,它启动下载任务

private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case DOWNLOAD_MESSAGE:
                FileInfo fileInfo = (FileInfo) msg.obj;
                // 启动下载任务
                downloadTask = new DownloadTask(DownloadService.this, fileInfo);
                downloadTask.download();
                break;
            case DOWNLOAD_CANCEL:
                FileInfo fileCancelInfo = (FileInfo) msg.obj;
                downloadTask = new DownloadTask(DownloadService.this);
                downloadTask.cancelDownload(fileCancelInfo);
                break;
        }
    }
};

 在download方法中,首先判断是否有线程下载过文件,如果没有就创建一个。有的话,从数据库直接得到。而且开启了下载的任务线程

public void download() {
    // 读取数据库的线程信息
    List<ThreadInfo> threadInfos = threadDao.queryThread(fileInfo.getUrl());
    ThreadInfo threadInfo = null;
    if (threadInfos.size() == 0) {
        threadInfo = new ThreadInfo(0, fileInfo.getUrl(), 0, fileInfo.getLength(), 0);
    } else {
        threadInfo = threadInfos.get(0);
    }
    new DownloadThread(threadInfo).start();
}

 在下载的线程中,通过Http请求数据并通过字节流的方式存储在本地的文件中。间隔500毫秒,就发送一次更新UI的广播。如果收到了暂停的信号,就暂停下载。在下载完成之后,删除数据库中的线程信息

class DownloadThread extends Thread {
    private ThreadInfo threadInfo = null;

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

    @Override
    public void run() {
        // 向数据库插入线程信息
        if (!threadDao.isThreadInfoExist(threadInfo.getUrl(), threadInfo.getThreadId())) {
            threadDao.insertThread(threadInfo);
        }
        HttpURLConnection connection = null;
        RandomAccessFile randomAccessFile = null;
        InputStream inputStream = null;
        try {
            URL url = new URL(threadInfo.getUrl());
            connection = (HttpURLConnection) url.openConnection();
            connection.setConnectTimeout(5000);
            connection.setRequestMethod("GET");

            int start = threadInfo.getStart() + threadInfo.getFinished();
            connection.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd());

            File file = new File(DownloadService.DOWNLOAD_PATH, fileInfo.getFileName());
            randomAccessFile = new RandomAccessFile(file, "rwd");
            randomAccessFile.seek(start);
            Intent intent = new Intent(DownloadService.ACTION_UPDATE);

            // 开始下载
            finished += threadInfo.getFinished();
            if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
                inputStream = connection.getInputStream();
                byte[] buffer = new byte[4 * 1024];
                int len = -1;
                long time = System.currentTimeMillis();
                long time1;
                while ((len = inputStream.read(buffer)) != -1) {
                    randomAccessFile.write(buffer, 0, len);
                    finished += len;
                    if ((time1 = System.currentTimeMillis() - time) > 500) {
                        time = System.currentTimeMillis();
                        intent.putExtra("finished", finished * 100 / fileInfo.getLength());
                        intent.putExtra("speed", (int) (len / time1));
                        context.sendBroadcast(intent);
                    }
                    if (isPause) {
                        threadDao.updateThread(threadInfo.getUrl(), threadInfo.getThreadId(), finished);
                        return;
                    }
                }
                // 删除线程信息,再次发送广播避免上面的广播延迟
                intent.putExtra("finished", finished * 100 / fileInfo.getLength());
                context.sendBroadcast(intent);
                threadDao.deleteThread(threadInfo.getUrl(), threadInfo.getThreadId());
                Log.i("Main", "finished: " + finished + ", and file length: " + fileInfo.getLength());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                connection.disconnect();
                randomAccessFile.close();
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 

六、 文件的暂停下载流程:如果下载任务在启动,那么设置isPause为true,在上述的讲解中我们知道,此时字节流停止的传输。

if (downloadTask != null) {
    downloadTask.isPause = true;
}

 

七、 文件的取消下载流程:

暂停下载的流程,然后删除本地文件,最后发送取消下载的消息:

FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
if (downloadTask != null) {
    downloadTask.isPause = true;
}
// 删除本地文件
File file = new File(DOWNLOAD_PATH, fileInfo.getFileName());
if (file.exists()) {
    file.delete();
}
handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget();

handler处理取消下载的消息:调用DownloadTask的cancelDownload方法,并把文件信息传入

case DOWNLOAD_CANCEL:
        FileInfo fileCancelInfo = (FileInfo) msg.obj;
        downloadTask = new DownloadTask(DownloadService.this);
        downloadTask.cancelDownload(fileCancelInfo);
        break;

在cancelDownload方法中删除数据库中的线程信息:

// 取消下载任务
public void cancelDownload(FileInfo fileInfo) {
    threadDao.deleteThread(fileInfo.getUrl());
}

最后在MainActivity中更新UI:

// 更新textView和progressBar的显示UI
textView.setText("");
progressBar.setVisibility(View.INVISIBLE);
progressView.setText("");
speedView.setText("");

 

友情链接

关于android中多线程的下载,请参见我的博客: android程序---->android多线程下载(二)

 

posted @ 2016-04-10 09:19  huhx  阅读(3301)  评论(20编辑  收藏