Android實戰:簡易斷點續傳下載器實現

實踐是檢驗真理的唯一標準,對于編程來說,理解了不一定會做,所以還是敲一遍讓身體也記住它吧。
現在讓我們來做一款簡單的單線程下載器吧。
本文的DEMO示例github下載地址:
https://github.com/liaozhoubei/Download

本文還有后續多線程下載器:http://www.lxweimin.com/p/c23b0c10c919

下載原理

對于Android來說,其下載器的原理非常簡單,僅僅是I/O流的實現而已,只要了解I/O流就能夠寫得出,下面這個是一個簡單java項目的下載代碼:

    try {
        // strUrl 下載的網絡地址
        URL url = new URL(strUrl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(10 * 1000);
        conn.setRequestMethod("GET");
        int code = conn.getResponseCode();
        if (code == 200) {
            System.out.println("下載開始");
            File file = new File("e:\\" + strUrl.substring((strUrl.lastIndexOf("/") + 1)));
            long length = conn.getContentLength();
            if (length > 1024) {
                long size = length / (1024 * 1024);
                System.out.println("下載大小" + size + "mb");
            }
            InputStream is = conn.getInputStream();
            byte[] bt = new byte[1024];
            int len = 0;
            RandomAccessFile raf = new RandomAccessFile(file, "rwd");
            raf.setLength(length);
            while ((len = is.read(bt)) != -1) {
                raf.write(bt, 0, len);
            }
            System.out.println("下載完成");
            is.close();
            raf.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

代碼很簡單,用支持http協議的網絡地址進行下載,然后使用I/O流下載,或許大家不熟悉的只有RandomAccessFile這個API了,這是一個支持任意位置下載的一個API,同時它有個setLength()方法,可以直接設置RandomAccessFile文件,的長度。還有個seek()方法,可以直接設定從文件的哪個位置開始寫入文件。

RandomAccessFile是個很重要的API,對于斷點下載而言。
直接下載可以了,那么如何斷點下載呢?
所謂斷點下載,就是在停止下載文件的時候記住停止時的下載位置,等下次繼續下載的時候從這個位置繼續下載。
這個時候我們只需設置一個停止位置,然后用RandomAccessFile的seek()方法讀取這個位置就可以了。

所以這時我們要分兩步走
1、初始化下載線程,獲取文件的信息,如文件的大小等
2、開始下載文件,如果文件信息已存在,則查詢先前下載到哪一個位置。
代碼斷點續傳代碼如下:

private static void mutilDownload(String path) {
    HttpURLConnection conn = null;
    try {
        URL url = new URL(path);
        conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(10 * 1000);
        conn.setRequestMethod("GET");
        conn.setReadTimeout(5 * 1000);
        int code = conn.getResponseCode();
        if (code == HttpURLConnection.HTTP_OK) {
            File file = new File("e:\\" + path.substring(path.lastIndexOf("/") + 1));
            long filelength = conn.getContentLength();
            RandomAccessFile randomFile = new RandomAccessFile(file, "rwd");
            randomFile.setLength(filelength);
            randomFile.close();
            long endposition = filelength;
            
            new newThreadDown(path, endposition).start();
        }

    } catch (Exception e) {

    } finally {
        conn.disconnect();
    }
}

public static class newThreadDown extends Thread {
    private String urlstr;
    private long lastPostion;
    private long endposition;
    public newThreadDown(String urlstr, long endposition) {
        this.urlstr = urlstr;
        this.endposition = endposition;
    }

    @Override
    public void run() {
        HttpURLConnection conn = null;
        try {
            URL url = new URL(urlstr);
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(10 * 1000);
            conn.setRequestMethod("GET");
            conn.setReadTimeout(10 * 1000);
            long startposition = 0;
            // 創建記錄緩存文件
            File tempfile = new File("e:\\" + 1 + ".txt");
            if (tempfile.exists()) {
                InputStreamReader isr = new InputStreamReader(new FileInputStream(tempfile));
                BufferedReader br = new BufferedReader(isr);
                String lastStr = br.readLine();
                lastPostion = Integer.parseInt(lastStr);
                conn.setRequestProperty("Range", "bytes=" + lastPostion + "-" + endposition);
                br.close();
            } else {
                lastPostion = startposition;
                conn.setRequestProperty("Range", "bytes=" + lastPostion + "-" + endposition);
            }

            if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
                System.out.println(206 + "請求成功");
                InputStream is = conn.getInputStream();
                RandomAccessFile accessFile = new RandomAccessFile(new File("e:\\" + path.substring(path.lastIndexOf("/") + 1)),
                        "rwd");
                accessFile.seek(lastPostion);
                System.out.println("開始位置" + lastPostion);
                byte[] bt = new byte[1024 * 200];
                int len = 0;
                long total = 0;
                while ((len = is.read(bt)) != -1) {
                    total += len;
                    accessFile.write(bt, 0, len);
                    long currentposition = startposition + total;
                    File cachefile = new File("e:\\" + 1 + ".txt");
                    RandomAccessFile rf = new RandomAccessFile(cachefile, "rwd");
                    rf.write(String.valueOf(currentposition).getBytes());
                    rf.close();
                }
                System.out.println("下載完畢");
                is.close();
                accessFile.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        super.run();
    }
}

這些都是java項目,可以在eclipse中直接運行測試。
下載的原理已經梳理清楚了,剩下的只要把下載程序移植到Android項目中去就好了。

Android下載器實現

作為一個實戰項目,我們要盡可能的完善,盡可能的使用Android中的控件,所以我們不做自己將上面的代碼復制到項目中,然后用ProgressBar更新UI的事情,我們要盡可能的復制!

我們的口號是:不做簡單活!

知識要點

  • Android四大組件之Service
  • Android四大組件之Broadcast
  • 數據存儲SQLiteDatabase
    現在讓我們開始完善這個單線程的下載器吧

下載器的布局

做一個簡單的界面,我們用到開始下載按鍵,停止下載按鍵,一個Progressbar,以及一個TextView顯示文件名。
如此簡單的布局就不寫代碼了,詳情可以下載我的Github項目研究。

封裝實體對象

在本項目中有個兩個實體類對象,即FileInfo類和ThreadInfo類,他們之中的變量都擁有get和set方法,FileInfo類需要實現序列化,詳細代碼請查看項目地址
FileInfo類代碼(略):

public class FileInfo implements Serializable {
private int id;
private String url;
private String fileName;
private int length;
private int finished;

public FileInfo() {
    super();
}
/**
 * 
 * @param id   文件的ID
 * @param url  文件的下載地址
 * @param fileName  文件的名字
 * @param length  文件的總大小
 * @param finished  文件已經完成了多少
 */
public FileInfo(int id, String url, String fileName, int length, int finished) {
    super();
    this.id = id;
    this.url = url;
    this.fileName = fileName;
    this.length = length;
    this.finished = finished;
}

public int getId() {
    return id;
}

public void setId(int id) {
    this.id = id;
}
}

ThreadInfo類代碼(略):

public class ThreadInfo {
private int id;
private String url;
private int start;
private int end;
private int finished;

public ThreadInfo() {
    super();
}
/**
 * @param id 綫程的ID
 * @param url 下載文件的網絡地址
 * @param start 綫程下載的開始位置
 * @param end 綫程下載的結束位置
 * @param finished  綫程已經下載到哪個位置
 */
public ThreadInfo(int id, String url, int start, int end, int finished) {
    super();
    this.id = id;
    this.url = url;
    this.start = start;
    this.end = end;
    this.finished = finished;
}

public int getId() {
    return id;
}

public void setId(int id) {
    this.id = id;
}

創建數據庫

使用SQLiteDatabase,我們首先要實現一個數據庫的幫助類,然后創建一個操作數據庫的接口類,最后實現這個接口的數據庫操作類。

使用數據庫是用于保存ThreadInfo對象的信息,并且實時更新下載進度,但需要斷點續傳的時候從數據庫中取出保存的信息,繼續下載。

這里提示一下,保存斷點信息可以不使用數據庫,試用SharedPreference也是可以起到同樣的作用,具體方法請讀著自己摸索。
數據庫幫助類代碼如下:

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, end 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 oldVersion, int newVersion) {
    db.execSQL(SQL_DROP);
    db.execSQL(SQL_CREATE);
}
}

操作數據庫的接口類代碼:

public interface ThreadDAO {
// 插入綫程
public void insertThread(ThreadInfo info);
// 刪除綫程
public void deleteThread(String url, int thread_id);
// 更新綫程
public void updateThread(String url, int thread_id, int finished);
// 查詢綫程
public List<ThreadInfo> queryThreads(String url);
// 判斷綫程是否存在
public boolean isExists(String url, int threadId);
}

實現接口的數據庫工具類:

public class ThreadDAOImple implements ThreadDAO {
private DBHelper dbHelper = null;

public ThreadDAOImple(Context context) {
    super();
    this.dbHelper = new DBHelper(context);
}

// 插入綫程
@Override
public void insertThread(ThreadInfo info) {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    ContentValues values = new ContentValues();
    values.put("thread_id", info.getId());
    values.put("url", info.getUrl());
    values.put("start", info.getStart());
    values.put("end", info.getEnd());
    values.put("finished", info.getFinished());
    db.insert("thread_info", null, values);
    
    db.close();
}

// 刪除綫程
@Override
public void deleteThread(String url, int thread_id) {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    db.delete("thread_info", "url = ? and thread_id = ?", new String[] { url, thread_id + "" });
    
    db.close();

}

// 更新綫程
@Override
public void updateThread(String url, int thread_id, int finished) {
    SQLiteDatabase db = dbHelper.getReadableDatabase();

    db.execSQL("update thread_info set finished = ? where url = ? and thread_id = ?",
            new Object[]{finished, url, thread_id});
    db.close();
}

// 查詢綫程
@Override
public List<ThreadInfo> queryThreads(String url) {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    
    List<ThreadInfo> list = new ArrayList<ThreadInfo>();
    
    Cursor cursor = db.query("thread_info", null, "url = ?", new String[] { url }, null, null, null);
    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.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
        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 = dbHelper.getReadableDatabase();
    Cursor cursor = db.query("thread_info", null, "url = ? and thread_id = ?", new String[] { url, thread_id + "" },
            null, null, null);
    boolean exists = cursor.moveToNext();
    
    db.close();
    db.close();
    return exists;
}
}

從Activity中向Service傳參

很好,前期的準備工作已經做好了,需要從Activity中啟動線程,并且將Activity中獲得的關于下載文件的信息傳遞到Service中去,我們只需要用Intent便可以將FileInfo對象傳遞過去。在這里要注意的是如果FileInfo沒有序列化,繼承Serializable接口,那么Intent無法將FileInfo對象傳送出去。

首先創建一個DownloadService服務類,繼承自Service,定義ACITON_START和ACTION_STOP兩個常量,重新onStartCommand方法,代碼如下:

public class DownloadService extends Service {

public static final String ACTION_START = "ACTION_START";
public static final String ACTION_STOP = "ACTION_STOP";

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

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // 獲得Activity穿來的參數
    if (ACTION_START.equals(intent.getAction())) {
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
        Log.i("test", "START" + fileInfo.toString());       
    } else if (ACTION_STOP.equals(intent.getAction())) {
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");         
    }
    return super.onStartCommand(intent, flags, startId);
}

}
然后修改MainActivity中的代碼:
定義Intent常量
定義FileInfo常量
在onCreate方法中初始化兩個常量:

fileInfo = new FileInfo(0, urlstr, getfileName(urlstr), 0, 0);
intent = new Intent(MainActivity.this, DownloadService.class);

設置點擊事件:

@Override
public void onClick(View v) {
    switch (v.getId()) {
    case R.id.start_button:
        // 開啟服務
        fileName.setText(getfileName(urlstr));
        intent.setAction(DownloadService.ACTION_START);
        intent.putExtra("fileInfo", fileInfo);
        startService(intent);

        break;
    case R.id.stop_button:
        intent.setAction(DownloadService.ACTION_STOP);
        intent.putExtra("fileInfo", fileInfo);
        startService(intent);
        break;
    }
}

相親我們已經可以啟動服務了,點擊按鍵啟動服務之后,就能調用DownloadService中的onStartCommand方法,接收到從MainActivity傳過來的fileInfo對象。

從DownloadService中初始化線程

在剛才從MainActivity傳過來的fileInfo對象中只有下載的URL地址以及文件名,但是我們還不知道文件的長度,也沒有設定好文件的保存位置等信息,初始化線程就是為了配置好這些信息。

從初始化線程中配置好fileInfo對象之后,需要將它傳遞給Handler,然后在Handler啟動真正的下載任務,
Handler代碼如下:

// 從InitThread綫程中獲取FileInfo信息,然後開始下載任務
Handler mHandler = new Handler() {
    public void handleMessage(android.os.Message msg) {
        switch (msg.what) {
        case MSG_INIT:
            FileInfo fileInfo = (FileInfo) msg.obj;
            Log.i("test", "INIT:" + fileInfo.toString());
            // 獲取FileInfo對象,開始下載任務
            mTask = new DownloadTask(DownloadService.this, fileInfo);
            mTask.download();
            break;
        }
    };
};

InitThread內部類在完成初始化線程之后,將fileInfo傳遞給Handler,代碼如下:

// 初始化下載綫程,獲得下載文件的信息
class InitThread extends Thread {
    private FileInfo mFileInfo = null;

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

    @Override
    public void run() {
        HttpURLConnection conn = null;
        RandomAccessFile raf = null;
        try {
            URL url = new URL(mFileInfo.getUrl());
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5 * 1000);
            conn.setRequestMethod("GET");
            int code = conn.getResponseCode();
            int length = -1;
            if (code == HttpURLConnection.HTTP_OK) {
                length = conn.getContentLength();
            }
            //如果文件長度為小于0,表示獲取文件失敗,直接返回
            if (length <= 0) {
                return;
            }
            // 判斷文件路徑是否存在,不存在這創建
            File dir = new File(DownloadPath);
            if (!dir.exists()) {
                dir.mkdir();
            }
            // 創建本地文件
            File file = new File(dir, mFileInfo.getFileName());
            raf = new RandomAccessFile(file, "rwd");
            raf.setLength(length);
            // 設置文件長度
            mFileInfo.setLength(length);
            // 將FileInfo對象傳遞給Handler
            Message msg = Message.obtain();
            msg.obj = mFileInfo;
            msg.what = MSG_INIT;
            mHandler.sendMessage(msg);
            msg.setTarget(mHandler);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            conn.disconnect();
            try {
                raf.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        super.run();
    }
}

然后修改onStartConnand方法,在點擊開啟服務的時候初始化線程

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // 獲得Activity穿來的參數
    if (ACTION_START.equals(intent.getAction())) {
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
        Log.i("test", "START" + fileInfo.toString());
        new InitThread(fileInfo).start();
    } else if (ACTION_STOP.equals(intent.getAction())) {
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
        Log.i("test", "STOP" + fileInfo.toString());
    }
    return super.onStartCommand(intent, flags, startId);
}

開啟下載任務

終于輪到真正的下載任務了,這就是最后一步了。
在之前的代碼中,即使用Handler接收InitThread中傳遞過來的fileInfo對象時,有一段代碼還沒有實現,這段代碼是正在的下載邏輯:

            // 獲取FileInfo對象,開始下載任務
            mTask = new DownloadTask(DownloadService.this, fileInfo);
            mTask.download();

現在我們開始實現DownloadTask下載任務這個類吧。
DownloadTask類有以下成員變量:

private Context mComtext = null;
private FileInfo mFileInfo = null;
private ThreadDAO mDao = null;
private int mFinished = 0;
public boolean mIsPause = false;

mComtext就不做介紹了,mFileInfo是封裝了下載文件的信息對象;
mDAO是對數據庫進行操作的工具類,它將會引用實現了它的接口的ThreadDAOImple類。
mFinished用于臨時存儲文件下載的進度。
mIsPause則用于判斷文件是否在下載狀態又或者停止狀態。
設定好成員變量,在創建DownloadTask的構造函數,將成員變量初始化

public DownloadTask(Context comtext, FileInfo fileInfo) {
    super();
    this.mComtext = comtext;
    this.mFileInfo = fileInfo;
    this.mDao = new ThreadDAOImple(mComtext);
}

下面開始的便是下載線程的代碼實現,將之前的代碼原理搬過來,改一改就好了,這里還是展示給大家看吧,代碼如下:

class DownloadThread extends Thread {
    private ThreadInfo threadInfo = null;

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

    @Override
    public void run() {
        // 如果數據庫不存在下載信息,添加下載信息
        if (!mDao.isExists(threadInfo.getUrl(), threadInfo.getId())) {
            mDao.insertThread(threadInfo);
        }
        HttpURLConnection conn = null;
        RandomAccessFile raf = null;
        InputStream is = null;
        try {
            URL url = new URL(mFileInfo.getUrl());
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5 * 1000);
            conn.setRequestMethod("GET");
            

            int start = threadInfo.getStart() + threadInfo.getFinished();
            // 設置下載文件開始到結束的位置
            conn.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd());
            File file = new File(DownloadService.DownloadPath, mFileInfo.getFileName());
            raf = new RandomAccessFile(file, "rwd");
            raf.seek(start);
            mFinished += threadInfo.getFinished();              
            int code = conn.getResponseCode();
            if (code == HttpURLConnection.HTTP_PARTIAL) {
                is = conn.getInputStream();
                byte[] bt = new byte[1024];
                int len = -1;
                // 定義UI刷新時間
                long time = System.currentTimeMillis();
                while ((len = is.read(bt)) != -1) {
                    raf.write(bt, 0, len);
                    mFinished += len;
                    // 設置爲500毫米更新一次
                    if (System.currentTimeMillis() - time > 500) {
                        time = System.currentTimeMillis();
                        
                        Intent intent = new Intent(DownloadService.ACTION_UPDATE);
                        intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());
                        Log.i("test", mFinished * 100 / mFileInfo.getLength() + "");
                        // 發送廣播給Activity
                        mComtext.sendBroadcast(intent);
                    }
                    if (mIsPause) {
                        mDao.updateThread(threadInfo.getUrl(), threadInfo.getId(), mFinished);
                        return;
                    }
                }
            }
            // 下載完成后,刪除數據庫信息
            mDao.deleteThread(threadInfo.getUrl(), threadInfo.getId());

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            conn.disconnect();
            try {
                is.close();
                raf.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        super.run();
    }
}

在這段代碼中我們還要在下載的時候發送廣播給Activity,用于更新Progressbar的進度,但更新不易過頻,以免影響UI效果,所以設置為每500毫米更新一下,根據系統時間設定。

我們在下載邏輯中,還要判斷當前下載的文件是否在數據庫中存在,如果不存在就添加,如果存在就要從數據庫中獲取當前下載位置,然后繼續下載,所以增加以下方法:

public void download(){
    // 從數據庫中獲取到下載的信息
    List<ThreadInfo> list = mDao.queryThreads(mFileInfo.getUrl());
    ThreadInfo info = null;
    if (list.size() == 0) {
        info = new ThreadInfo(0, mFileInfo.getUrl(), 0, mFileInfo.getLength(), 0);
    }else{
        info= list.get(0);           
    }
    new DownloadThread(info).start();
}

好了,整個的下載任務類已經完成了,下面我們繼續完善我們的代碼吧。

完善Service和MainActivity代碼

在之前,我們雖然把初始化下載線程InitThread寫好了,然后通過初始化線程獲取FileInfo對象,將其傳遞給Handler,在Handler中開啟真正的下載任務。但是當時并沒有調用這個InitThread類,現在再次修改DownloadService中的onStartCommand方法來啟動InitThread任務吧

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // 獲得Activity穿來的參數
    if (ACTION_START.equals(intent.getAction())) {
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
        Log.i("test", "START" + fileInfo.toString());
        new InitThread(fileInfo).start();
    } else if (ACTION_STOP.equals(intent.getAction())) {
        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
        Log.i("test", "STOP" + fileInfo.toString());
        if (mTask != null) {
            mTask.mIsPause = true;
        }
    }
    return super.onStartCommand(intent, flags, startId);
}

我們通過MainActivity的點擊事件開啟服務,如果Intent中傳過來的值為ACTION_START的時候,開啟初始化線程,獲得FileInfo對象,然后將其傳遞給Handler啟動下載任務。

如果MainActivity傳遞過來的值為ACTION_STOP,就判斷當前是否有下載任務,如果有下載任務,就將DownloadTask中的成員變量mIsPause設置為true,這時就更新數據庫中的下載進度了。

然后我們在修改MainActivity中代碼,添加一個廣播接收者的內部類,它接收從DownloadTask中傳過來的廣播--下載進度,然后實時更新ProgressBar。代碼如下:

// 從DownloadTadk中獲取廣播信息,更新進度條
class UIRecive extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) {
            int finished = intent.getIntExtra("finished", 0);
            downloadProgress.setProgress(finished);
            
        }
    }
}

這是一個自己手動撰寫的廣播,因此需要動態注冊,在MainActivity中的創建一個成員變量廣播接收者對象mRecive,在onCreate方法注冊廣播接收者:

// 從DownloadTadk中獲取廣播信息,更新進度條
class UIRecive extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) {
            int finished = intent.getIntExtra("finished", 0);
            downloadProgress.setProgress(finished);             
        }
    }
}

最后不要忘記在Activity的onDestry方法中注銷廣播!
最后的最后就是千萬記得在Androidmainfest中獲取網絡、存儲卡讀取/寫入權限等等。

總結

這是一個單線程的斷點續傳下載,也僅僅是一個下載DEMO,但是它包含了Android中各種組件的混合使用,對于Android新手全面的了解Android項目很有好處。

但是要注意的是這個項目仍然有許多BUG等著大家自己去修復,如在下載的時候再次點擊下載,你會發現又多了一個新的下載線程,導致進度條跳來跳去。解決這個問題也簡單,只需要在開啟之前判斷一下是否已經有這個文件了,如果有就直接跳。

其次還有個bug,那就是在本項目中存儲進度的數據類型是int類型,如果你說下載的文件過大,如超過30M的時候,你會發現你的進度條下載到一半就消失了。這是因為下載數據超過int的數據范圍,導致內存泄漏。這個問題只需要將數據類型修改為long類型就好了。

但是,不管怎么說這是一個很好的練習項目。
最后做一下自來水,這個項目在慕課網中有教程,哈哈。

如果有哪位大神看到本文有什么錯誤之處,還請不吝賜教~~

本文的DEMO示例github下載地址:
https://github.com/liaozhoubei/Download

本文后續多線程下載器:http://www.lxweimin.com/p/c23b0c10c919

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容