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多線程下載(二)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM