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