擔心原文消失,做此記錄,感謝
https://www.cnblogs.com/net168/p/4075126.html
前言
很久很久以前就聽說了,每一個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的原理,我將會在后面的博客文章中專題介紹,這里只提供一個使用方法而已。