由runOnUiThread引發(fā)的思考之Toast源碼解析

在子線程中不能直接調(diào)用Toast.makeText(this,title,Toast.LENGTH_SHORT).show();需要放在主線程里面反症。原因我們后面會講梢褐。因此帆谍,我將Toast放在了runOnUiThread方法中(此方法寫在一個我測試的Activity里面)饼酿。

public void shotToast(final String title) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(this,title,Toast.LENGTH_SHORT).show();
                    }
                });
            }
        }).start();
    }

但是這段話編譯出錯了运准。錯在哪里了呢吭敢?就錯在makeText的第一個參數(shù)上面碰凶。我這java基礎(chǔ)真不扎實呀。Activity中的匿名類或者內(nèi)部類中不能直接用this鹿驼,因為此時this指的是這個內(nèi)部類欲低,所以要訪問外部類的Activity就要用Activity.this,如果不是在內(nèi)部類中畜晰,就直接用this砾莱。

我們還可以把上面的this改成getBaseContext()。點進getBaseContext()方法看到這個方法在ContextWrapper中是這樣的:

/**
     * @return the base context as set by the constructor or setBaseContext
     */
    public Context getBaseContext() {
        return mBase;
    }

然后我就疑惑凄鼻,mBase就一定有值嗎腊瑟?
看過我寫的另一篇文章Context及其子類源碼分析(一)會知道Activity繼承自ContextThemeWrapper,ContextThemeWrapper繼承自ContextWrapper块蚌。ContextWrapper持有一個抽象構(gòu)件的引用闰非,就是代碼里的mBase,真正的ContextImpl的實例會在Activity實例被創(chuàng)建后被創(chuàng)建峭范,然后通過Activity的attach方法賦給mBase财松。所以正常ContextImpl的實例被創(chuàng)建后,mBase是有值的纱控。

上面我們講了兩個事實:
1辆毡、匿名類或內(nèi)部類中用this指的是這個匿名類或內(nèi)部類。
2甜害、Activity中ContextImpl的實例被創(chuàng)建后會通過attach方法傳入ContextWrapper舶掖。

接著,我又提出了疑問——為什么Toast不能直接在子線程中使用唾那?唯有看源碼來找答案了访锻。點進Toast的makeText方法可以看到:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        Toast result = new Toast(context);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);
        
        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

沒發(fā)現(xiàn)什么特別的,不過我點進Toast的構(gòu)造方法看了看:

    public Toast(Context context) {
        mContext = context;
        //TN是什么東西闹获?
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }

發(fā)現(xiàn)一個我沒見過的TN類期犬,不過我很快就轉(zhuǎn)去show方法里看源碼:

public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        //又出現(xiàn)了TN這個類
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
             //注意這行
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

又出現(xiàn)了TN這個類,但正常情況下我們會先關(guān)注service.enqueueToast(pkg, tn, mDuration);這行代碼避诽。點進getService()看到:

static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

看到Stub.asInterface龟虎,我們知道這是利用Binder進行跨進程調(diào)用了。百度上看到說INotificationManager service = getService()返回的就是NotificationManagerService沙庐,我也不知道怎么根據(jù)源碼來確定是NotificationManagerService類實現(xiàn)了enqueueToast()方法鲤妥,姑且就相信吧佳吞。功力有待加強。好慘好氣棉安。

而TN類就是遵循AIDL的實現(xiàn)底扳。接著我們看看TN類。TN類內(nèi)部使用Handler機制贡耽,post一個mShow和mHide:

private static class TN extends ITransientNotification.Stub {
        //省略部分代碼
        /**
         * schedule handleShow into the right thread
         */
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            //下方有mShow的代碼
            mHandler.post(mShow);
        }

        /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }

        //mShow的代碼
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don't do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };
    }

快速瀏覽一下handleShow()方法的實現(xiàn)衷模。通過WindowManager的addView方法實現(xiàn)Toast的添加。其中trySendAccessibilityEvent()方法會把當(dāng)前的類名蒲赂、應(yīng)用的包名通過AccessibilityManager來做進一步的分發(fā)阱冶,以供后續(xù)的處理。代碼就不貼了滥嘴。

public void handleShow() {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                // We can resolve the Gravity here by using the Locale for getting
                // the layout direction
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                    mParams.horizontalWeight = 1.0f;
                }
                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                    mParams.verticalWeight = 1.0f;
                }
                mParams.x = mX;
                mParams.y = mY;
                mParams.verticalMargin = mVerticalMargin;
                mParams.horizontalMargin = mHorizontalMargin;
                mParams.packageName = packageName;
                mParams.removeTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }

看到這里木蹬,我們其實可以知道為了讓設(shè)備顯示出Toast,TN類的show()方法一定是在哪里被調(diào)起過若皱。我們關(guān)鍵要找一下到底哪里被調(diào)到镊叁。我Ctrl+F搜了一下Toast類,并沒有看到show()哪里被調(diào)用是尖,倒是發(fā)現(xiàn)hide()又被調(diào)用過意系。

于是乎我只好回過頭看看NotificationManagerService類里的enqueueToast()方法,發(fā)現(xiàn)答案就在這個方法里面!

public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            //省略部分代碼
             // (1) 判斷是否為系統(tǒng)Toast饺汹。如果當(dāng)前Toast所屬的進程的包名為“android”蛔添,則為系統(tǒng)Toast,
             //否則還可以調(diào)用isCallerSystem()方法來判斷兜辞。
            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
 
            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    int index = indexOfToastLocked(pkg, callback);
                    // If it's already in the queue, we update it in place, we don't
                    // move it to the end of the queue.
                    //如果它已經(jīng)在隊列中迎瞧,我們更新它,而不是將它放在隊列后頭
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        // Limit the number of toasts that any given package except the android
                        // package can enqueue.  Prevents DOS attacks and deals with leaks.
                        //限制toast的數(shù)量
                        //如果是非系統(tǒng)的Toast(通過應(yīng)用包名進行判斷)逸吵,且Toast的總數(shù)大于等于50凶硅,不再把新的Toast放入隊列。
                        //MAX_PACKAGE_NOTIFICATIONS的值為50
                        if (!isSystemToast) {
                            int count = 0;
                            final int N = mToastQueue.size();
                            for (int i=0; i<N; i++) {
                                 final ToastRecord r = mToastQueue.get(i);
                                 if (r.pkg.equals(pkg)) {
                                     count++;
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

                        record = new ToastRecord(callingPid, pkg, callback, duration);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        //通過keepProcessAliveLocked(callingPid)方法來設(shè)置對應(yīng)的進程為前臺進程扫皱,保證不被銷毀足绅。保證不會系統(tǒng)殺死。這也就解釋了為什么當(dāng)我們finish當(dāng)前Activity時韩脑,Toast還可以顯示氢妈,因為當(dāng)前進程還在執(zhí)行。
    (4) index為0時段多,對隊列頭的Toast進行顯示首量。源碼如下:
                        keepProcessAliveLocked(callingPid);
                    }
                    // If it's at index 0, it's the current toast.  It doesn't matter if it's
                    // new or just been updated.  Call back and tell it to show itself.
                    // If the callback fails, this will remove it from the list, so don't
                    // assume that it's valid after this.
                    //如果index = 0,說明Toast就處于隊列的頭部,直接進行顯示加缘。
                    if (index == 0) {
                        //注意這里鸭叙!點進去看看有沒有我們要的show()方法調(diào)用!
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

辛苦了各位拣宏,看到了這里沈贝,最后我們點進showNextToastLocked()方法看看:

void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                //我在這我在這!看我看我J唇W撼獭!
                record.callback.show();
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
                Slog.w(TAG, "Object died trying to show notification " + record.callback
                        + " in package " + record.pkg);
                // remove it from the list and let the process die
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

終于找到show()方法被調(diào)用的地方啦市俊,差點我就放棄啦。好像網(wǎng)上并沒有很多人分析過Toast源碼滤奈,估計是司空見慣摆昧,或者說覺得太容易了吧,但我覺得多看點源碼還是好的蜒程。希望能因此提高自己的閱讀源碼能力绅你。

所以 ,為什么Toast不能直接在子線程中顯示昭躺,要用runOnUiThread處理一下忌锯?

Handler不能在子線程里運行的,因為子線程沒有創(chuàng)建Looper.prepare(); 所以就報錯了领炫。主線程不需要調(diào)用偶垮,是因為主線程已經(jīng)默認(rèn)幫你調(diào)用了。

除了runOnUiThread外帝洪,還有一種解決方案就是缺什么補什么——

new Thread(){
        @Override
        public void run() {
            super.run();
            Looper.prepare();
            try {
                Toast.makeText(MainActivity.this,title,Toast.LENGTH_SHORT).show();
            }catch (Exception e) {
                Logger.e("error",e.toString());
            }
            Looper.loop();
        }
    }.start();

最后似舵,用一張序列圖展示整個調(diào)用流程:



再文字講兩句:

  1. 如果當(dāng)前Toast所屬的進程的包名為“android”,則為系統(tǒng)Toast葱峡,否則還可以調(diào)用isCallerSystem()方法來砚哗;判斷當(dāng)前Toast所屬進程的uid是否為SYSTEM_UID、0砰奕、PHONE_UID中的一個蛛芥,如果是,則為系統(tǒng)Toast;如果不是军援,則不為系統(tǒng)Toast仅淑。 是否為系統(tǒng)Toast。系統(tǒng)Toast一定可以進入到系統(tǒng)Toast隊列中盖溺,不會被黑名單阻止漓糙。系統(tǒng)Toast在系統(tǒng)Toast隊列中沒有數(shù)量限制,而普通pkg所發(fā)送的Toast在系統(tǒng)Toast隊列中有數(shù)量限制烘嘱。

  2. 查看將要入隊的Toast是否已經(jīng)在系統(tǒng)Toast隊列中昆禽。這是通過比對pkg和callback來實現(xiàn)的蝗蛙。

  3. 將當(dāng)前Toast所在進程設(shè)置為前臺進程,這里的mAm=ActivityManagerNative.getDefault(),調(diào)用了setProcessForeground方法將當(dāng)前pid的進程置為前臺進程醉鳖,保證不會系統(tǒng)殺死捡硅。這也就解釋了為什么當(dāng)我們finish當(dāng)前Activity時,Toast還可以顯示盗棵,因為當(dāng)前進程還在執(zhí)行壮韭。

參考地址:徹底理解Toast原理和解決小米MIUI系統(tǒng)上沒法彈Toast的問題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市纹因,隨后出現(xiàn)的幾起案子喷屋,更是在濱河造成了極大的恐慌,老刑警劉巖瞭恰,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屯曹,死亡現(xiàn)場離奇詭異,居然都是意外死亡惊畏,警方通過查閱死者的電腦和手機恶耽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颜启,“玉大人偷俭,你說我怎么就攤上這事$终担” “怎么了涌萤?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長乳规。 經(jīng)常有香客問我形葬,道長,這世上最難降的妖魔是什么暮的? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任笙以,我火速辦了婚禮,結(jié)果婚禮上冻辩,老公的妹妹穿的比我還像新娘猖腕。我一直安慰自己,他們只是感情好恨闪,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布倘感。 她就那樣靜靜地躺著,像睡著了一般咙咽。 火紅的嫁衣襯著肌膚如雪老玛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天,我揣著相機與錄音蜡豹,去河邊找鬼麸粮。 笑死,一個胖子當(dāng)著我的面吹牛镜廉,可吹牛的內(nèi)容都是我干的弄诲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼娇唯,長吁一口氣:“原來是場噩夢啊……” “哼齐遵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起塔插,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤梗摇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后佑淀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體留美,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年伸刃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逢倍。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡捧颅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出较雕,到底是詐尸還是另有隱情碉哑,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布亮蒋,位于F島的核電站扣典,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏慎玖。R本人自食惡果不足惜贮尖,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望趁怔。 院中可真熱鬧湿硝,春花似錦、人聲如沸润努。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铺浇。三九已至痢畜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丁稀。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工吼拥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人二驰。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓扔罪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親桶雀。 傳聞我的和親對象是個殘疾皇子矿酵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,111評論 25 707
  • Toast簡介 官方介紹:A toast provides simple feedback about an op...
    Viking_Den閱讀 1,780評論 0 2
  • 前言:toast再常見不過,但是一個小小的toast居然內(nèi)有乾坤矗积,呵(w)呵(t)呵(f) 源碼如下: publi...
    super超_9754閱讀 1,321評論 0 0
  • 本文出自 Eddy Wiki 全肮,轉(zhuǎn)載請注明出處:http://eddy.wiki/interview-androi...
    eddy_wiki閱讀 3,267評論 0 20
  • 雪花還沒有來得及片片融化 云帆還沒有來得及層層展開 而我還沒有來得及細(xì)細(xì)妝容 她卻像夢幻一般出現(xiàn) 出現(xiàn)在我的視線里...
    徐尋香閱讀 254評論 1 1