一、項目整體介紹
使用HttpUrlConnection與服務器建立連接,獲取文件長度,開多個線程下載資源,使用RandomAccessFile寫入文件;
本項目沒有使用高大上的OKhttp
1、項目邏輯流程
2、項目目的:
我把這個小demo寫在簡歷中,有兩次面試都問道爲什麼採用多線程下載,多線程下載比單線程下載的好處?我在這裏總結了一下
- 多線程主要的優勢是與服務器建立多個http鏈接
多線程下載有可能比單線程下載速度快,多線程主要的優勢是與服務器建立多個http鏈接(目前底層都是tcp,編程就基於socket);相當於多個人搬東西,
- 限制下載速度的主要因素是帶寬,而不是CPU執行速度
這個帶寬指服務器和客戶端兩端的帶寬;若服務器限制每個socket鏈接傳輸數據帶寬時,多線程下載在服務器端可以獲得更大的帶寬,若是單個socket服務器端帶寬就大於客戶端的網絡帶寬,那麼多線程是不會快過單線程的,因爲TCP的擁塞控制和流量控制,服務器端發這麼塊,客戶端接收慢,瓶頸就在客戶端。
二、項目代碼
主活動onCreate方法
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_path=(EditText) findViewById(R.id.et_path);
et_threadCount =(EditText) findViewById(R.id.et_threadCount);
ll_pb_layout=(LinearLayout) findViewById(R.id.ll_layout);
pbList=new ArrayList<ProgressBar>();
// 獲取權限
String[] PERMISSIONS_STORAGE = {
Manifest.permission.INTERNET,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
};
for (String str : PERMISSIONS_STORAGE){
int permission = ActivityCompat.checkSelfPermission(MainActivity.this, str);
if(permission!= PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
ActivityCompat.requestPermissions(
MainActivity.this,
PERMISSIONS_STORAGE,
1
);
}
}
}
下載資源線程
private class DownLoadThread extends Thread {
//通過構造方法把每個線程下載的開始位置和結束位置傳遞進來
private int startIndex;
private int endIndex;
private int threadId;
private int PbMaxSize;//代表當前線程下載的最大值
// 如果中斷過獲取上次下載的位置
private int pblastPostion;
public DownLoadThread(int startIndex, int endIndex, int threadId) {
this.startIndex = startIndex;
this.endIndex = endIndex;
this.threadId = threadId;
}
@Override
public void run() {
try{
//計算當前進度條的最大值
PbMaxSize=endIndex-startIndex;
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
//[4.0]如果中間斷過繼續上次的位置繼續下載從文件中讀取上次下載的位置
File file=new File(getFilename(path)+threadId+".txt");
if(file.exists() && file.length()>0){
FileInputStream fis=new FileInputStream(file);
BufferedReader bufr=new BufferedReader(new InputStreamReader(fis));
String lastPositionn =bufr.readLine();
int lastPosition=Integer.parseInt(lastPositionn);
pblastPostion=lastPosition-startIndex;
startIndex=lastPosition+1;
System.out.println("線程id"+threadId+"真實下載的位置:"+startIndex+"---"+endIndex);
fis.close();
}
conn.setRequestProperty("Range","bytes="+startIndex+"-"+endIndex);
int code =conn.getResponseCode();
if(code==206){
RandomAccessFile raf=new RandomAccessFile(getFilename(path),"rw");
raf.seek(startIndex);
InputStream in=conn.getInputStream();
int len=-1;
byte[] buffer =new byte[1024*1024];
int total=0;
while((len=in.read(buffer))!=-1){
raf.write(buffer,0,len);
total+=len;
//實現斷點續傳就是把當前線程下載的位置給存起來下次再下載的時候就是按照上次下載的位置繼續下載就可以了
int currentThreadPosition =startIndex+total;
RandomAccessFile raff=new RandomAccessFile(getFilename(path)+threadId+".txt","rwd");
raff.write(String.valueOf(currentThreadPosition).getBytes());
raff.close();
//[10]設置一下當前進度條的最大值和當前進度,無需在子線程中更新
pbList.get(threadId).setMax(PbMaxSize);
pbList.get(threadId).setProgress(pblastPostion+total);
}
raf.close();
System.out.println("線程id:"+threadId+"---下載完畢了");
//
synchronized (DownLoadThread.class){
runningThread--;
if(runningThread==0){
for (int i=0;i<threadCount;i++){
File delteFile =new File(getFilename(path)+i+".txt");
delteFile.delete();
}
}
}
}
}
catch(Exception e){
e.printStackTrace();
}
}
}
其他
//獲取文件的名字"http://192.168.11.73:8080/feiq.exe"
public String getFilename(String path) {
int start = path.lastIndexOf("/") + 1;
String substring =path.substring(start);
String filename = Environment.getExternalStorageDirectory().getPath()+"/"+substring;
return filename;
}
按鈕點擊事件
public void click(View v){
//[2]獲取下載的路徑
path=et_path.getText().toString().trim();
//[3]獲取線程的數量
String threadCountt =et_threadCount.getText().toString().trim();
threadCount=Integer.parseInt(threadCountt);
//先移除進度條在添加
ll_pb_layout.removeAllViews();
pbList.clear();
for(int i=0;i< threadCount;i++) {
//[3.1]把我定義的item佈局轉換成一個view對象
ProgressBar pbview = (ProgressBar) View.inflate(getApplicationContext(), R.layout.item, null);
pbList.add(pbview);
//[4]動態的添加進度條
ll_pb_layout.addView(pbview);
}
// 創建子線程去獲取文件大小,
// 獲取到大小後,根據線程下載數,再循環開多個線程去取部分資源
new Thread(){
@Override
public void run() {
try {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
int code = conn.getResponseCode();
if (code == 200)//200代表獲取服務器資源全部成功206請求部分資源
{
//(6)獲取服務器文件的大小
int length = conn.getContentLength();
runningThread = threadCount;
System.out.println("length:" + length);
RandomAccessFile randomAccessFile = new RandomAccessFile(getFilename(path), "rw");
randomAccessFile.setLength(length);
//(7)算出每個線程下載的大小|
int blockSize = length / threadCount;
for (int i = 0; i < threadCount; i++) {
int startIndex = i * blockSize;//每個線
//特殊情況就是最後一個線程
int endIndex = (i + 1) * blockSize - 1;
if (i == threadCount - 1) {
//說明是最後一個線程
endIndex = length - 1;
}
System.out.println("線程id:" + i + "理論下載的位置:" + startIndex + "-----" + endIndex);
DownLoadThread thread=new DownLoadThread(startIndex,endIndex,i);
thread.start();
}
}
}
catch(Exception e){
e. printStackTrace();
}
}
}.start();
}
三、展示
我的本地帶寬是50M
單個線程
多個線程