Android基礎夯實--你了解Handler有多少?


概述

對於剛入門的同學來說,往往都會對Handler比較迷茫,到底Handler是個什么樣的東西。當然,可能對於一些有工作經驗的工程師來說,他們也不一定能很准確地描述,我們來看下API的介紹。

Handler是用來結合線程的消息隊列來發送、處理“Message對象”和“Runnable對象”的工具。每一個Handler實例之后會關聯一個線程和該線程的消息隊列。當你創建一個Handler的時候,從這時開始,它就會自動關聯到所在的線程/消息隊列,然后它就會陸續把Message/Runnalbe分發到消息隊列,並在它們出隊的時候處理掉。

從官方文檔中,我們不難找出其中的關鍵詞,就是“線程”。我們都知道,一個涉及到網絡操作,耗時操作等的Android應用,都離不開多線程操作,然而,如果這時我們允許並發更新UI,那么最終導致控件的狀態都是不可確定的。所以,我們可以通過對控件進行加鎖,在不需要用時解鎖,這是一個解決方案之一,但最后很容易造成線程阻塞,效率會非常差。所以,谷歌采用了只允許在主線程更新UI,所以作為線程通信橋梁的Handler也就應運而生了。

Looper、MessageQueue、Message、Handler的關系

講到Handler,肯定離不開Looper、MessageQueue、Message這三者和Handler之間的關系,下面簡略地帶過,詳細自己可以查閱相關資料,或者查看源碼,這樣更方便大家深入學習。

Looper

每一個線程只有一個Looper,每個線程在初始化Looper之后,然后Looper會維護好該線程的消息隊列,用來存放Handler發送的Message,並處理消息隊列出隊的Message。它的特點是它跟它的線程是綁定的,處理消息也是在Looper所在的線程去處理,所以當我們在主線程創建Handler時,它就會跟主線程唯一的Looper綁定,從而我們使用Handler在子線程發消息時,最終也是在主線程處理,達到了異步的效果。

那么就會有人問,為什么我們使用Handler的時候從來都不需要創建Looper呢?這是因為在主線程中,ActivityThread默認會把Looper初始化好,prepare以后,當前線程就會變成一個Looper線程。

MessageQueue

MessageQueue是一個消息隊列,用來存放Handler發送的消息。每個線程最多只有一個MessageQueue。MessageQueue通常都是由Looper來管理,而主線程創建時,會創建一個默認的Looper對象,而Looper對象的創建,將自動創建一個MessageQueue。其他非主線程,不會自動創建Looper。

Message

消息對象,就是MessageQueue里面存放的對象,一個MessageQueu可以包括多個Message。當我們需要發送一個Message時,我們一般不建議使用new Message()的形式來創建,更推薦使用Message.obtain()來獲取Message實例,因為在Message類里面定義了一個消息池,當消息池里存在未使用的消息時,便返回,如果沒有未使用的消息,則通過new的方式創建返回,所以使用Message.obtain()的方式來獲取實例可以大大減少當有大量Message對象而產生的垃圾回收問題。

四者關系總體如下(如有不對的地方,謝謝指出)
image

Handler的主要用途

  1. 推送未來某個時間點將要執行的Message或者Runnable到消息隊列。
  2. 在子線程把需要在另一個線程執行的操作加入到消息隊列中去。

廢話不多說,通過舉例來說明Handler的兩個主要用途。

1. 推送未來某個時間點將要執行的Message或者Runnable到消息隊列

實例:通過Handler配合Message或者Runnable實現倒計時

  • 首先看一下效果圖

image

  • 方法一,通過Handler + Message的方式實現倒計時。代碼如下:
public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding mBinding;

    private Handler mHandler ;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        //設置監聽事件
        mBinding.clickBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //通過Handler + Message的方式實現倒計時
                for (int i = 1; i <= 10; i++) {
                    Message message = Message.obtain(mHandler);
                    message.what = 10 - i;
                    mHandler.sendMessageDelayed(message, 1000 * i); //通過延遲發送消息,每隔一秒發送一條消息
                }
            }
        });

        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                mBinding.time.setText(msg.what + "");   //在handleMessage中處理消息隊列中的消息
            }
        };
    }
}

其實代碼不用怎么解釋,都比較通俗易懂,但是這里用到了DataBiding,可能沒用過的同學看起來有點奇怪,但其實反而簡略了代碼,有一定基礎的同學看起來都不會有太大壓力,所以不做太多解釋。通過這個小程序,作者希望大家可以了解到Handler的一個作用就是,在主線程中,可以通過Handler來處理一些有順序的操作,讓它們在固定的時間點被執行。

  • 方法二,通過Handler + Runnable的方式實現倒計時。代碼如下:
public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding mBinding;
    private Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        //設置監聽事件
        mBinding.clickBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (int i = 1; i <= 10; i++) {
                    final int fadedSecond = i;
                    //每延遲一秒,發送一個Runnable對象
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mBinding.time.setText((10 - fadedSecond) + "");
                        }
                    }, 1000 * i);
                }
            }
        });
    }
}

方法二也是通過代碼讓大家加深Handler處理有序事件的用途,之所以分開Runnable和Message兩種方法來實現,是因為很多人都搞不清楚為什么Handler可以推送Runnable和Message兩種對象。其實,無論Handler將Runnable還是Message加入MessageQueue,最終都只是將Message加入到MessageQueue。只要大家看一下源碼就可以知道,Handler的post Runnable對象這個方法只是對post Message進行了一層封裝,所以最終我們都是通過Handler推送了一個Message罷了,至於為什么會分開兩種方法,下文會給大家詳說究竟。下面再來看看Handler的第二個主要用途。

2. 在子線程把需要在另一個線程執行的操作加入到消息隊列中去

實例:通過Handler + Message來實現子線程加載圖片,在UI線程顯示圖片

  • 效果圖如下

image

  • 代碼如下(布局代碼也不放出來了)

public class ThreadActivity extends AppCompatActivity implements View.OnClickListener {
    private ActivityThreadBinding mBinding = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_thread);
        // 設置點擊事件
        mBinding.clickBtn.setOnClickListener(this);
        mBinding.resetBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            // 響應load按鈕
            case R.id.clickBtn:
                // 開啟一個線程
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // 在Runnable中進行網絡讀取操作,返回bitmap
                        final Bitmap bitmap = loadPicFromInternet();
                        // 在子線程中實例化Handler同樣是可以的,只要在構造函數的參數中傳入主線程的Looper即可
                        Handler handler = new Handler(Looper.getMainLooper());
                        // 通過Handler的post Runnable到UI線程的MessageQueue中去即可
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                // 在MessageQueue出隊該Runnable時進行的操作
                                mBinding.photo.setImageBitmap(bitmap);
                            }
                        });
                    }
                }).start();
                break;
            case R.id.resetBtn:
                mBinding.photo.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.default_pic));
                break;
        }
    }

    /***
     * HttpUrlConnection加載圖片,不多說
     * @return
     */
    public Bitmap loadPicFromInternet() {
        Bitmap bitmap = null;
        int respondCode = 0;
        InputStream is = null;
        try {
            URL url = new URL("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1421494343,3838991329&fm=23&gp=0.jpg");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(10 * 1000);
            connection.setReadTimeout(5 * 1000);
            connection.connect();
            respondCode = connection.getResponseCode();
            if (respondCode == 200) {
                is = connection.getInputStream();
                bitmap = BitmapFactory.decodeStream(is);
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
            Toast.makeText(getApplicationContext(), "訪問失敗", Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return bitmap;
    }
}

Handler推送Message和Runnable的區別

在上文我們通過用Handler推送Message和Runnable實現相同的倒計時效果,這里我們就說一下Post(Runnable)和SendMessage(Message)的區別。

首先我們看看post方法和sendMessage方法的源碼:

    public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }
    public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

可見,兩個方法都是通過調用sendMessageDelayed方法實現的,所以可以知道它們的底層邏輯是一致的。

但是,post方法的底層調用sendMessageDelayed的時候,卻是通過getPostMessage(r)來將Runnable對象來轉為Message,我們點進方getPostMessage()法可以看到:

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

其實,最終runnable最終也是轉化為一個Message,而這個Message只有一個被賦值的成員變量,就是Runnable的回調函數,也就是說,這個Message在進入MessageQueue之后,它只是一個“動作”,即我們Runnbale的run方法里面的操作。

要知道,我們的Message類可是有很多參數的,所以你可以理解為它是一個非常豐富的JavaBean,可以看看它的成員變量:

  • public int what;
  • public int arg1;
  • public int arg2;
  • public Object obj;
  • ...

那么講到這里,大家也應該有所理解為什么Google工程師為什么會封裝這兩種方法,我總結如為:為了更方便開發者根據不同需要進行調用。當我們需要傳輸很多數據時,我們可以使用sendMessage來實現,因為通過給Message的不同成員變量賦值可以封裝成數據非常豐富的對象,從而進行傳輸;當我們只需要進行一個動作時,直接使用Runnable,在run方法中實現動作內容即可。當然我們也可以通過Message.obtain(Handler h, Runnable callback)來傳入callback接口,但這樣看起來就沒有post(Ruannable callback)那么直觀。

API

API是我們學習最好的文檔,所以我也簡要跟大家學習一下,其實大家認真看我上面的介紹加上自己親手實踐,Handler的API大家都可以隨便翻閱了。

構造函數

  • Handler()
  • Handler(Handler.Callback callback):傳入一個實現的Handler.Callback接口,接口只需要實現handleMessage方法。
  • Handler(Looper looper):將Handler關聯到任意一個線程的Looper,在實現子線程之間通信可以用到。
  • Handler(Looper looper, Handler.Callback callback)

主要方法

  • void dispatchMessage (Message msg)

一般情況下不會使用,因為它的底層實現其實是作為處理系統消息的一個方法,如果真要用,效果和sendMessage(Message m)效果一樣。

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            // 如果有Runnbale,則直接執行它的run方法
            handleCallback(msg);
        } else {
            //如果有實現自己的callback接口
            if (mCallback != null) {
                //執行callback的handleMessage方法
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //否則執行自身的handleMessage方法
            handleMessage(msg);
        }
    }
    
    private static void handleCallback(Message message) {
        message.callback.run();
    }
  • void dump (Printer pw, String prefix)

主要Debug時使用的一個方法,dump函數只是使用了Printer對象進行了打印,打印出Handler以及Looper和Queue中的一些信息,源碼如下:

    public final void dump(Printer pw, String prefix) {
        pw.println(prefix + this + " @ " + SystemClock.uptimeMillis());
        // 如果Looper為空,輸出Looper沒有初始化
        if (mLooper == null) {
            pw.println(prefix + "looper uninitialized");
        } else {
            // 否則調用Looper的dump方法,Looper的dump方法也是
            mLooper.dump(pw, prefix + "  ");
        }
    }

通過測試用例大家會了解得更清晰:

        //測試代碼
        Printer pw = new LogPrinter(Log.ERROR, "MyTag");
        mHandler.dump(pw, "prefix");

結果:
image

  • Looper getLooper ()

拿到Handler相關聯的Looper

  • String getMessageName (Message message)

獲取Message的名字,默認名字為message.what的值。

  • void handleMessage (Message msg)

處理消息。

  • boolean hasMessages (int what)

判斷是否有Message的what值為參數what。

  • boolean hasMessages (int what, Object object)

判斷是否有Message的what值為參數what,obj值為參數object。

  • Message obtainMessage (int what, Object obj)

從消息池中拿到一個消息並賦值what和obj,其他重載函數同理。

  • boolean post (Runnable r)

將Runnable對象加入MessageQueue。

  • boolean post (Runnable r)

將Runnbale加入到消息隊列的隊首。但是官方不推薦這么做,因為很容易打亂隊列順序。

  • boolean postAtTime (Runnable r, Object token, long uptimeMillis)

在某個時間點執行Runnable r。

  • boolean postDelayed (Runnable r, long delayMillis)

當前時間延遲delayMillis個毫秒后執行Runnable r。

  • void removeCallbacks (Runnable r, Object token)

移除MessageQueue中的所有Runnable對象。

  • void removeCallbacksAndMessages (Object token)

移除MessageQueue中的所有Runnable和Message對象。

  • void removeMessages (int what)

移除所有what值得Message對象。

  • boolean sendEmptyMessage (int what)

直接拿到一個空的消息,並賦值what,然后發送到MessageQueue。

  • boolean sendMessageDelayed (Message msg, long delayMillis)

在延遲delayMillis毫秒之后發送一個Message到MessageQueue。

Handler引發的內存泄漏

在上面的例子中,為了展示方便,我都沒有考慮內存泄漏的情況,但是在實際開發中,如果不考慮代碼的安全性的話,尤其當一個項目到達了一定的規模之后,那么對於代碼的維護和系統的調試都是非常困難的。而Handler的內存泄漏在Android中也是一個非常經典的案例。

詳細可以參考:How to Leak a Context: Handlers & Inner Classes

或參考翻譯文:Android中Handler引起的內存泄露

通常我們都會在一個Activity內部定義一個Handler的內部類:

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                 ...
            }
        }
    };
    
        @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);


        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                ...
            }
        }, 1000000);

    }
}

(1)外部類Activity中定義了一個非靜態內部類Handler,非靜態內部類默認持有對外部類的引用。如果外部Activity突然關閉了,但是MessageQueue中的消息還沒處理完,那么Handler就會一直持有對外部Activty的引用,垃圾回收器無法回收Activity,從而導致內存泄漏。

(2) 如上代碼,在postDelayed中,我們在參數中傳入一個非靜態內部類Runnable,這同樣會造成內存泄漏,假如此時關閉了Activity,那么垃圾回收器在接下來的1000000ms內都無法回收Activity,造成內存泄漏。

解決方案:

(1) 將非靜態內部類Handler和Runnable轉為靜態內部類,因為非靜態內部類(匿名內部類)都會默認持有對外部類的強引用。

(2) 改成靜態內部類后,對外部類的引用設為弱引用,因為在垃圾回收時,會自動將弱引用的對象回收。

避免內存泄漏的例子:

public class HandlerActivity extends AppCompatActivity {

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            // 操作
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fourth);

        mHandler.postDelayed(mRunnable, 1000*10);
        
        finish();   
    }


    private static class MyHandler extends Handler {
        WeakReference<HandlerActivity> mWeakActivity;

        public MyHandler(HandlerActivity activity) {
            this.mWeakActivity = new WeakReference<HandlerActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            final HandlerActivity mActivity = mWeakActivity.get();
            if (mActivity != null) {
                // 處理消息
            }
        }
    }

}

HandlerThread

思考一下,假如我們需要同時下載A和B,下載A需要6s,下載B需要5s,在它們下載完成后Toast信息出來即可,此時HandlerThread便是一種解決方式之一。那么HandlerThread到底是什么?

  • HandlerThread就是一種線程。
  • HandlerThread和普通的Thread之間的區別就是HandlerThread在創建的時候會提供自己該線程的Looper對象。

所以,如果大家了解清楚了我前面所講的Looper、Message、Handler、MessageQueue的關系的話,這里就很清楚HandlerThread是什么東西了。大家都知道,我們在Actvity創建時系統會自動幫我們初始化好主線程的Looper,然后這個Looper就會管理主線程的消息隊列。但是在我們創建子線程時,系統並不會幫我們創建子線程的Looper,需要我們自己手動創建,如下:

    new Thread(){
        @Override
        public void run() {
            super.run();
            Looper.prepare();
            Handler mHandler = new Handler(Looper.myLooper());
            Looper.loop();
        }
    }.start();

所以HandlerThread就在內部幫我們封裝了Looper的創建過程,從源碼可以看到,HandlerThread集成於Thread,然后覆寫run方法,進行Looper的創建,從而通過getLooper方法暴露出該線程的Looper對象

public class HandlerThread extends Thread {
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    
    ...
    
    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
    
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }
    
    ...
}    

所以通過HandlerThread,我們可以輕松創建一個包含了Looper的子線程:

final HandlerThread mHandlerThread = new HandlerThread("HandlerThread");

mHandlerThread.start();

Handler mHandler = new Handler(mHandlerThread.getLooper());

使用HandlerThread同時下載A和B的Demo:

image

代碼:

public class HandlerThreadActivity extends AppCompatActivity {
    private TextView tv_A, tv_B;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_thread);

        tv_A = (TextView) findViewById(R.id.txt_dlA);
        tv_B = (TextView) findViewById(R.id.txt_dlB);

        final Handler mainHandler = new Handler();

        final HandlerThread downloadAThread = new HandlerThread("downloadAThread");
        downloadAThread.start();
        Handler downloadAHandler = new Handler(downloadAThread.getLooper());

        // 通過postDelayed模擬耗時操作
        downloadAHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), "下載A完成", Toast.LENGTH_SHORT).show();
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        tv_A.setText("A任務已經下載完成");
                    }
                });
            }
        }, 1000 * 5);


        final HandlerThread downloadBThread = new HandlerThread("downloadBThread");
        downloadBThread.start();
        Handler downloadBHandler = new Handler(downloadBThread.getLooper());

        // 通過postDelayed模擬耗時操作
        downloadBHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), "下載B完成", Toast.LENGTH_SHORT).show();
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        tv_B.setText("B任務已經下載完成");
                    }
                });

            }
        }, 1000 * 7);
    }
}

總結

由於Android的UI更新只能在主線程,所以Handler是Android中一套非常重要的更新UI線程機制,雖然在很多框架的幫助下我們可以減少了很多Handler的代碼編寫,但實際上很多框架的底層實現都是通過Handler來更新UI的,所以可見掌握好Handler對我們來說是多么重要,所以這也是很多面試官在面試中的高頻考點之一。雖然Handler對開發者來說是一個非常方便的存在,但是我們也不能否認它也是存在缺點的,如處理不當,Handler所造成的的內存泄漏對開發者來說也是一個非常頭疼的難題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM