編輯:關於Android編程
今天我們來接觸一下多線程下載,當然也包括斷點續傳,我們可以看到很多下載器,當開通會員的時候下載東西的速度就變得快了許多,這是為什麼呢?這就是跟今天講的多線程有關系了,其實就是多開了幾個線程一起下載罷了。當然真正的多線程下載要比這個復雜,要考慮很多問題。
做個不恰當的比喻:
假如我們把一個服務器上的文件看作是一個水缸裡的水的話,那麼多線程下載就相當於從水缸上打了多個小孔,然後塞進去小管道進行抽水。呵呵,也許這個比喻不夠准確。
效果:
這裡下載的是本地服務器上的文件,你們可以下載網絡上的一些文件。
先來看看多線程下載的原理吧:
通常服務器同時與多個用戶連接,用戶之間共享帶寬。如果N個用戶的優先級都相同,那麼每個用戶連接到該服務器上的實際帶寬就是服務器帶寬的N分之一。可以想象,如果用戶數目較多,則每個用戶只能占有可憐的一點帶寬,下載將會是個漫長的過程。
假設服務器的帶寬為20M/s,服務器上有很多電影資源,現在有三位同學都想要下載 小澤.avi 這部電影,現在三位同學都在下載,所以每位同學的速度應該為1/3 * 20M/s = 6.7M/s ,但是 小澤.avi 這部電影的大小有 2G左右,這時王五同學可能有點趕時間,等不及,下的這麼慢,所以他就使用他所學的多線程的知識多開了幾個線程,結果他最先下完。
這次可以看到分給每個線程的帶寬為1/5 * 20M/s = 4M/s,但是後面三個線程都是王五同學的,這時王五同學的帶寬其實為 12M/s ,沒錯,王五同學成功運用多線程知識解決了下載慢的問題。(神不知鬼不覺)
看到這裡我們可以知道,影響用戶帶寬的因素:
①服務器的帶寬
②線程數
不過筆者認為凡事適可而止,不要做得太絕了。這樣讓別的用戶怎麼辦呢(不管?不太好吧!)?
好的,那讓我們來看下具體如何實現:
要實現這個,需要解決以下幾個問題:
問題1:怎麼在一個文件裡面寫數據的時候按照指定的位置寫(因為每個線程的下載區間需要不一樣,不然數據會覆蓋,導致文件下不全)
問題2:如何去獲取要下載的文件大小(因為怕下載中途需要下載其他東西,導致本次需要下載的文件內存不足,所以需要先預留一個和要下載的文件大小一樣大的空間)
問題3:計算每個子線程的下載區間(因為每個線程的下載區間肯定不一樣,不然怎麼加快速度呢)
第一個問題的解決辦法:
借助RandomAccessFile 隨機文件訪問類的 seek(long offset)方法,這個方法可以把文件的寫入位置移動至offset。
第二個問題的解決辦法:
我們可以使用HttpURLConnection 對象的 getContentLength() 方法得到你當前請求文件的大小。
第三個問題的解決辦法:
假設下載的文件大小為10B(0-9,數組下標從0開始),線程數為3,那麼
線程0的下載區間應該是: 0—2
線程1的下載區間應該是: 3—5
線程2的下載區間應該是: 6—9
每個線程下載文件的大小 = 文件長度 / 線程數 (最後一個線程除外,因為可能不能均分)
那麼i線程的下載開始位置: i*每個線程下載文件的大小
i線程的下載結束位置: (i+1)*每個線程下載文件的大小 - 1
最後一個線程的結束位置為:文件長度 - 1
搞清楚以上問題就可以多線程下載了,接下裡就是斷點續傳了。
因為有時我們在下載下到一半的時候突然停電了,等來電時我們應該接到上次下載的地方繼續下載。如何實現呢??
我們可以把每個線程下載的進度都存在一個文件裡,等來電時我們先去檢索有沒有進度文件,如果有,說明上次下載過,但沒下完,就將次進度取出來繼續下載。不過線程下載的開始位置應該是 原來的開始位置+上次的進度,為了用戶體驗,我們應該在線程全部下載完成之後將保存的下載進度文件刪除(因為這個文件對用戶也沒什麼用)。
下面我們來理一下思路:
①請求網絡得到需要下載的文件的大小,並生成一個和原文件一樣大小的文件(先占空間)(響應碼為200)
②確定每個線程的下載區間(最後一個線程的結束位置應該單獨考慮)
③先查看有沒有進度文件,有則從上次進度開始下載,沒有則請求網絡獲取需要下載區間的數據,並生成下載進度文件以便斷點續傳。(記住請求的數據不是所有數據,而是各個線程它需要下載的那部分區間,響應碼為206)
注:不是所有的服務器都支持斷點續傳,這取決於服務器那邊。
④待各個線程全部下載完成,將進度文件刪掉。
⑤開啟線程下載
核心代碼:
布局文件activity_main.xml
Mactivity.java
package com.example.multithreaddownload;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
public class MainActivity extends Activity {
//進度條
private ProgressBar pb;
//顯示進度(百分比)
private TextView tv;
//記錄當前進度條的下載進度
private int currentProgress;
//進行下載的線程數量
public static final int THREADCOUNT = 3;
//下載完成的線程數量
public int finishedThread = 0;
//下載完成生成的文件名
public String fileName = "navicat.pdf";
//請求的文件下載地址(本地文件)
public String path = "http://192.168.1.100:8089/" + fileName;
//請求的文件下載地址(網絡文件)
// public String path =
// "thunder://QUFodHRwOi8vZGw0NS44MHMuaW06OTIwLzE2MDUv6LaF6ISRNDjlsI/ml7Zb5Zu96K+tRFZE54mIXS/otoXohJE0OOWwj+aXtlvlm73or61EVkTniYhdX2JkLm1wNFpa";
private Handler mHandler = new Handler(){
public void handleMessage(android.os.Message msg) {
if (msg.what == 0x1) {
tv.setText(pb.getProgress()*100/pb.getMax() + "%");
}
};
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
/**
* 初始化組件
*/
private void initView() {
pb = (ProgressBar) findViewById(R.id.pb);
tv = (TextView) findViewById(R.id.tv);
}
/**
* 點擊下載的事件
* @param view
*/
public void download(View view) {
new Thread() {
public void run() {
try {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url
.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(3000);
conn.setReadTimeout(8000);
//請求成功時的響應碼為200(注意響應碼為200)
if (conn.getResponseCode() == 200) {
// 拿到需要下載的文件的大小
int length = conn.getContentLength();
// 先占個位置,生成臨時文件
File file = new File(Environment.getExternalStorageDirectory(),fileName);
RandomAccessFile raf = new RandomAccessFile(file, "rwd");
raf.setLength(length);
//設置進度條的最大進度為文件的長度
pb.setMax(length);
raf.close();
//每個線程應該下載的長度(最後一個線程除外,因為不一定能夠平分)
int size = length / THREADCOUNT;
for (int i = 0; i < THREADCOUNT; i++) {
// 1.確定每個線程的下載區間
// 2.開啟對應的子線程
int startIndex = i * size; //開始位置
int endIndex = (i + 1) * size - 1; //結束位置
// 最後一個線程
if (i == THREADCOUNT - 1) {
endIndex = length - 1;
}
System.out.println("第" + (i + 1) + "個線程的下載區間為:"
+ startIndex + "-" + endIndex);
new DownloadThread(startIndex, endIndex, path, i)
.start();
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
};
}.start();
}
class DownloadThread extends Thread{
private int lastProgress;
private int startIndex,endIndex,threadId;
private String path;
public DownloadThread(int startIndex,int endIndex,String path,int threadId) {
this.startIndex = startIndex;
this.endIndex = endIndex;
this.path = path;
this.threadId = threadId;
}
@Override
public void run() {
try {
//建立進度臨時文件,其實這時還沒有創建。當往文件裡寫東西的時候才創建。
File progressFile = new File(Environment.getExternalStorageDirectory(), threadId+".txt");
//判斷臨時文件是否存在,存在表示已下載過,沒下完而已
if (progressFile.exists()) {
FileInputStream fis = new FileInputStream(progressFile);
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
//從進度臨時文件中讀取出上一次下載的總進度,然後與原本的開始位置相加,得到新的開始位置
lastProgress = Integer.parseInt(br.readLine());
startIndex += lastProgress;
//斷點續傳,更新上次下載的進度條
currentProgress += lastProgress;
pb.setProgress(currentProgress);
Message msg = Message.obtain();
msg.what = 0x1;
mHandler.sendMessage(msg);
br.close();
fis.close();
}
//真正請求數據
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(3000);
conn.setReadTimeout(8000);
//設置本次http請求所請求的數據的區間(這是需要服務器那邊支持斷點),格式需要這樣寫,不能寫錯
conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
//請求部分數據,響應碼是206(注意響應碼是206)
if (conn.getResponseCode() == 206) {
//此時流中只有1/3原數據
InputStream is = conn.getInputStream();
File file = new File(Environment.getExternalStorageDirectory(),fileName);
RandomAccessFile raf = new RandomAccessFile(file, "rwd");
//把文件的寫入位置移動至startIndex
raf.seek(startIndex);
byte[] b = new byte[1024];
int len = 0;
int total = lastProgress;
while ((len = is.read(b)) != -1) {
raf.write(b, 0, len);
total += len;
System.out.println("線程" + threadId + "下載了" + total);
//生成一個專門用來記錄下載進度的臨時文件
RandomAccessFile progressRaf = new RandomAccessFile(progressFile, "rwd");
//每次讀取流裡數據之後,同步把當前線程下載的總進度寫入進度臨時文件中
progressRaf.write((total + "").getBytes());
progressRaf.close();
//下載時更新進度條
currentProgress += len;
pb.setProgress(currentProgress);
Message msg = Message.obtain();
msg.what = 0x1;
mHandler.sendMessage(msg);
}
System.out.println("線程" + threadId + "下載完成");
raf.close();
//每完成一個線程就+1
finishedThread ++;
//等標志位等於線程數的時候就說明線程全部完成了
if (finishedThread == THREADCOUNT) {
for (int i = 0; i < finishedThread; i++) {
//將生成的進度臨時文件刪除
File f = new File(Environment.getExternalStorageDirectory(),i + ".txt");
f.delete();
}
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
對話框 Dialog什麼是對話框對話框是在當前的頁面之上彈出的小窗口, 用於顯示一些重要的提示信息, 提示用戶的輸入,確認信息,或顯示某種狀態.如 : 顯示進度條對話框,
文本和輸入 使用文字服務添加便利功能,例如復制/粘貼和拼寫檢查到您的應用程序。您也可以開發自己的文字服務,提供自定義的輸入法,字典和拼寫檢查,你可以分發給用戶為應用程序。
Android apk反編譯一步到位 Andriod的apk在編譯打包的時候將資源文件進行了二進制編譯,.class文件編譯成了.dex文件,經過了這幾步之後,我們是無法
本文實例為大家分享了Android自定義View之組合控件,仿電商app頂部欄的相關代碼,供大家參考,具體內容如下效果圖:分析:左右兩邊可以是TextView和Butto