前言
很久很久以前就聽說了,每一個android的應用程序都會分別運行在一個獨立的dalvik虛擬機進程中,而在每個虛擬機在啟動時會運行一個UI主線程(Main Thread),而為啥叫UI主線程而不是AI主線程或者是BI主線程呢?因為它要處理全部和UI相關的事件;因為Android系統采用的是UI單線程模型,只能由UI主線程對其進行UI操作,如果子線程抱著眾人拾柴火焰高的覺悟來幫忙UI主線程更新UI界面的話,對不起哦~Android系統就會報錯的。粗俗點講就是:我們只能通過UI主線程來蹂躏UI界面,但是其他線程來的話會被告弓雖女干滴。。
那麼現在問題來了!鑒於近來挖掘機那麼火,我也不好意思繼續問這個問題了。。。嗯嗯~網絡操作之類耗時操作就像挖掘機那樣,我們在下載文件的時候一樣跟挖掘機挖個大坑一樣需要一定的時間;當挖掘機司機挖好一個大坑要找老板反饋工作完成一樣,我們下載好一個文件自然要馬上告訴屏幕前苦逼等待的用戶們,誰知道他們多著急想看**.avi呢;但是你在挖坑時好意思叫老板在旁邊看你嗎?老板分分鐘為幾千萬上下的事忙著呢~所以嘛同理,對於網絡操作,我們當然也不能在UI主線程中進行網絡操作,因為這樣會阻塞主線程造成界面卡死,也會造成ANR(應用程序無響應)。我們應該把文件下載、文件讀取諸如此類的耗時操作放到子線程中去進行,等到子線程耗時操作完成時通知UI界面做出響應。
不要在UI主線程中進行耗時操作
如果你不信邪一定要在UI主線程進行下載文件、加載大文件之類的耗時操作。如下代碼:
復制代碼
private Button btn;
//onCreate之類的生命周期的方法就是允許在UI主線程中
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
downLoad();//調用UI主線程的下載函數
}
});
}
private void downLoad(){
try {
Thread.sleep(10000);//休眠10秒,模擬網絡文件下載耗時操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
復制代碼
你會發現界面卡主了10秒:(模擬下載操作的按鈕為深色,說明按鈕一直為按下狀態)
如果這時候你手比較管不住的話,雖然點幾下界面,沒事~Androi系統會馬上送你一份ANR大禮哦,而且還不用998元耶!
小結一個:不要在UI主線程中進行耗時操作,你可能會疑問什麼是UI主線程,UI主線程主要運行的就是Activity、Service等裡面的生命周期方法,所以不要在生命周期方法如onCreate()中進行下載這些大事件。對於耗時操作,我們應該新建一個子線程並交給他處理,但是還需要注意一點。
不要在子線程中更新UI界面
既然我們說下載文件要在子線程中進行,那麼我們就新建一個子線程把下載操作放到裡面進行咯,代碼如下:
復制代碼
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = (Button) findViewById(R.id.btn);
text = (TextView) findViewById(R.id.text);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new Thread(){
@Override
public void run() {
//在子線程中進行下載操作
try {
Thread.sleep(10000);//休眠10秒,模擬耗時操作
} catch (InterruptedException e) {
e.printStackTrace();
}
text.setText("下載完成");//設置TextView,通知UI界面下載完成
}
}.start();
}
});
}
復制代碼
10秒後,你覺得會在UI界面完美顯示“下載完成”麼?一般,出現這個才符合Androi系統的一貫作風
並且在Log中報錯如下
小弟英語其實很廢柴,但是隱隱約約有人告訴我:這不是叫只能在主線程中更新UI嗎?不信,金山翻譯一下去呀。。。。
小結一個:不要在子線程中更新UI界面,這樣會導致android系統報錯、應用崩潰退出。UI界面時單線程模式,我們只能通過UI主線程中對UI的界面進行相關的更新,千萬不要越線辦事,你要記住的是~UI界面是UI主線程的老婆,你們這些子線程誰都別想動!
利用Thread+Handler進行異步處理
那麼問題來了,現在我們需要進行耗時操作(例如下載文件)時不能在主線程執行,我們又需要在UI界面通知用戶我們活干完了不能再子線程中執行。這似乎是一個棘手的熱山芋呀,幸好谷歌給我們提供了一個救我們於危難之中的Handler,一個能讓主線程監聽子線程發送來消息的東東,至於Handler的實現原理我會在後面的文章詳細介紹,現在我們只需要先了解Handler的用法。
復制代碼
private Button btn;
private TextView text;
private Handler handler = new Handler(){
private int process = 0;
@Override
public void handleMessage(Message msg) {
switch(msg.what){
case 0://更細下載進度
process += 1;
text.setText("下載" + process + "%");//在主線程中更新UI界面
break;
case 1://提示下載完成
text.setText("下載完成");//在主線程中更新UI界面
break;
default:
break;
}
}
};
//onCreate之類的生命周期的方法就是允許在UI主線程中
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = (Button) findViewById(R.id.btn);
text = (TextView) findViewById(R.id.text);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new Thread(){
@Override
public void run() {
//在子線程中進行下載操作
for(int i = 0; i < 100; i++){
try {
Thread.sleep(200);//休眠0.2秒,模擬耗時操作
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0);//發送消息到handler,通知下載進度
}
handler.sendEmptyMessage(1);//發送消失到handler,通知主線程下載完成
}
}.start();
}
});
}
復制代碼
這裡來解釋一下Handler的使用方法:
1、我們為了不阻塞主線程,將下載任務通過子線程來執行。
復制代碼
new Thread(){
@Override
public void run() {
//在子線程中進行下載操作
for(int i = 0; i < 100; i++){
try {
Thread.sleep(200);//休眠0.2秒,模擬耗時操作
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0);//發送消息到handler,通知下載進度
}
handler.sendEmptyMessage(1);//發送消失到handler,通知主線程下載完成
}
}.start();
復制代碼
2、當子線程需要跟主線程交流時,也就是當子線程要跟UI主線程說:親,偶下載文件到80%了或者偶已經把文件下載完成了!執行這句代碼
handler.sendEmptyMessage(1);//發送消失到handler,通知主線程下載完成
3、當發送空消息之後,在Handler將會收到子線程發來的消息,觸發回調方法handlerMessage(),我們就在這裡對UI界面進行更新,這個回調方法是運行在UI主線程的
復制代碼
@Override
public void handleMessage(Message msg) {
switch(msg.what){
case 0://更細下載進度
process += 1;
text.setText("下載" + process + "%");//在主線程中更新UI界面
break;
case 1://提示下載完成
text.setText("下載完成");//在主線程中更新UI界面
break;
default:
break;
}
}
復制代碼
4、最後,UI界面更新成功!(圖嘛,我這裡就不上了。。。。)
小結一個:對於比較耗時間的任務,我們一般需要放在子線程中執行;當子線程更新UI界面時,子線程可以通過Handler來通知主線程更新,一般通過發送消息來觸發handlerMessage()這個回調方法來執行UI界面的更新。
進一步簡略de操作:handler.post方法和view.post方法
但是如果你覺得每次都要重寫handlerMessage()比較麻煩,我們完全可以用更加簡略的方法來解決我們的需求,就是用handler中的post方法。代碼如下
復制代碼
new Thread(){
@Override
public void run() {
//在子線程中進行下載操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.post(new Runnable() {
@Override
public void run() {
text.setText("下載完成");
}
});//發送消失到handler,通知主線程下載完成
}
}.start();
復制代碼
這樣處理的話我們就可以不用重寫handlerMessage()方法了,適合子線程與主線程進行較為單一的交流。但在這裡我們要強調的一點的是,post裡面的Runnable還是在UI主線程中運行的,而不會另外開啟線程運行,千萬不要在Runnable的run()裡面進行耗時任務,不然到時又ANR了可別找我哦。。
如果你有時候連handler都不想搞,還可以這樣寫代碼滴。
我們只需要把handler換成View組件進行post,更新任務自然會加載到UI主線程中進行處理。
復制代碼
text.post(new Runnable() {
@Override
public void run() {
text.setText("下載完成");
}
});//發送消失到handler,通知主線程下載完成
復制代碼
至於Handler機制以及這兩種post的原理,我將會在後面的博客文章中專題介紹,這裡只提供一個使用方法而已。