Android下載檔案(一)下載進度&斷點續傳
索引
- Android下載檔案(一)下載進度&斷點續傳
- Android下載檔案(二)單任務多執行緒併發&斷點續傳(待續)
- Android下載檔案(三)自定義進度條(待續)
- Android下載檔案(四)任務資訊持久化儲存(待續)
- Android下載檔案(五)IPC(待續)
- Android下載檔案(六)XDownloader(待續)
前言
從接觸Android開發至今也快兩年了,一路走過來可以說是站在巨人的肩膀上前進,真的很感激為開源世界作出貢獻的人。話說回來,搞了這麼久的開發卻一直在用別人的勞動成果也不是回事,所以我決定寫幾篇文章分享我對Android下載檔案的理解,並在最後整合並開源一個框架,也是對我在Android之旅中的一個小小的總結。
注意:本人能力有限,如有錯誤、不合理、可優化的地方 請務必告知我!複製程式碼
實現效果
本節主要講解Android下載檔案的進度獲取和斷點續傳,效果如下
所需知識點
- volatile
- RandomAccessFile
- HttpURLConnection
- Handler
volatile
volatile是java中修飾變數的關鍵字,在這裡重點講下其特性,後面會用到。
如需深入理解請參考 《深入理解Java虛擬機器》12.3.3 對於volatile型變數的特殊規則
1. 保證可見性
根據JVM記憶體模型得知,JVM將記憶體分為主記憶體與工作記憶體兩個部分,所有的變數都存放在主記憶體中。而每條執行緒有自己的工作記憶體,其存放部分主存中變數的拷貝,執行緒對變數的操作必須在工作記憶體中完成,然後更新到主存中。
當一個共享變數被volatile修飾,它會保證修改的值立即更新到主存中,其他執行緒訪問時會去主存中讀取新的值。而普通的共享變數不能保證可見性,因為普通共享變數被修改之後,什麼時候被寫入主存是不確定的,當其他執行緒去讀取時,此時主存中可能還是原來的舊值,因此無法保證可見性。
2. 禁止指令重排
當程式碼編譯時JVM會對指令執行的順序進行優化,但volatile不會,如下所示
//x、y為非volatile變數
//flag為volatile變數
x = 2; //語句1
y = 0; //語句2
flag = true; //語句3
x = 4; //語句4
y = -1; //語句5複製程式碼
語句3必定在語句1/2後執行,但語句1/2順序不做保證,同理,語句3也必定在語句4/5前面執行,語句4/5執行的順序也不做保證。
3. 非原子性
volatile變數是不保證原子性的,但是需要注意的是 volatile關鍵字對long/double型別的get/set操作保證了原子性,詳見這裡 。
HttpURLConnection
Android基本網路請求類,這個不必多說,接觸過Android開發的同學也一定會了解,如果是Android新同學請點我 。至於為什麼我用HttpURLConnection而不用OKhttp或者Retrofit,因為最終我會開源一個Android下載檔案的框架,所以不做過多的外部依賴。
RandomAccessFile
這個類很特殊,雖然是java.io包下的,但是隻實現了DataOutput, DataInput, Closeable這三個介面,唯一父類是Object。其功能是隨機讀寫檔案,換句話說就是可以在一個檔案的任何位置讀取或者寫入。在本文中用它來實現檔案下載的斷點續傳。
Handler
Android開發必然涉及到的東西,新同學請點我 。
###準備好了,開始擼程式碼
1.首先下載檔案需要下載連結/下載路徑/檔名等屬性,所以我們寫一個JavaBean,這裡用到了volatile關鍵字,詳見註釋
public class TaskInfo {
private String name;//檔名
private String path;//檔案路徑
private String url;//連結
private long contentLen;//檔案總長度
/**
* 迄今為止java虛擬機器都是以32位作為原子操作,而long與double為64位,當某執行緒
* 將long/double型別變數讀到暫存器時需要兩次32位的操作,如果在第一次32位操作
* 時變數值改變,其結果會發生錯誤,簡而言之,long/double是非執行緒安全的,volatile
* 關鍵字修飾的long/double的get/set方法具有原子性。
*/
private volatile long completedLen;//已完成長度
getter/setter省略複製程式碼
2.下載檔案需要在子執行緒中進行,所以我們寫一個類,實現Runnable介面,方便任務的建立
public class DownloadRunnable implements Runnable {
private TaskInfo info;//下載資訊JavaBean
private boolean isStop;//是否暫停
/**
* 構造器
* @param info 任務資訊
*/
public DownloadRunnable(TaskInfo info) {
this.info = info;
}
/**
* 停止下載
*/
public void stop() {
isStop = true;
}
/**
* Runnable的run方法,進行檔案下載
*/
@Override
public void run() {
HttpURLConnection conn;//http連線物件
BufferedInputStream bis;//緩衝輸入流,從伺服器獲取
RandomAccessFile raf;//隨機讀寫器,用於寫入檔案,實現斷點續傳
int len = 0;//每次讀取的陣列長度
byte[] buffer = new byte[1024 * 8];//流讀寫的緩衝區
try {
//通過檔案路徑和檔名例項化File
File file = new File(info.getPath() + info.getName());
//例項化RandomAccessFile,rwd模式
raf = new RandomAccessFile(file, "rwd");
conn = (HttpURLConnection) new URL(info.getUrl()).openConnection();
conn.setConnectTimeout(120000);//連線超時時間
conn.setReadTimeout(120000);//讀取超時時間
conn.setRequestMethod("GET");//請求型別為GET
if (info.getContentLen() == 0) {//如果檔案長度為0,說明是新任務需要從頭下載
//獲取檔案長度
info.setContentLen(Long.parseLong(conn.getHeaderField("content-length")));
} else {//否則設定請求屬性,請求制定範圍的檔案流
conn.setRequestProperty("Range", "bytes=" + info.getCompletedLen() + "-" + info.getContentLen());
}
raf.seek(info.getCompletedLen());//移動RandomAccessFile寫入位置,從上次完成的位置開始
conn.connect();//連線
bis = new BufferedInputStream(conn.getInputStream());//獲取輸入流並且包裝為緩衝流
//從流讀取位元組陣列到緩衝區
while (!isStop && -1 != (len = bis.read(buffer))) {
//把位元組陣列寫入到檔案
raf.write(buffer, 0, len);
//更新任務資訊中的完成的檔案長度屬性
info.setCompletedLen(info.getCompletedLen() + len);
}
if (len == -1) {//如果讀取到檔案末尾則下載完成
Log.i("tag", "下載完了");
} else {//否則下載系手動停止
Log.i("tag", "下載停止了");
}
} catch (IOException e) {
e.printStackTrace();
Log.i("tag",e.toString());
}
}
}複製程式碼
3.任務開始/停止和進度回撥
public class MainActivity3 extends AppCompatActivity {
private ProgressBar bar;//進度條
private TaskInfo info;//任務資訊
private DownloadRunnable runnable;//下載任務
//用於更新進度的Handler
private Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
//使用Handler製造一個200毫秒為週期的迴圈
handler.sendEmptyMessageDelayed(1, 200);
//計算下載進度
int l = (int) ((float) info.getCompletedLen() / (float) info.getContentLen() * 100);
//設定進度條進度
bar.setProgress(l);
if (l>=100) {//當進度>=100時,取消Handler迴圈
handler.removeCallbacksAndMessages(null);
}
return true;
}
});
@Override
protected void onDestroy() {
//在Activity銷燬時移除回撥和msg,並置空,防止記憶體洩露
if(handler != null){
handler.removeCallbacksAndMessages(null);
handler = null;
}
super.onDestroy();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
//例項化任務資訊物件
info = new TaskInfo("aa.apk"
, Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/Download/"
, "https://download.alicdn.com/wireless/taobao4android/latest/702757.apk");
bar = (ProgressBar) findViewById(R.id.bar);
//設定進度條的最大值
bar.setMax(100);
}
/**
* 開始下載按鈕監聽
* @param view
*/
public void start(View view) {
//建立下載任務
runnable = new DownloadRunnable(info);
//開始下載任務
new Thread(runnable).start();
//開始Handler迴圈
handler.sendEmptyMessageDelayed(1, 200);
}
/**
* 停止下載按鈕監聽
* @param view
*/
public void stop(View view) {
//呼叫DownloadRunnable中的stop方法,停止下載
runnable.stop();
runnable = null;//強迫症,不用的物件手動置空
}
}複製程式碼
Q:為什麼進度資訊不用handler傳送到主執行緒,而是直接從主記憶體中的TaskInfo獲取下載進度?
A:單個執行緒任務確實可以用handler攜帶下載資訊進行執行緒切換,但是我們過後會涉及到多執行緒下載,一個下載任務甚至可以達到128執行緒併發,這麼多子執行緒“同時”向主執行緒傳遞訊息,主執行緒壓力太大會造成“掉幀”,也就是我們所說的卡頓,並且TaskInfo中所有屬性的均具有原子性,不會出現執行緒安全問題。
Q:Handler是非靜態的不會造成記憶體洩露嗎?
A:不會,造成記憶體洩露的原因是Message持有Handler,Handler持有Activity,造成Message-Handler-Activity的引用鏈,導致在Activity銷燬時無法被GC回收。但在Activity銷燬時移除未處理的Message,這樣就從源頭上解決了記憶體洩露。
後記
再次強調,本人能力有限,難免有知識上的空缺或者疏漏,如有不足之處請告知!我會用業餘時間繼續更新,感謝您的閱讀。