Android應用界面開發——Handler(實現倒計時)
Android的消息傳遞機制是另一種形式的“事件處理”,這種機制主要是爲了解決Android應用的多線程問題——Android平臺只允許UI線程修改Activity裏的UI組件,這會導致新啓動的線程無法動態改變界面組件的屬性值。但在實際Android應用開發中,需要讓新啓動的線程週期性的改變界面組件的屬性值,這就需要藉助於Handler的消息機制來實現了。
多線程與異步
在學習Handler之前,有必要了解一下多線程與異步。
當一個程序第一次啓動時,Android會同時啓動一條主線程(Main Thread),主線程主要負責處理與UI相關的事件,所以,主線程通常又被叫做UI線程。
當在主線程中進行耗時操作時(例如請求網絡資源),主線程可能被卡死,這就需要創建一個新的線程來完成耗時操作,該操作完成後再通知主線程(Handler可以完成線程與線程之間的通信工作),這幾個線程同時工作,就是多線程,而這種處理方式就是異步。
什麼是Handler?
一個Handler允許發送、處理消息和與線程消息隊列相關的可執行對象。
Handler類的主要作用:
- 在新啓動的線程中發送消息。
在主線程中獲取、處理消息。
Handler類包含如下方法用於發送、處理消息:- void handleMessage(Message msg):處理消息的方法。該方法通常用於被重寫。
- final boolean hasMessages(int what):檢查消息隊列中是否包含what屬性爲指定值的消息。
- final boolean hasMessages(int what, Object object):檢查消息隊列中是否包含what屬性爲指定值且object屬性爲指定對象的消息。
- 多個重載的Message obtainMessage():獲取消息。
- sendEmptyMessage(int what):發送空消息。
- final boolean sendEmptyMessageDelayed(int what, long delayMillis):指定多少毫秒後發送空消息。
- final boolean sendMessage(Message msg):立即發送消息。
- final boolean sendMessageDelayed(Message msg, long delayMillis):指定多少毫秒後發送消息。
Message、Handler、MessageQueue、Looper工作原理
- Message:Handler接收和處理的消息對象。
-
2個整型數值:輕量級存儲int類型的數據。
1個Object:任意對象。
replyTo:線程通信時使用。
what:用戶自定義的消息碼,讓接收者識別消息。
MessageQueue:Message的隊列。
採用先進先出的方式管理Message。
每一個線程最多可以擁有一個。
Looper:消息泵,是MessageQueue的管理者,會不斷從MessageQueue中取出消息,並將消息分給對應的Handler處理。
每個線程只有一個Looper。
Looper.prepare():爲當前線程創建Looper對象。
Looper.myLooper():可以獲得當前線程的Looper對象。
Handler:能把消息發送給MessageQueue,並負責處理Looper分給它的消息。
異步消息機制處理流程圖如下:
異步消息處理的整個流程如上圖所示,首先需要在主線程當中創建一個Handler對象,並重寫handleMessage()方法。然後當子線程中需要進行UI操作時,就創建一個Message對象,並通過Handler將這條消息發送出去。之後這條消息會被添加到MessageQueue的隊列中等待被處理,而Looper則會一直嘗試從MessageQueue中取出待處理消息,最後分發回Handler的handleMessage()方法中。由於Handler是在主線程中創建的,所以此時handleMessage()方法中的代碼也會在主線程中運行,於是在這裏就可以安心地進行UI操作了。
實現倒計時Demo
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.trampcr.countdowndemo.MainActivity">
<TextView
android:id="@+id/count_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text=""
android:textColor="#000000"
android:textSize="50sp" />
<Button
android:id="@+id/start"
android:layout_width="150dp"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="30dp"
android:text="開始倒計時"
android:textColor="#000000"
android:textSize="18sp" />
</RelativeLayout>
包含一個文本用於顯示倒計時數字,一個按鈕用於開啓倒計時。
MainActivity.java
public class MainActivity extends AppCompatActivity {
private TextView mCountNumber;
private Button mStart;
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case 88888:
int value = (int) msg.obj;
mCountNumber.setText(String.valueOf(value / 1000));
msg = Message.obtain();//重新獲取消息
msg.arg1 = 0;
msg.arg2 = 1;
msg.what = 88888;
msg.obj = value - 1000;
if (value > 0){
sendMessageDelayed(msg, 1000);
}
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCountNumber = (TextView) findViewById(R.id.count_number);
mStart = (Button) findViewById(R.id.start);
mStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Message message = handler.obtainMessage();
message.arg1 = 0;
message.arg2 = 1;
message.what = 88888;
message.obj = 10000;
handler.sendMessageDelayed(message, 1000);
}
});
}
}
首先創建一個Handler對象,並實現handleMessage方法,用於接收消息。
接下來在點擊事件中創建Message對象,不建議使用new Message(),而應該用handler.obtainMessage()來創建Message,然後使用handler.sendMessageDealyed延遲發送消息,發出的消息在上面創建好的handler中可以接收到,然後處理消息。
以上代碼就實現了倒計時效果,但是我們看到handler這部分代碼的背景色爲黃色,究其原因是handler是一個內部類,可能產生內存泄漏。
解決方法:使用外部類。
代碼如下:
public class MainActivity extends AppCompatActivity {
private TextView mCountNumber;
private Button mStart;
private CountDownHandler mCountDownHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCountNumber = (TextView) findViewById(R.id.count_number);
mStart = (Button) findViewById(R.id.start);
mCountDownHandler = new CountDownHandler(this);
mStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Message message = mCountDownHandler.obtainMessage();
message.arg1 = 0;
message.arg2 = 1;
message.what = 88888;
message.obj = 10000;
mCountDownHandler.sendMessageDelayed(message, 1000);
}
});
}
public TextView getmCountNumber() {
return mCountNumber;
}
public static class CountDownHandler extends Handler {
public final WeakReference<MainActivity> mainActivityWeakReference;
public CountDownHandler(MainActivity activity) {
mainActivityWeakReference = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity mainActivity = mainActivityWeakReference.get();
switch (msg.what) {
case 88888:
int value = (int) msg.obj;
mainActivity.getmCountNumber().setText(String.valueOf(value / 1000));
msg = Message.obtain();//重新獲取消息
msg.arg1 = 0;
msg.arg2 = 1;
msg.what = 88888;
msg.obj = value - 1000;
if (value > 0) {
sendMessageDelayed(msg, 1000);
}
break;
}
}
}
}
定義一個外部類繼承Handler,並實現TextView的getter方法,最後運行效果和原來一樣,但是該方法可避免內存泄漏。