handler機制

希望曾經(jīng)堅持的努力不會被辜負

1.handler 介紹

handler 的正常工作需要Looper 否則會報錯(下面會從源碼知悉報錯的原因)
Handler發(fā)送消息的過程是向消息隊列MessageQueue 插入了一條消息,MessageQueue 是由Looper 負責(zé)維護的MessageQueue 的next 方法返回這條消息給當前線程的Looper,Looper 收到消息后開始處理龙誊,最終消息由Looper交個Handler處理,即Handler的dispatchMessage()方法被調(diào)用,最后回調(diào)到handler的 handleMessage中進行處理

在android中汰聋,非UI線程中是不能更新UI的门粪,如果在子線程中做UI相關(guān)操作,可能會出現(xiàn)程序崩潰烹困。一般的做法是玄妈,創(chuàng)建一個Message對象,Handler發(fā)送該message髓梅,給handler傳遞主線程的Looper,將線程切回主線程就可以在Handler的handleMessage()方法中做ui相關(guān)操作
handler 主要有兩個功能:
1.刷新UI拟蜻,(需要用主線程的looper)
2.不刷新ui,只是處理消息

1.1 使用handler刷新UI

主線程中創(chuàng)建Handler,接受各線程發(fā)送來的消息進行UI操作我們使用的比較多枯饿,不做過多說明

    Handler handler = new Handler(){
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    switch (msg.what){
                        case  0:
                            btn.setText("更新");
                            //ToastUtils.showShort("---handler()----");
                            LogUtils.e("----handler-----Thread.currentThread="+Thread.currentThread().getName());
                            break;
                        default:
                            break;
                    }

                }
            };  

    new Thread(new Runnable() {
     @Override public void run() {
      Message message = handler1.obtainMessage();
       message.arg1 = 1;
        handler1.sendMessage(message);
         }
      }).start();

在子線程中酝锅,創(chuàng)建Handler 進行UI更新,就特別需要注意,在創(chuàng)建handler 時要傳Looper.getMainLooper(),將線程切回到主線程進行

    new Thread(new Runnable() {
            @Override
            public void run() {
             Handler handler = new Handler(Looper.getMainLooper()){
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        switch (msg.what){
                            case  0:
                                btn.setText("更新")
                                //ToastUtils.showShort("---handler()----");
                                LogUtils.e("----handler-----Thread.currentThread="+Thread.currentThread().getName());
                                break;
                            default:
                                break;
                        }

                    }
                };
                Message message = new Message();
                message.what = 0;
                handler.sendMessage(message);
            }
        }).start();  
1.2 子線程進行消息處理

子線程中只進行消息處理時奢方,不更新UI 時
(1) 初始化handler可以同上傳Looper.getMainLooper()切到主線程
(2) 也可以不傳Looper.getMainLooper()搔扁,但是必須在子線程中新建一個Looper 對象

    new Thread(new Runnable() {
        @Override
        public void run() {
            LogUtils.e("1111-----Thread.currentThread="+Thread.currentThread().getName());
            Looper.prepare();
            Handler handler = new Handler(){
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    switch (msg.what){
                        case  0:
                            LogUtils.e("22--Thread.currentThread="+Thread.currentThread().getName());
                            break;
                    }
                }
            };
            Message message = new Message();
            message.what = 0;
            handler.sendMessage(message);
            Looper.loop();
        }
    }).start();

如果子線程中創(chuàng)建handler 不調(diào)用Looper.prepare(),會提示運行時異常:
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
查看handler源碼可知

public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

public static @Nullable Looper myLooper() {
      return sThreadLocal.get();
  }

sThreadLocal.get()是空的爸舒,而 Looper.prepare()就完成了這個設(shè)置操作

public static void prepare() {
    prepare(true);
     }
private static void prepare(boolean quitAllowed) {
 if (sThreadLocal.get() != null) {
  throw new RuntimeException("Only one Looper may be created per thread");
   }
    sThreadLocal.set(new Looper(quitAllowed));
     }

主線程為什么不需要調(diào)用Looper.prepare()? 因為在ActivityThread中 會默認創(chuàng)建Looper對象

1.3 子線程真的不能更新UI嗎

我們試著在activity,onCreate()方法中啟動這個線程

new Thread(new Runnable() {
        @Override
        public void run() {
            LogUtils.e("1111-----Thread.currentThread="+Thread.currentThread().getName());
            Looper.prepare();
            Handler handler = new Handler(){
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    switch (msg.what){
                        case  0:
                            btnLogin.setText("WTF");
                            //ToastUtils.showShort("---handler()----");
                            LogUtils.e("22--Thread.currentThread="+Thread.currentThread().getName());
                            break;
                    }
                }
            };
            Message message = new Message();
            message.what = 0;
            handler.sendMessage(message);
            Looper.loop();

        }
    }).start();

運行后發(fā)現(xiàn)也沒有出現(xiàn)崩潰現(xiàn)象,難道我們就可以下結(jié)論子線程也可以更新UI 了么

其實不然

我們試著在更新UI前讓子線程休眠200ms ,發(fā)現(xiàn)出現(xiàn)崩潰了稿蹲,或者我們把這段代碼放在Button 的點擊事件中去處理扭勉,也會發(fā)生崩潰!崩潰日志是這樣的:

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

那我們就要從這段崩潰日志中找原因苛聘,跟蹤源碼涂炎,我們發(fā)現(xiàn)ViewRootImpl類中會有一個checkThread()方法來判斷當前訪問UI的線程是不是主線程,因為mThread這個主線程是在app程序啟動時就初始化的

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

其中 mThread 是創(chuàng)建 ViewRootImpl 的線程设哗,而ViewRootImpl是在主線程中創(chuàng)建的唱捣,所以,我們習(xí)慣地稱它為主線程熬拒,mThread和當前代碼運行的線程來做了個等式運算爷光,相同就出錯,也就是說澎粟,并不是子線程不能刷新UI蛀序,準確來說,是發(fā)送進行 UI 刷新消息的消息活烙,因為真正的底層刷新也不是當前 APP 的主線程徐裸。而是限制了,如果當ViewRootImpl是由子線程創(chuàng)造的啸盏,那么就可以在該子線程中發(fā)送更新UI的消息重贺,自然地就能更新了,那么為什么限制呢?

這背后是線程同步的開銷問題回懦。顯然兩個線程同時繪制屏幕气笙,屏幕會顯示一些意想不到的圖像,所以兩個線程不能同時去更新Ui怯晕。需要互斥潜圃,比如鎖。結(jié)果就是同一時刻只有一個線程可以做ui舟茶。那么當兩個線程互斥幾率較大時谭期,或者互斥的代碼很復(fù)雜,選擇其中一個長期持有其他發(fā)消息就是典型的解決方案吧凉。所以普遍的要求ui只能單線程操作

在ActivityThread中隧出,我們找到handleResumeActivity方法,跟進這個方法我們會發(fā)現(xiàn) activity就是在這里面調(diào)用onResume()生命周期的阀捅,這方法之后胀瞪,有一個makeVisible()方法;

r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
    if (r.activity.mVisibleFromClient) {
        r.activity.makeVisible();
}

跟進去,我們發(fā)現(xiàn)饲鄙,這里是activity 讓view顯示的地方

void makeVisible() {
 if (!mWindowAdded) {
  ViewManager wm = getWindowManager();
   wm.addView(mDecor, getWindow().getAttributes());
    mWindowAdded = true;
     }
      mDecor.setVisibility(View.VISIBLE);
       }

往WindowManager中添加DecorView赏廓,那現(xiàn)在應(yīng)該關(guān)注的就是WindowManager的addView方法了涵紊。而WindowManager是一個接口來的,
我們應(yīng)該找到WindowManager的實現(xiàn)類才行幔摸,而WindowManager的實現(xiàn)類是WindowManagerImpl摸柄,找到了WindowManagerImpl的addView方法,
如下:

@Override 
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

里面調(diào)用了WindowManagerGlobal的addView方法既忆,那現(xiàn)在就鎖定WindowManagerGlobal的addView方法
我們發(fā)現(xiàn)

public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow) {
    ...
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams); mViews.add(view);
    mRoots.add(root); mParams.add(wparams);
    ...
    }

ViewRootImpl是在WindowManagerGlobal的addView方法中創(chuàng)建的驱负。
所以我們可以總結(jié)一下,我們在onCreate()或者onResume()中使用子線程更新UI時患雇,由于viewRootImpl 還未創(chuàng)建完成跃脊,所以并沒有報錯,我們延時200ms 后苛吱,ViewRootImpl已經(jīng)創(chuàng)建了酪术,可以執(zhí)行checkThread方法檢查當前線程。
所以翠储,系統(tǒng)考慮資源調(diào)度及線程開銷上绘雁,通過viewRootImpl線程判斷的方式來讓UI線程(長期持有發(fā)送消息進行UI更新操作的線程)來進行UI更新操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末援所,一起剝皮案震驚了整個濱河市庐舟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌住拭,老刑警劉巖挪略,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異滔岳,居然都是意外死亡杠娱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門谱煤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摊求,“玉大人,你說我怎么就攤上這事趴俘《么兀” “怎么了奏赘?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵寥闪,是天一觀的道長。 經(jīng)常有香客問我磨淌,道長疲憋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任梁只,我火速辦了婚禮缚柳,結(jié)果婚禮上埃脏,老公的妹妹穿的比我還像新娘。我一直安慰自己秋忙,他們只是感情好彩掐,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著灰追,像睡著了一般堵幽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上弹澎,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天朴下,我揣著相機與錄音,去河邊找鬼苦蒿。 笑死殴胧,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的佩迟。 我是一名探鬼主播团滥,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼音五!你這毒婦竟也來了惫撰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤躺涝,失蹤者是張志新(化名)和其女友劉穎厨钻,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坚嗜,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡夯膀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了苍蔬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诱建。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖碟绑,靈堂內(nèi)的尸體忽然破棺而出俺猿,到底是詐尸還是另有隱情,我是刑警寧澤格仲,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布押袍,位于F島的核電站,受9級特大地震影響凯肋,放射性物質(zhì)發(fā)生泄漏谊惭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望圈盔。 院中可真熱鬧豹芯,春花似錦、人聲如沸驱敲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽众眨。三九已至木缝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間围辙,已是汗流浹背我碟。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留姚建,地道東北人矫俺。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像掸冤,于是被迫代替她去往敵國和親厘托。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355