Handler原理與使用,以及Android中異步更新的UI的方式

好長時間都沒有發佈新的博客了,今天來寫一篇關於Android中使用頻率極高,但是也經常讓大家感覺摸不着頭腦的handler.

下面呢 主要從以下幾個方面來講.

1.     爲什麼禁止在非UI線程更新UI

2.     Handler概述

3.     Handler的幾種使用方式

4.     Handler的原理,以及handler與message queue, looper之間的關係

5.     HandlerThread是什麼?

6.     異步更新UI 的幾種方式

7.     非UI線程真的不能更新UI嗎?

爲什麼禁止在非UI線程更新UI?

         Handler翻譯成中文就是”處理者”的意思,實際上也確實如此.當我們在處理Android中多線程問題的時候,有需要處理的事情就可以去找Handler了.那麼爲什麼會產生多線程問題呢?

        Google在設計的framework的時候禁止開發者在非UI線程去更新界面UI.那麼Google爲什麼要這麼做呢?查看源碼之後可以發現更新UI 的相關方法爲了保證效率都是沒有加上同步鎖的.而更新UI繪製圖形會調用底層硬件相關方法,如果不加以限制可能會產生意想不到的後果.所以爲了兼顧效率與安全,就設計成只能在主線程更新UI.另外不只是Google,iOS系統也是禁止在主線程更新UI的,足以證明這樣的設計是一種通用做法.

Handler概述

       但是如果只是簡簡單單設計成只能在主線程更新UI的話,那麼對開發者的素質要求勢必會提高,java中的多線程問題想必大家學習的時候也是一大難點吧.開發者爲了保證線程間通信的代碼不會亂掉勢必要自己設計一套線程間通信的框架.這樣的要求未免太高,不利於Android平臺的初期拓展.於是Google就在framework中封裝好了這麼一套線程間通信的框架.這套框架在Google的代碼中也是應用相當廣泛,我們常用的Activity的生命週期回調,事件傳遞等framework層中的代碼中,Handler機制也是隨處可見.下面就讓我們來學習一下Handler的基本使用吧.

Handler的基本使用

   Handler的使用主要和下面幾個方法相關:

      sendMessage()

      sendMessageDelayed()

      post()

      postDelayed()

   帶delayed後綴的方法都是在原方法上延遲幾秒的方法.下面只演示sendMessage()方法和post()方法.

廢話不多說上代碼:

   首先是sendMessage()方法

public class MainActivity extends ActionBarActivity {
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            textView.setText("文字被改變啦");
        }
    };
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.test);
        new Thread(new Runnable() {
            @Override
            public void run() {
                               try {
                    Thread.sleep(2000);
                    handler.sendEmptyMessage(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            }
        }).start();
    }
}

運行結果:

     這段代碼裏我們使用handler在子線程向主線程裏發送了一條空消息.這樣handlemessage方法就可以在主線程執行了

     Message也可以指定並傳遞數據,message共有幾個主要的屬性.what,object,arg1,arg2.

使用示例:

public class MainActivity extends ActionBarActivity {
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            String string = "";
            for (String temp : (ArrayList<String>)msg.obj)
                string += temp;
            textView.setText("what:" + msg.what + "arg1" + msg.arg1 + "arg2" + msg.arg2 + "objString:"+string);
        }
    };
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.test);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    Message message = Message.obtain();
                    message.what = 1;
                    message.arg1 = 100;
                    message.arg2 = 200;
                    ArrayList<String> strings = new ArrayList<String>();
                    strings.add("這是第一條");
                    strings.add("這個第二條");
                    message.obj = strings;
                    handler.sendMessage(message);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

    可以注意到上面這段代碼使用Message.obtain()方法獲取message對象.這樣就可以複用系統提供給我們的message對象了.當然,你不在乎內存的話也可以直接new,但是不推薦那麼寫.

運行結果:




   下面是post()方法的使用.

代碼示例

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.test);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText("post被執行了");
                        }
                    });
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

執行結果:

    這樣的話post裏面的代碼就可以運行在主線程了.需要注意的是如果使用post方法執行,那麼handlemessage方法就不會再執行了.

    此外message處理方式還允許使用者指定一個callback(),在handlemessage之前執行,如果該callback()返回true,則該message會被攔截.返回false就可以繼續向handler傳遞.

具體示例如下:

public class MainActivity extends Activity {
    private Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            textView.setText("消息被callback攔截");
            return true;
        }
    }){
        @Override
        public void handleMessage(Message msg) {
            textView.setText("handler接收到消息,hanlemessage執行");
        }
    };
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.test);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    handler.sendEmptyMessage(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

執行結果:

  看到這裏有的哥們可能就迷糊了,完全可以直接在handlemessage()方法根據message.what判斷,爲什麼還要多此一舉設計一個callback出來呢.其實我個人猜想,這樣的設計可能都是爲了擴展性考慮的.如果有大牛寫了第三方框架,但是又希望給使用框架的程序員控制框架的一些行爲,就可以對外把callback提供出去.讓使用者程序員自行攔截message.但是筆者還沒有使用過這樣的框架.

Handler的原理

         好的,說完了handler的基本使用,下面就輪到handler的原理了.說到handler的原理,就不得不提到message queue和looper.

         下面讓我們來追蹤一下源碼,看看調用sendmessage()方法之後,到底發生了什麼.

查看源碼可知,不論是post(),postDelayed(),sendmessage()還是sendmessageDelayed()最終都會輾轉調用到sendMessageAtTime().

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
}

經過一些健壯性判斷之後,調用了enqueueMessage()方法.

繼續追蹤,

   private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
}

繼續追蹤

  boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

可以發現,message被放入messagequeue管理的一個線性結構中.到這裏handler的使命就完成了.那麼message是如何被主線程使用的呢,handlemessage()怎麼樣在主線程執行的呢?這裏就要請出我們的第三個主角looper了.顧名思義,looper是一個和旋轉和圓有關的東西.事實也確實如此,looper中有個loop()方法.讓我們來看看它的代碼.

  public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            msg.target.dispatchMessage(msg);

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

   可以看到,loop()方法的任務就是在loop關聯的queue中遍歷,如果取到了message,就調用該message的target屬性的dispatchMessage()方法.那麼message的target屬性又是在哪裏被賦值的呢.

   大家還記得上面追蹤到的handler.enqueueMessage()方法嗎,這個方法裏有這麼一句msg.target =this;所以message的target對象就是發送它的handler對象.所以所有使用handler.sendMessage()發送的消息,最終都會發送給handler自己.

  那麼如果我們不希望發送給默認的target對象該怎麼辦呢.除了直接調用目標handler的sendMessage()方法,這裏補充一個發送消息的方法.

  Message message = Message.obtain();
  message.setTarget(handler);
  message.sendToTarget();

    好的補充完畢,我們繼續追蹤dispatchMessage()方法.

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

        這裏需要注意的是,出現了兩個callback,一個是message對象的callback,另一個是mCallback.其中如果使用post()相關的方法發送消息.那麼message.callback屬性就會被賦值傳進來的runnable對象.而mCallback則是創建handler時傳進去的攔截消息的方法.

      到了這裏,邏輯應該很清楚了,handlecallback()方法就是調用了message.callback的run()方法.

      那麼我們該如何,讓looper工作起來呢.如果希望代碼是在主線程工作的,那麼只需要初始化handler的代碼是在主線程執行的.其餘的不用管,一股腦的發就行.

       但是如果代碼的目的地不是主線程.那麼就必須在Handler初始化之前調用looper.Prepare().並在最後調用looper.loop()方法讓looper工作起來.總而言之,Handler是在那個線程中初始化的(未指定looper參數),那麼handlemessage方法就會在哪個線程中執行.但本質上handler的handlemessage方法在哪個線程中執行最主要決定權還是在looper.如果未傳遞,那麼looper就會與當前線程關聯,從而在當前線程執行該方法.

HandlerThread是什麼?

         首先,看下面代碼:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
        myRunnable.handler.sendEmptyMessage(1);

    }

    private class MyRunnable implements Runnable {
        private Handler handler;

        @Override
        public void run() {
            Looper.prepare();
            handler = new Handler(){
                @Override
                public void handleMessage(Message msg) {
                    Log.e("tag", "currentThread:" + Thread.currentThread());
                }
            };
            Looper.loop();
        }
    }
}

    這段代碼並沒有如預期般打出log,而是報出了空指針異常.那麼是什麼原因呢?主要的問題就在於多線程的安全問題.

    當使用Handler發送消息時,此時的handler對象在另一條線程中還未初始化.所以就報出了空指針異常.

   解決這個問題,系統已經給我們封裝好了一個handlerThread類,使用方法如下:

public  class MainActivity extends Activity {
 
    @Override
    protected void onCreate(BundlesavedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        HandlerThread handlerThread = newHandlerThread("handler thread");
        handlerThread.start();
        Handler handler = newHandler(handlerThread.getLooper()){
            @Override
            public void handleMessage(Messagemsg) {
                Log.e("TAG",Thread.currentThread().toString());
            }
        };
        handler.sendEmptyMessage(1);
    }
}

      這樣就可以實現主線程向子線程發送消息了.

      那麼HandlerThread爲什麼可以解決這一問題呢,查看源碼可以發現,內部採用了wait()和notify()機制來保證getLooper()每次都可以獲得非空的值.

更新UI 的幾種方式

         下面來總結一些Android中更新UI 的幾種方式:下面列舉的幾種方式其實本質都是用Handler實現.

1.     Activity    runOnUiThread();

2.     Handler    post()

3.     Handler    sendMessage()

4.     View      post()

非UI線程真的不能更新UI嗎

      實際上這種說法是不嚴謹的.下面這段代碼實現了在非UI線程更新UI.

@Override
    protected voidonCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
        textView = (TextView)findViewById(R.id.test);
        new Thread(newRunnable() {
            @Override
            public void run(){
               textView.setText("子線程中執行");
            }
        }).start();
}

運行效果:


          看到這裏可能大家的世界觀都崩塌了.一直查看各種資料都是說子線程中無法更新UI,而這裏爲什麼又可以用子線程更新UI呢.要弄明白這個問題,我們就來查看一下更新UI的調用步驟.

  ->android.widget.TextView.setText

   -> android.widget.TextView.checkForRelayout

     -> android.view.View.invalidate

       -> android.view.ViewGroup.invalidateChild

         -> android.view.ViewRoot.invalidateChildInParent

           -> android.view.ViewRoot.invalidateChild

             -> android.view.ViewRoot.checkThread

         由調用棧可以看到,經過各種調用最後調用了ViewRoot的checkThread()方法.奧祕就在這裏,因爲ViewRoot的創建時機是在OnResume()方法內.所以在oncreate()時,是不會調用到ViewRoot的checkThread()方法的.也就不會報出異常了.

         可是如果我們讓子線程睡眠2秒再去執行settext方法,那麼就又會報出無法執行在origin線程執行更新UI的操作.

         實際上,如果我們讓子線程也擁有自己的ViewRoot的話,那麼子線程也是可以更新UI的.具體實踐步驟可以參見參考資料的這篇文章,這裏就不詳細展開了.





參考資料: http://blog.csdn.net/imyfriend/article/details/6877959

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章