注意:本人能力有限,若有錯誤、不合理、可優化的地方 請務必告知我!複製代碼
1. 保證可見性
根據JVM內存模型得知,JVM將內存分爲主內存與工做內存兩個部分,全部的變量都存放在主內存中。而每條線程有本身的工做內存,其存放部分主存中變量的拷貝,線程對變量的操做必須在工做內存中完成,而後更新到主存中。
當一個共享變量被volatile修飾,它會保證修改的值當即更新到主存中,其餘線程訪問時會去主存中讀取新的值。而普通的共享變量不能保證可見性,由於普通共享變量被修改以後,何時被寫入主存是不肯定的,當其餘線程去讀取時,此時主存中可能仍是原來的舊值,所以沒法保證可見性。html
2. 禁止指令重排
當代碼編譯時JVM會對指令執行的順序進行優化,但volatile不會,以下所示java
//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執行的順序也不作保證。android
3. 非原子性
volatile變量是不保證原子性的,可是須要注意的是 volatile關鍵字對long/double類型的get/set操做保證了原子性,詳見這裏 。數組
Android基本網絡請求類,這個沒必要多說,接觸過Android開發的同窗也必定會了解,若是是Android新同窗請點我 。至於爲何我用HttpURLConnection而不用OKhttp或者Retrofit,由於最終我會開源一個Android下載文件的框架,因此不作過多的外部依賴。安全
這個類很特殊,雖然是java.io包下的,可是隻實現了DataOutput, DataInput, Closeable這三個接口,惟一父類是Object。其功能是隨機讀寫文件,換句話說就是能夠在一個文件的任何位置讀取或者寫入。在本文中用它來實現文件下載的斷點續傳。bash
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,這樣就從源頭上解決了內存泄露。
再次強調,本人能力有限,不免有知識上的空缺或者疏漏,若有不足之處請告知!我會用業餘時間繼續更新,感謝您的閱讀。