Handler 常見打開姿勢(shì)及踩坑分析

1. 引文

handler 基本定義:先直接看看最權(quán)威的官方定義

  • A Handler allows you to send and process {@link Message} and Runnable
  • objects associated with a thread's {@link MessageQueue}. Each Handler
  • instance is associated with a single thread and that thread's message
  • queue. When you create a new Handler it is bound to a {@link Looper}.
  • It will deliver messages and runnables to that Looper's message
  • queue and execute them on that Looper's thread.
  • <p>There are two main uses for a Handler: (1) to schedule messages and
  • runnables to be executed at some point in the future; and (2) to enqueue
  • an action to be performed on a different thread than your own.

大意就是:
Handler 允許與唯一的線程綁定凰兑,主要作用:

  1. 允許延期執(zhí)行任務(wù)
  2. 允許切換線程執(zhí)行任務(wù)

主要API:
post(Runnable)熙暴、
postAtTime(Runnable龙优、long)卫玖、
postDelayed(Runnable绪励、long)庶骄、
sendEmptyMessage(int)、
sendMessage(msg)题暖、
sendMessageAtTime(msg、long)
sendMessageDelayed(msg墩莫、long)方法來完成的芙委。

** 文末會(huì)給簡(jiǎn)單的demo示例 **

2. 先看常見錯(cuò)誤

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.tv);
        Log.d(TAG, "1--》" + Thread.currentThread().getId());

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    Log.e(TAG, e.toString());
                }
                tv.setText("oncreate方法,無耗時(shí)狂秦,改變Tv 文本");

            }
        }).start();
    }

如果手抖寫出上述代碼灌侣,那恭喜你,喜提下列報(bào)錯(cuò):

 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
        at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1083)
        at android.view.ViewGroup.invalidateChild(ViewGroup.java:5205)
        at android.view.View.invalidateInternal(View.java:13656)
        at android.view.View.invalidate(View.java:13620)

這個(gè)比較簡(jiǎn)單裂问,子線程不允許更新Ui (really?)

3. 正常打開姿勢(shì)

android更新UI的姿勢(shì)有:
runOnUiThread();
handler.post();
handler.sendMessage();
view.post();
簡(jiǎn)單的demo界面


image.png

3.1 runOnUiThread

Activity中的API ,使用簡(jiǎn)單粗暴侧啼,不過必須在Activity的上下文環(huán)境使用

 public void runOnUIBtnOnclick(View view) {
     runOnUiThread(new Runnable() {  // Activity 方法牛柒,只能在Activity中使用
         @Override
         public void run() {
             tv.setText("runOnUiThread 改變Tv 文本"); 
         }
     });
    }

3.2 handler.post()

使用比較方便,主要用來切換線程痊乾。
基本的使用

 Handler handler = new Handler(Looper.myLooper());
 handler.post(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText("切換線程皮壁,改變Tv 文本"); 
                    }
                });

來看下線程是如何切換的

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.tv);
        Log.d(TAG, "1--》" + Thread.currentThread().getId());
...
 public void changeThreadBtnOnclick(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "2---》" + Thread.currentThread().getId());
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        Log.d(TAG, "3---》" + Thread.currentThread().getId());
                        tv.setText("切換線程,改變Tv 文本"); 
                    }
                });
            }
        }).start();
    }

測(cè)試的結(jié)果是:

2020-12-31 20:40:19.544 8170-8170/com.gavin.handlerdemo D/MainActivity: 1--》1
2020-12-31 20:42:15.357 8170-8208/com.gavin.handlerdemo D/MainActivity: 2---》346
2020-12-31 20:42:15.365 8170-8170/com.gavin.handlerdemo D/MainActivity: 3---》1

可以看出哪审,2處的線程的確是子線程蛾魄,3處是主線程
總結(jié)一下:
注意不是寫在子線程方法里的就是子線程執(zhí)行....

3.3 handler.sendMessage()

這個(gè)主要用來傳遞消息,消息常見的做法是子線程向主線程發(fā)送來通知更新UI,不過湿滓,主線程一樣可以向子線程發(fā)消息滴须。
下面造一個(gè)死循環(huán),主線程向子線程發(fā)消息叽奥,子線程收到后扔水,回傳主線程,然后循環(huán)下去朝氓。 這樣演示的目的魔市,是為了警示一下,退出時(shí)赵哲,不反注冊(cè)這個(gè)callback 后果可能很嚴(yán)重待德。

 private static String TAG = "SecActivity";
    TextView tv;
    Handler threadHandler;

    Handler mainHandler = new Handler(Looper.myLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            if (msg.obj != null) {
                Log.e(TAG,  "主線程收到了呼叫, \"" + msg.obj + "\"" );
            } else {
                Log.e(TAG,  "主線程收到了呼叫");
            }
            Message message = new Message();
            message.obj = "這是來自主線程的呼叫!J母汀磅网!";
            if (threadHandler != null) {
                threadHandler.sendMessageDelayed(message, 1000);
            }
        }
      };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sec);
        tv = findViewById(R.id.tv);
        HandlerThread thread = new HandlerThread("HandlerThread"); // 使用HandlerThread主要是方便獲取Looper
        thread.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                threadHandler = new Handler(thread.getLooper()) {
                    @Override
                    public void handleMessage(@NonNull Message msg) {
                        if (msg.obj != null) {
                            Log.e(TAG,  "子線程收到了呼叫, \"" + msg.obj + "\"" );
                        } else {
                            Log.e(TAG,  "子線程收到了呼叫");
                        }
                        Message message = new Message();
                        message.obj = "這是來自子線程的呼叫L附亍?曷拧!";
                        mainHandler.sendMessageDelayed(message,1000);

                    }
                };
            }
        }).start();
    }

    public void testMsgTransform(View view) {
        mainHandler.sendEmptyMessageDelayed(0, 100); // 發(fā)送一個(gè)空消息簸喂,被自身的handleMessage 捕獲毙死。誰發(fā)送 誰接收。
    }

得到的結(jié)果:

2020-12-31 20:48:29.521 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫
2020-12-31 20:48:30.523 8170-8221/com.gavin.handlerdemo E/SecActivity: 子線程收到了呼叫, "這是來自主線程的呼叫S黯6筇取!"
2020-12-31 20:48:31.526 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫, "這是來自子線程的呼叫3恰T倬铡!"
2020-12-31 20:48:32.527 8170-8221/com.gavin.handlerdemo E/SecActivity: 子線程收到了呼叫, "這是來自主線程的呼叫Q赵>腊巍!"
2020-12-31 20:48:33.529 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫, "這是來自子線程的呼叫7汉馈3砘濉侦鹏!"
2020-12-31 20:48:34.531 8170-8221/com.gavin.handlerdemo E/SecActivity: 子線程收到了呼叫, "這是來自主線程的呼叫!M涡稹略水!"
2020-12-31 20:48:35.533 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫, "這是來自子線程的呼叫!H坝渊涝!"
2020-12-31 20:48:36.537 8170-8221/com.gavin.handlerdemo E/SecActivity: 子線程收到了呼叫, "這是來自主線程的呼叫!4蚕印驶赏!"
2020-12-31 20:48:37.537 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫, "這是來自子線程的呼叫!<染稀煤傍!"

總結(jié)一下:

  1. Handler 可以有很多個(gè),不過每個(gè)Handler 只能綁定一個(gè)線程嘱蛋, 只能有一個(gè)Looper蚯姆,Handler 發(fā)出的消息,最終要由自己的handleMessage 消費(fèi)洒敏,是真正的肥水不流外人田龄恋。

  2. 即使你關(guān)閉了頁面,你會(huì)發(fā)現(xiàn)凶伙,這個(gè)死循環(huán)還在郭毕,它的生命周期是app的生命周期,不關(guān)閉函荣,后果真的很嚴(yán)重显押。
    關(guān)閉方法:

  @Override
    protected void onDestroy() {
        super.onDestroy();
        //如果這里不移除 callback,這個(gè)callback會(huì)一直存在傻挂,容易引發(fā)內(nèi)存泄漏
        mainHandler.removeCallbacksAndMessages(null);
        threadHandler.removeCallbacksAndMessages(null);
    }

3.4 view.post()

  public void viewpostTest(View view) {
        tv.post(new Runnable() {
            @Override
            public void run() {
                tv.setText("view.post方法乘碑,改變Tv 文本");
            }
        });
    }

view.post與Handler.post 基本類似,不過也有所不同金拒,這里不繼續(xù)展開了兽肤。

4. 子線程真的就不能更新Ui 么?

答案是一般都不能更新绪抛,但是面試時(shí)资铡,你就得回答可以更新。幢码。笤休。
可以先驗(yàn)證下效果。

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.tv);
        Log.d(TAG, "1--》" + Thread.currentThread().getId());

         new Thread(new Runnable() {
            @Override
            public void run() {
                tv.setText("oncreate方法蛤育,無耗時(shí)宛官,改變Tv 文本");
            }
        }).start();
    }

可以看出頁面并未報(bào)錯(cuò)葫松,正常展示。
不過如果你加上延遲底洗,就是這么一段話

   new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tv.setText("oncreate方法腋么,無耗時(shí),改變Tv 文本");
            }
        }).start();

就會(huì)喜提上面提到的崩潰了亥揖。
為啥呢珊擂?

看報(bào)錯(cuò)提示:

 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)

這個(gè)報(bào)錯(cuò)是ViewRootImpl 給出的。
流程是這樣:
onreate方法费变,并不會(huì)給出這個(gè)效驗(yàn)摧扇,因此程序正常執(zhí)行,Ui 也確實(shí)更新了挚歧。
但在Activity 的onresume 階段扛稽,會(huì)調(diào)用ViewRootImpl 的方法,然后checkThread去校驗(yàn)這個(gè)更新Ui的線程是否為主線程滑负。
具體的代碼相對(duì)復(fù)雜在张,簡(jiǎn)單講的話,可以看下onresume的注釋


image.png

大意是View 變?yōu)榭梢姇r(shí)矮慕,會(huì)調(diào)用ViewRootImpl performTraversals方法帮匾。

因此如果子線程沒有耗時(shí),初始進(jìn)入時(shí)痴鳄,還沒來的及執(zhí)行onresume中的方法瘟斜,因此在ViewRootImpl checkThread 執(zhí)行前已經(jīng)搶先完成了UI 更新,就不會(huì)有問題痪寻。 但一旦執(zhí)行耗時(shí)方法螺句,就會(huì)觸發(fā)checkThread機(jī)制。

5. 內(nèi)存泄漏問題

以上代碼槽华,僅作為測(cè)試demo是沒有問題的壹蔓。但上線的話,會(huì)存在內(nèi)存泄漏問題猫态。
( 這里假設(shè)ondestroy 時(shí),沒有removeCallbacksAndMessages)
原因主要是:
通過匿名內(nèi)部類實(shí)例化的Handler類對(duì)象披摄,隱式的持有外部Activity對(duì)象亲雪。
Activity退出時(shí),Handler 的生命周期與APP 生命周期相同疚膊,因其持有Activity實(shí)例义辕,導(dǎo)致Activity 也不能被GC 回收,引發(fā)內(nèi)存泄漏寓盗。

解決方案:
通過創(chuàng)建繼承Handler的靜態(tài)內(nèi)部類灌砖,并使用弱引用來避免Handler對(duì)象持有外部類對(duì)象的強(qiáng)引用璧函。

 
    public MyHandler mHandler = new MyHandler(this);
 
    //靜態(tài)內(nèi)部類
    static class MyHandler extends Handler {
 
        private WeakReference<Context> weakReference;
 
        MyHandler(Context context) {
            weakReference = new WeakReference<>(context);
        }
 
     @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity mainActivity = mWeakReference.get();
            if (mainActivity != null) {
                tv.setText(msg.obj + "");
        }

6. demo 地址

https://github.com/jjbheda/handlerDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市基显,隨后出現(xiàn)的幾起案子蘸吓,更是在濱河造成了極大的恐慌,老刑警劉巖撩幽,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件库继,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡窜醉,警方通過查閱死者的電腦和手機(jī)宪萄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來榨惰,“玉大人拜英,你說我怎么就攤上這事±糯撸” “怎么了聊记?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長恢暖。 經(jīng)常有香客問我排监,道長,這世上最難降的妖魔是什么杰捂? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任舆床,我火速辦了婚禮,結(jié)果婚禮上嫁佳,老公的妹妹穿的比我還像新娘挨队。我一直安慰自己,他們只是感情好蒿往,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布盛垦。 她就那樣靜靜地躺著,像睡著了一般瓤漏。 火紅的嫁衣襯著肌膚如雪腾夯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天蔬充,我揣著相機(jī)與錄音蝶俱,去河邊找鬼。 笑死饥漫,一個(gè)胖子當(dāng)著我的面吹牛榨呆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庸队,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼积蜻,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼闯割!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起竿拆,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤宙拉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后如输,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鼓黔,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年不见,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了澳化。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡稳吮,死狀恐怖缎谷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情灶似,我是刑警寧澤列林,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站酪惭,受9級(jí)特大地震影響希痴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜春感,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一砌创、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鲫懒,春花似錦嫩实、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至颂翼,卻和暖如春晃洒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疚鲤。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工锥累, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人集歇。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像语淘,于是被迫代替她去往敵國和親诲宇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子际歼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容