編輯:關於Android編程
為什麼要使用多線程下載呢?
究其原因就一個字:"快",使用多線程下載的速度遠比單線程的下載速度要快,說到下載速度,決定下載速度的因素一般有兩個:
一個是客戶端實際的網速,另一個則是服務端的帶寬。我們經常使用的是單線程下載,也就是下載一個文件就是開啟一個線程去請求下載資源。
這裡我們不考慮客戶端實際網速因素,因為這個因素多變,不好控制。我們主要考慮的因素就是服務端的帶寬。那麼服務端是如何給每個客戶端分配
它的下載帶寬的呢???它分配的原理大致是這樣的,服務端只會給請求它的每個線程平均分配相應的下載帶寬,這也就說明每個線程享受
服務端CPU的時間片大小是一樣的,當其中一個線程的下載時間片完了,盡管此時它的資源沒有下載完畢,它也不得不讓出CPU資源給下一個線程使用
這實際上也就是操作系統中的時間片輪轉算法的原理,所以它的下載帶寬分配單位是“線程”,這也就是為什麼有時候我們下載資源的過程中速度忽快忽慢的原因。
說到這裡,相信你大致明白了為什麼多線程要比單線程的下載速度要快了吧。如果不明白,也沒有關系,下面我將會通過兩張示意圖和一個例子來說明
它的原因。
我們假如有下面這樣的一個情景:小明、小華、小強,他們都想看同一部電影XXX.avi,所以就同時到同一個服務器上下載XXX.avi.開始的時候三個人都很老實
都只開著一個線程去請求下載資源,這時候服務端帶寬假設是30M,那麼按照我們上面講的分配原理,三個人應該平均分的10M的下載速度。
但是小明就實在忍不住,他再也無法忍受這麼慢的下載速度,所以他就開始請教高手,經過高手的指點,終於練就多線程的本領,於是乎他就開掛了,
使出了多線程下載的絕招了,頓時其他兩個人就懵逼了。他是怎麼做的呢?就在其他兩個人在下載的時候,他卻默默地多開了兩個線程,也就是總共開
了三個線程去下載同一部電影,此時服務器就會發現,"咦,這裡又多了兩個線程,那麼也給他們分點吧"。而此時服務器端卻不知,其中三個線程是來自
於同一個用戶(注意:服務端下載帶寬分配單位是"線程"而不是“用戶”)。那麼服務器就會把原來30M帶寬,現在總共有5個線程,再次重新平均分配
最終分得每個線程只有6M,那麼小明就很開心,因為他開了3個線程,所以他下載這部電影的速度是18M,而其他兩個人則由原來的10M變為現在的6M,
估計其他兩個人得砸電腦了,尼瑪越下越慢,所以這也就是典型的犧牲別人的速度來提高自己的速度例子。
既然,這個多線程下載這麼厲害,那麼下面就由小明來講解其中的奧秘吧。
2、怎麼去使用多線程下載??
首先,我們得來說下多線程下載實現的大致思路,以及在使用多線程下載過程應該需要注意的問題。
多線程下載實現的大致思路:
大致思路是這樣的,也就是把整個一個文件資源分為若干個部分,然後開啟若干個線程,並且使得每個線程負責下載每個子部分的文件,由於
線程下載是異步的,大大縮短了下載的時間,最後將所有的子部分寫入到同一個文件中,最後重組得到一個完整的文件。
首先,我們得說下整個資源下載,我們通過網絡請求然後可以得到一個文件的整體輸入流,然後我們需要得到不是整個文件的輸入流,而是
得到每個線程負責下載的子部分文件的輸入流。然後得到這些指定大小的輸入流,再次寫入到我們本地文件中,寫入流的時候也需要注意,
每個子部分輸入流必須寫入相對應的文件位置上,否則會造成後一個寫入文件中的輸入流會覆蓋上一部分寫入文件的輸入流。
再次整理一下思路我們需要注意哪些問題:
問題一:如何獲得整個下載資源的大小
問題二:獲得整個文件資源的大小後,如何去拆分這個文件,如何去分配每個子部分文件,並且讓不同的子線程來下載,也就是如何
確定每個子線程下載文件的區間(即每個子線程負責下載的子部分文件的開始下載和結束下載位置)
問題三:如何去獲得我們需要的指定的大小的輸入流,而不是獲得由服務端一下把整個文件的輸入流
問題四:如何使得每個子線程寫入文件合適的起始位置,並且系統默認就是每次往文件中寫數據的時候都是從0位置開始寫,這樣會出現後面
寫入的數據,會覆蓋前面寫入的數據。
3、逐個擊破解決上面幾個問題,待這些問題都解決了,那麼我們的多線程也就實現了
問題一的解決辦法:
獲取整個下載資源大小,這個很簡單,可以直接通過HttpURLConnection網絡請求而得到一個HttpURLConnection類型的連接對象
中的getContentLength來得到我們需要下載資源文件的大小。
更重要的是:我們拿到這個文件大小來干什麼???其實說白了,就是仿造一個一樣大小的空白文件來占用本地存儲空間
為什麼要這樣做呢??其實細心的人就發現,當我們在下載電影或者文件的時候我們會發現在下載目錄中會出現一個臨時文件
而這個臨時文件大小與我們要下載的文件的大小一致,並且這個文件此時是空白的。不信你可以右擊查看屬性文件大小,
為什麼要占用空間,我們可以去設想一下這個情景,假如電腦中的儲存空間只剩1G了,而你下載的電影正好1G,電影正在下載過程
假如沒有提前占好空間的話,在下載過程中你又下載一個首歌,此時空間明顯不足以裝下這部電影,那請問這部電影將怎麼辦?
所以為了防止這種情況出現,也就出現所謂提前占用存儲空間。
問題二的解決辦法:
如何去拆分整個文件,因為我們要讓每個線程去負責每個子部分文件的下載任務,所以直接按照線程的數目來分吧,但是有個問題
就是無法做到每個線程平均分配每個子部分文件的長度,所以我們就采用一個辦法,假設有n個線程,就是讓前n-1大小一樣,最後一個
就包括一些剩余的零頭吧,也就是最後一個線程處理最後剩余所有的子部分文件長度。
所以就有如下公式:
前n-1個線程的子部分文件尺寸: size=len/threadCount
這樣也就很容易得到了每個線程負責子部分文件的長度
偽代碼:
int size=length/threadCount;
for (int id = 0; id < threadCount; id++) {
//1、確定每個線程的下載區間
//2、開啟對應子線程下載
int startIndex=id*size;//確定每個線程的子部分文件的開始位置
int endIndex=(id+1)*size-1;//確定每個線程的子部分文件的結束位置
if (id==threadCount-1) {//判斷如果是最後一個線程,直接讓它的子部分文件結束位置延伸最後即可,也即文件長度-1
endIndex=length-1;
}
System.out.println("第"+id+"個線程的下載區間為"+startIndex+"--"+endIndex);
問題三的解決辦法:
如何去指定確定大小的輸入流呢?在HttpURLConnection對象中有個setRequestProperty方法設置頭部信息可以拿到拿到指定大小的輸入流
conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);
但是需要注意的一點是:你的請求的服務器必須支持多線程下載,並且才能拿到指定大小輸入流
為什麼要拿到指定大小的輸入流為的就是與劃分子部分文件長度對應起來,得到的對應指定大小的輸入流通過輸出流寫入到相應大小的子部分文件中
問題四的解決辦法:
防止默認設置(每次都從0位置開始寫)的影響使得後面寫入的數據會覆蓋前面寫入的數據,通過RandomAccessFile中的seek方法傳入每個子部分文件開始的
位置,也就間接更改了默認每次都從0開始寫,從每個子部分文件起始位置寫,這樣就不會覆蓋前面的數據。
RandomAccessFile mAccessFile=new RandomAccessFile(file, "rwd");//"rwd"可讀,可寫
mAccessFile.seek(startIndex);//表示從不同的位置寫文件
通過解決上面四個問題,把整個實現的思路梳理了一下,那麼我將實現過程大致總結為以下5點:
1、得到下載資源文件的大小,產生相同大小的隨機RandomAccessFile空白文件,來占用空間
2、並且把RandomAccessFile空白文件分割成若干個部分,並且確定每個子部分文件的下載空間
3、開啟對應的子線程
4、從網絡服務器拿到指定大小的部分輸入流
5、從RandomAccessFile文件的不同的開始位置開始往其中寫入我們得到對應的指定大小的輸入流
Main
package com.mikyou.multithread; import java.io.File; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; /** * @author zhongqihong * 多線程下載 * */ public class Main { public static final String PATH="http://120.203.56.190:8088/upload/mobilelist.xml"; public static int threadCount=3;//進行下載的線程數量 public static void main(String[] args) { try { URL url=new URL(PATH); HttpURLConnection conn=(HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(8000); conn.setReadTimeout(8000); conn.connect(); if (conn.getResponseCode()==200) { int length=conn.getContentLength();//返回文件大小 //占據文件空間 File file =new File("mobilelist.xml"); RandomAccessFile mAccessFile=new RandomAccessFile(file, "rwd");//"rwd"可讀,可寫 mAccessFile.setLength(length);//占據文件的空間 int size=length/threadCount; for (int id = 0; id < threadCount; id++) { //1、確定每個線程的下載區間 //2、開啟對應子線程下載 int startIndex=id*size; int endIndex=(id+1)*size-1; if (id==threadCount-1) { endIndex=length-1; } System.out.println("第"+id+"個線程的下載區間為"+startIndex+"--"+endIndex); new DownLoadThread(startIndex, endIndex, PATH, id).start(); } } } catch (Exception e) { e.printStackTrace(); } } }
DownLoadThread
package com.mikyou.multithread; 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; public class DownLoadThread extends Thread{ private int startIndex,endIndex,threadId; private String urlString; public DownLoadThread(int startIndex,int endIndex,String urlString,int threadId) { this.endIndex=endIndex; this.startIndex=startIndex; this.urlString=urlString; this.threadId=threadId; } @Override public void run() { try { URL url=new URL(urlString); HttpURLConnection conn=(HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(8000); conn.setReadTimeout(8000); conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);//設置頭信息屬性,拿到指定大小的輸入流 if (conn.getResponseCode()==206) {//拿到指定大小字節流,由於拿到的使部分的指定大小的流,所以請求的code為206 InputStream is=conn.getInputStream(); File file =new File("mobilelist.xml"); RandomAccessFile mAccessFile=new RandomAccessFile(file, "rwd");//"rwd"可讀,可寫 mAccessFile.seek(startIndex);//表示從不同的位置寫文件 byte[] bs=new byte[1024]; int len=0; int current=0; while ((len=is.read(bs))!=-1) { mAccessFile.write(bs,0,len); current+=len; System.out.println("第"+threadId+"個線程下載了"+current); } mAccessFile.close(); System.out.println("第"+threadId+"個線程下載完畢"); } } catch (Exception e) { e.printStackTrace(); } super.run(); } }
運行結果:
現在常見的廣告欄效果,自動切換廣告,也可手動滑動切換。 我用ViewPager實現的,廢話不多說,上代碼: 1、布局文件 layout_a
提示:因為該新聞app已經基本完成,所以下方代碼量較大,請謹慎!或者從 ViewPager和Fragment結合使用實現新聞類app(一)一步步向下看!經過幾天的努力,不
(一)概述本章給大家帶來的是Android中的Menu(菜單),而在Android中的菜單有如下幾種:OptionMenu:選項菜單,android中最常見的菜單,通過M
有時候關閉了手機qq還是能收到信息,手機qq如何完全退出呢?下面我們就一起來看看吧! 手機QQ推出登陸教程方法一、退出QQ程序 第一步:打開手機QQ 第二步