多線程下載就是將同一個網路上的原始文件根據線程個數分成均等份,然後每個單獨的線程下載對應的一部分 ...
首先來看一下多線程下載的原理。多線程下載就是將同一個網路上的原始文件根據線程個數分成均等份,然後每個單獨的線程下載對應的一部分,然後再將下載好的文件按照原始文件的順序“拼接”起來就構
成了完整的文件了。這樣就大大提高了文件的下載效率。對於文件下載來說,多線程下載是必須要考慮的環節。
多線程下載大致可分為以下幾個步驟:
一.獲取伺服器上的目標文件的大小
顯然這一步是需要先訪問一下網路,只需要獲取到目標文件的總大小即可。目的是為了計算每個線程應該分配的下載任務。
二. 在本地創建一個跟原始文件同樣大小的文件
在本地可以通過RandomAccessFile 創建一個跟目標文件同樣大小的文件,該api 支持文件任意位置的讀寫操作。這樣就給多線程下載提供了方便,每個線程只需在指定起始和結束腳標範圍內寫數據即可。
三.計算每個線程下載的起始位置和結束位置
我們可以把原始文件當成一個位元組數組,每個線程只下載該“數組”的指定起始位置和指定結束位置之間的部分。在第一步中我們已經知道了“數組”的總長度。因此只要再知道總共開啟的線程的個數就好計算每個線程要下載的範圍了。每個線程需要下載的位元組個數(blockSize)=總位元組數(totalSize)/線程數(threadCount)。 假設給線程按照0,1,2,3...n 的方式依次進行編號,那麼第n 個線程下載文件的範圍為:
起始腳標startIndex=n*blockSize。
結束腳標endIndex=(n-1)*blockSize-1。
考慮到totalSize/threadCount 不一定能整除,因此對已最後一個線程應該特殊處理,最後一個線程的起始腳標計算公式不變,但是結束腳標為endIndex=totalSize-1即可。
四.開啟多個子線程開始下載
在子線程中實現讀流操作,將conn.getInputStream()讀到RandomAccessFile中。
五.記錄下載進度
為每一個單獨的線程創建一個臨時文件,用於記錄該線程下載的進度。對於單獨的一個線程,每下載一部分數據就在本地文件中記錄下當前下載的位元組數。這樣子如果下載任務異常終止了,那麼下次重新開始下載時就可以接著上次的進度下載。
六. 刪除臨時文件
當多個線程都下載完成之後,最後一個下載完的線程將所有的臨時文件刪除。
Android有界面可以跟用戶進行良好的交互,在界面上讓用戶輸入原文件地址、線程個數,然後點擊確定開始下載。為了讓用戶可以清晰的看到每個線程下載的進度根據線程個數動態的生成等量的進度條(ProgressBar)。ProgressBar 是一個進度條控制項,用於顯示一項任務的完成進度。其有兩種樣式,一種是圓形的,該種樣式是系統預設的,由於無法顯示具體的進度值,適合用於不確定要等待多久的情形下;另一種是長條形的,此類進度條有兩種顏色,高亮顏色代表任務完成的總進度。對於我們下載任務來說,由於總任務(要下載的位元組數)是明確的,當前已經完成的任務(已經下載的位元組數)也是明確的,因此特別適合使用後者。由於在我們的需求里ProgressBar 是需要根據線程的個數動態添加的,而且要求是長條形的。因此可以事先在佈局文件中編寫好ProgressBar 的樣式。當需要用到的時候再將該佈局填充起來。ProgressBar 的max 屬性代表其最大刻度值,progress 屬性代表當前進度值。使用方法如下:
ProgressBar.setMax(int max);設置最大刻度值。
ProgressBar.setProgress(int progress);設置當前進度值。
給ProgressBar 設置最大刻度值和修改進度值可以在子線程中操作的,其內部已經特殊處理過了,因此不需要再通過handler發送Message 讓主線程修改進度。
下麵就給出我們自己寫的安卓環境下的多線程。
多線程下載界面佈局如下,三個進度條分別表示三個子線程的下載進度。
<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=".MainActivity" > <EditText android:id="@+id/et_path" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="請輸入要下載的文件資源路徑" android:text="http://192.168.1.104:8080/gg.exe" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="download" android:text="下載" /> <ProgressBar android:id="@+id/pb0" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" /> <ProgressBar android:id="@+id/pb1" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" /> <ProgressBar android:id="@+id/pb2" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>
多線程下載的內部邏輯如下,其實這在開頭已經了,只不過是代碼的實現了。
public class MainActivity extends Activity { private EditText et_path; private ProgressBar pb0; private ProgressBar pb1; private ProgressBar pb2; /** * 開啟幾個線程從伺服器下載數據 */ public static int threadCount = 3; public static int runningThreadCount; private String path; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化控制項 et_path = (EditText) findViewById(R.id.et_path); pb0 = (ProgressBar) findViewById(R.id.pb0); pb1 = (ProgressBar) findViewById(R.id.pb1); pb2 = (ProgressBar) findViewById(R.id.pb2); } //下載按鈕的點擊事件 public void download(View view) { path = et_path.getText().toString().trim(); if (TextUtils.isEmpty(path) || (!path.startsWith("http://"))) { Toast.makeText(this, "對不起路徑不合法", 0).show(); return; } new Thread(){ public void run() { try { //1.獲取伺服器上的目標文件的大小 URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); conn.setRequestMethod("GET"); int code = conn.getResponseCode(); if (code == 200) { int length = conn.getContentLength(); System.out.println("伺服器文件的長度為:" + length); //2.在本地創建一個跟原始文件同樣大小的文件 RandomAccessFile raf = new RandomAccessFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+getFileName(path), "rw"); raf.setLength(length); raf.close(); //3.計算每個線程下載的起始位置和結束位置 int blocksize = length / threadCount; runningThreadCount = threadCount; for (int threadId = 0; threadId < threadCount; threadId++) { int startIndex = threadId * blocksize; int endIndex = (threadId + 1) * blocksize - 1; if (threadId == (threadCount - 1)) { endIndex = length - 1; } //4.開啟多個子線程開始下載 new DownloadThread(threadId, startIndex, endIndex).start(); } } } catch (Exception e) { e.printStackTrace(); } }; }.start(); } private class DownloadThread extends Thread { /** * 線程id */ private int threadId; /** * 線程下載的理論開始位置 */ private int startIndex; /** * 線程下載的結束位置 */ private int endIndex; /** * 當前線程下載到文件的那一個位置了. */ private int currentPosition; public DownloadThread(int threadId, int startIndex, int endIndex) { this.threadId = threadId; this.startIndex = startIndex; this.endIndex = endIndex; System.out.println(threadId + "號線程下載的範圍為:" + startIndex + " ~~ " + endIndex); currentPosition = startIndex; } @Override public void run() { try { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //檢查當前線程是否已經下載過一部分的數據了 File info = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position"); RandomAccessFile raf = new RandomAccessFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+getFileName(path), "rw"); if(info.exists()&&info.length()>0){ FileInputStream fis = new FileInputStream(info); BufferedReader br = new BufferedReader(new InputStreamReader(fis)); currentPosition = Integer.valueOf(br.readLine()); conn.setRequestProperty("Range", "bytes="+currentPosition+"-"+endIndex); System.out.println("原來有下載進度,從上一次終止的位置繼續下載"+"bytes="+currentPosition+"-"+endIndex); fis.close(); raf.seek(currentPosition);//每個線程寫文件的開始位置都是不一樣的. }else{ //告訴伺服器 只想下載資源的一部分 conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex); System.out.println("原來沒有有下載進度,新的下載"+ "bytes="+startIndex+"-"+endIndex); raf.seek(startIndex);//每個線程寫文件的開始位置都是不一樣的. } InputStream is = conn.getInputStream(); byte[] buffer = new byte[1024]; int len = -1; while((len = is.read(buffer))!=-1){ //把每個線程下載的數據放在自己的空間裡面. // System.out.println("線程:"+threadId+"正在下載:"+new String(buffer)); raf.write(buffer,0, len); //5.記錄下載進度 currentPosition+=len; File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position"); RandomAccessFile fos = new RandomAccessFile(file,"rwd"); //System.out.println("線程:"+threadId+"寫到了"+currentPosition); fos.write(String.valueOf(currentPosition).getBytes()); fos.close();//fileoutstream數據是不一定被寫入到底層設備裡面的,有可能是存儲在緩存裡面. //raf 的 rwd模式,數據是立刻被存儲到底層硬碟設備裡面. //更新進度條的顯示 int max = endIndex - startIndex; int progress = currentPosition - startIndex; if(threadId==0){ pb0.setMax(max); pb0.setProgress(progress); }else if(threadId==1){ pb1.setMax(max); pb1.setProgress(progress); }else if(threadId==2){ pb2.setMax(max); pb2.setProgress(progress); } } raf.close(); is.close(); System.out.println("線程:"+threadId+"下載完畢了..."); File f = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position"); f.renameTo(new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position.finish")); synchronized (MainActivity.class) { runningThreadCount--; //6.刪除臨時文件 if(runningThreadCount<=0){ for(int i=0;i<threadCount;i++){ File ft = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+i+".position.finish"); ft.delete(); } } } } catch (Exception e) { e.printStackTrace(); } } } /** * 獲取一個文件名稱 * @param path * @return */ public String getFileName(String path){ int start = path.lastIndexOf("/")+1; return path.substring(start); } }
最後別忘了添加許可權,在該工程中不僅用到了網路訪問還用到了sdcard 存儲,因此需要添加兩個許可權。
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
另外,xUtils同樣可以實現多線程下載。xUtils 是開源免費的Android 工具包,代碼托管在github 上。目前xUtils 主要有四大模塊:DbUtils 模塊,主要用於操作資料庫的框架。ViewUtils 模塊,通過註解的方式可以對UI,資源和事件綁定進行管理。HttpUtils 模塊,提供了方便的網路訪問,斷點續傳等功能。BitmapUtils 模塊,提供了強大的圖片處理工具。我們在這裡只簡單實用xUtils 工具中的HttpUtils 工具。第三方包的使用較為簡單,直接拷貝xUtils的jar包到libs目錄下,然後添加依賴。
接下來就可以使用xUtils中的httpUtils的功能了:
HttpUtils http = new HttpUtils(); /** * 參數1:原文件網路地址 * 參數2:本地保存的地址 * 參數3:是否支持斷點續傳,true:支持,false:不支持 * 參數4:回調介面,該介面中的方法都是在主線程中被調用的, * 也就是該介面中的方法都可以修改UI */ http.download(path, "/mnt/sdcard/xxx.exe", true, new RequestCallBack<File>() { //下載成功後調用一次 @Override public void onSuccess(ResponseInfo<File> arg0) { Toast.makeText(MainActivity.this, "下載成功", 0).show(); } /** * 每下載一部分就被調用一次,通過該方法可以知道當前下載進度 * 參數1:原文件總位元組數 * 參數2:當前已經下載好的位元組數 * 參數3:是否在上傳,對於下載,該值為false */ @Override public void onLoading(long total, long current, boolean isUploading) { pb0.setMax((int) total); pb0.setProgress((int) current); super.onLoading(total, current, isUploading); } //失敗後調用一次 @Override public void onFailure(HttpException arg0, String arg1) { Toast.makeText(MainActivity.this, "下載失敗"+arg1, 0).show(); } });