【W(wǎng)indow系列】——Toast源碼解析

本系列博客基于android-28版本
【W(wǎng)indow系列】——Toast源碼解析
【W(wǎng)indow系列】——PopupWindow的前世今生
【W(wǎng)indow系列】——Dialog源碼解析
【W(wǎng)indow系列】——Window中的Token

前言

Toast組件應(yīng)該是接觸Android中使用率非常高的一個(gè)原生控件,其使用的便捷性一直是開發(fā)者選用的原因,短短的一行代碼就可以實(shí)現(xiàn)支持跨頁面的提示功能。但是隨著Google對于Android系統(tǒng)自身安全性的限制瞬矩,導(dǎo)致Toast組件目前在高版本上也出現(xiàn)了許多問題,例如當(dāng)關(guān)閉應(yīng)用的通知欄權(quán)限墓贿,全局的Toast就無法展示了祥款。本期博客就先從源碼角度分析Toast的實(shí)現(xiàn)原理较锡,只有了解了Toast的實(shí)現(xiàn)原理,才能想辦法解決問題伏钠。

源碼解析

我們使用Toast一般的使用方式如下:
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
所以我們來分別看一下兩個(gè)方法横漏。

/**
     * Make a standard toast that just contains a text view.
     *
     * @param context  The context to use.  Usually your {@link android.app.Application}
     *                 or {@link android.app.Activity} object.
     * @param text     The text to show.  Can be formatted text.
     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
     *                 {@link #LENGTH_LONG}
     *
     */
    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
    }
    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);

        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;
    }

這里有兩個(gè)注意點(diǎn):
1.可以看到這里注釋寫到了,延時(shí)duration只能是變量LENGTH_SHORTLENGTH_LONG具體原因后面源碼分析到再看熟掂。
2.我們每次使用Toast都會(huì)new一個(gè)新的Toast對象缎浇,而這個(gè)布局就是一個(gè)transient_notification.xml文件
現(xiàn)在首先來看一下Toast的構(gòu)造函數(shù)

public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);
        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);
    }

可以看到這里創(chuàng)建了一個(gè)TN對象,這個(gè)TN后面會(huì)貫穿整個(gè)Toast的使用全過程赴肚,所以我們先看一下這是個(gè)什么對象素跺。

private static class TN extends ITransientNotification.Stub {
    TN(String packageName, @Nullable Looper looper) {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            //type為TYPE_TOAST類型
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

            mPackageName = packageName;

            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                //獲取Looper對象
                looper = Looper.myLooper();
                //如果自線程,沒有創(chuàng)建Looper對象誉券,則拋異常
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            //創(chuàng)建Handler對象
            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
        }
}

首先可以看到這個(gè)TN對象繼承ITransientNotification.Stub指厌,看到這個(gè)名字,如果了解過AIDL機(jī)制的話横朋,或者了解過Binder機(jī)制的仑乌,應(yīng)該對這個(gè)名字很熟悉百拓,這個(gè)不就是AIDL的實(shí)現(xiàn)類琴锭,所以可以看出Toast機(jī)制的底層實(shí)現(xiàn)肯定用到了Binder機(jī)制⊙么可以看到這里面有兩個(gè)方法被@Override標(biāo)記决帖,show()方法和hide()方法,這不是正好和我們的顯示和隱藏對應(yīng)嗎蓖捶。
這里我注釋著重寫了幾個(gè)點(diǎn)
1.首先可以看到這里創(chuàng)建了WindowManager.LayoutParams對象地回,并且設(shè)置了一系列熟悉,其中比較重要的一個(gè)是俊鱼,這里設(shè)置了一個(gè)type屬性為TYPE_TOAST刻像,這個(gè)標(biāo)記了這個(gè)Window的類型,而關(guān)閉通知欄權(quán)限導(dǎo)致Toast無法展示也是和這個(gè)屬性有關(guān)并闲,不影響本次原理分析细睡,所以暫不分析。
2.獲取Looper對象帝火,如果屬性Handler機(jī)制的話溜徙,應(yīng)該看到這個(gè)方法很熟悉,Looper.myLooper()這個(gè)方法底層利用ThreadLocal獲取Looper對象犀填,而一般我們使用Toast都是在主線程使用蠢壹,主線程的main方法,已經(jīng)自動(dòng)完成了Looper.prepare()方法和Looper.loop()方法九巡,
所以已經(jīng)自動(dòng)完成了Looper的創(chuàng)建图贸。這里可以看到,如果沒有獲取到Looper對象,則會(huì)拋出異常疏日。所以這里我們也可以對應(yīng)分析一個(gè)問題:

自線程使用Toast對象會(huì)怎么樣乏盐?

如果熟悉Handler機(jī)制的話,應(yīng)該立馬能得出答案制恍,當(dāng)然是崩潰了父能,猶豫創(chuàng)建出來的自線程沒有創(chuàng)建Looper對象,所以這里無法獲取到Looper對象净神,那么就會(huì)拋異常何吝,導(dǎo)致崩潰。

那么自線程如何使用Toast呢鹃唯?

還是Handler機(jī)制爱榕,既然沒有Looper機(jī)制,那么就創(chuàng)建咯

new Thread(){
        public void run(){
          Looper.prepare();//給當(dāng)前線程初始化Looper
          Toast.makeText(getApplicationContext(),"自線程Toast",0).show();//Toast初始化的時(shí)候會(huì)new Handler();無參構(gòu)造默認(rèn)獲取當(dāng)前線程的Looper坡慌,如果沒有prepare過黔酥,則拋出題主描述的異常。上一句代碼初始化過了洪橘,就不會(huì)出錯(cuò)跪者。
          Looper.loop();//這句執(zhí)行,Toast排隊(duì)show所依賴的Handler發(fā)出的消息就有人處理了熄求,Toast就可以吐出來了渣玲。但是,這個(gè)Thread也阻塞這里了弟晚,因?yàn)閘oop()是個(gè)for (;;) ...
        }
  }.start();

3.后面就創(chuàng)建了Handler對象忘衍,所以如果是常規(guī)情況,那么在Handler中執(zhí)行的應(yīng)該是主線程的方法卿城。

看完了構(gòu)造函數(shù)枚钓,現(xiàn)在我們就來看一下Toast的show()方法

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

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

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

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

果然和上面分析的一樣,這里首先利用Binder獲取了NotificationManagerService的代理瑟押,然后調(diào)用了它的enqueueToast()方法搀捷,注意這里將剛才創(chuàng)建的TN對象傳了過去,果然是利用了Binder勉耀,雙向通信指煎。

private final IBinder mService = new INotificationManager.Stub() {
        // Toasts
        // ============================================================================

        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            if (DBG) {
                Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
                        + " duration=" + duration);
            }

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }
            final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
            final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());

            if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
                    (!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
                            || isPackageSuspended)) {
                Slog.e(TAG, "Suppressing toast from package " + pkg
                        + (isPackageSuspended
                                ? " due to package suspended by administrator."
                                : " by user request."));
                return;
            }

            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    int index;
                    // All packages aside from the android package can enqueue one toast at a time
                    //是否是系統(tǒng)應(yīng)用
                    if (!isSystemToast) {
                        index = indexOfToastPackageLocked(pkg);
                    } else {
                        index = indexOfToastLocked(pkg, callback);
                    }

                    // If the package already has a toast, we update its toast
                    // in the queue, we don't move it to the end of the queue.
                    if (index >= 0) {
                    //如果當(dāng)前隊(duì)列里已經(jīng)有Toast,直接更新
                        record = mToastQueue.get(index);
                        record.update(duration);
                        try {
                            record.callback.hide();
                        } catch (RemoteException e) {
                        }
                        record.update(callback);
                    } else {
                    //沒有便斥,則創(chuàng)建新的ToastRecord
                        Binder token = new Binder();
                    //生成一個(gè)Toast窗口至壤,并且傳遞token等參數(shù)
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        //生產(chǎn)一個(gè)ToastRecord
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        //將Toast加入隊(duì)列
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                    }
                    //設(shè)置當(dāng)前進(jìn)程為前臺(tái)進(jìn)程
                    keepProcessAliveIfNeededLocked(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.
                    if (index == 0) {
                    //如果當(dāng)前Toast為隊(duì)頭,則顯示Toast
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

可以看到枢纠,果然利用了Binder像街,這里首先用isSystemToast判斷了是否是系統(tǒng)應(yīng)用

final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));

可以看到這里,兩個(gè)判斷條件一個(gè)是通過進(jìn)程Id判斷是否是系統(tǒng)進(jìn)程,一個(gè)是通過包名判斷是否"android"镰绎,所以后面會(huì)的博客會(huì)介紹一種通過偽造包名的方式脓斩,以系統(tǒng)Toast的方式彈Toast。
后面在定位Toast在隊(duì)列中的位置的時(shí)候畴栖,如果隊(duì)列中已經(jīng)存在Toast的話随静,走的就是更新流程,而如果是一個(gè)新的Toast吗讶,則會(huì)首先創(chuàng)建一個(gè)Binder對象燎猛,然后生成一個(gè)ToastRecord對象,并加入隊(duì)列照皆,這里注意創(chuàng)建的Token對象會(huì)被保存在ToastRecord對象中重绷。
接下來這個(gè)函數(shù)很重要:

void keepProcessAliveIfNeededLocked(int pid)
    {
        int toastCount = 0; // toasts from this pid
        ArrayList<ToastRecord> list = mToastQueue;
        int N = list.size();
        for (int i=0; i<N; i++) {
            ToastRecord r = list.get(i);
            if (r.pid == pid) {
                toastCount++;
            }
        }
        try {
            mAm.setProcessImportant(mForegroundToken, pid, toastCount > 0, "toast");
        } catch (RemoteException e) {
            // Shouldn't happen.
        }
    }

這里將當(dāng)前彈Toast的進(jìn)程設(shè)置為了前臺(tái)進(jìn)程,熟悉Toast的應(yīng)該都知道膜毁,Toast的特殊性在于它支持跨頁面顯示昭卓,甚至當(dāng)應(yīng)用關(guān)閉的時(shí)候,Toast仍然能夠展示瘟滨,就是這個(gè)函數(shù)發(fā)揮的作用候醒,這里利用AMS,還是通過Binder室奏,調(diào)用了setProcessImportant火焰,將Toast所在的進(jìn)程設(shè)置為了前臺(tái)進(jìn)程劲装,保證了進(jìn)程的存活胧沫,所以當(dāng)頁面銷毀了,Toast還是可以正常顯示占业。

if (index == 0) {
                //如果當(dāng)前Toast為隊(duì)頭绒怨,則顯示Toast
                    showNextToastLocked();
                }


void showNextToastLocked() {
        //取出隊(duì)列頭的Toast
        ToastRecord record = mToastQueue.get(0);
        //居然是個(gè)循環(huán)
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                    //調(diào)用callback的show方法,傳入剛才創(chuàng)建的Token對象
                record.callback.show(record.token);
                //延時(shí)移除Toast
                scheduleDurationReachedLocked(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) {
                //移除Toast
                    mToastQueue.remove(index);
                }
                //喚醒進(jìn)程
                keepProcessAliveIfNeededLocked(record.pid);
                if (mToastQueue.size() > 0) {
                //再次獲取
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

最后如果是Toast為隊(duì)列頭谦疾,那么此時(shí)就會(huì)執(zhí)行showNextToastLocked()方法南蹂,可以看到這里首先嘗試獲取隊(duì)列頭的Toast,后面居然是一個(gè)while循環(huán)念恍,這塊我感覺Google有點(diǎn)過度嚴(yán)謹(jǐn)了六剥,可以看到如果沒有取到ToastRecord,這里就移除后,再次執(zhí)行喚醒進(jìn)程峰伙,然后再次嘗試獲取疗疟,直到獲取到,但是這樣為了一個(gè)Toast的展示瞳氓,甚至可能導(dǎo)致這個(gè)循環(huán)一直再執(zhí)行策彤,感覺有些不值當(dāng)了,這只是我個(gè)人的看法,歡迎大家討論店诗。
當(dāng)取到ToastRecord后裹刮,會(huì)執(zhí)行其callbackshow方法,當(dāng)看到這個(gè)方法名的時(shí)候庞瘸,感覺很熟悉捧弃,那么這個(gè)callback是什么對象呢,看一下ToastRecord的構(gòu)造的地方擦囊。

public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {       
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
 
            }
        }

還是剛才那個(gè)函數(shù)塔橡,可看到,callback就是入?yún)⒌膶ο笏冢敲丛倏匆幌?code>Toast的show()方法

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

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

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

這樣整個(gè)流程就通了葛家,這個(gè)callback就是最初的TN對象,還是利用Binder的雙向通信泌类,所以這里就會(huì)回到TN對象的show()方法癞谒,這里要注意,再調(diào)用show方法的時(shí)候刃榨,會(huì)把剛才創(chuàng)建的Token對象弹砚,傳入。

@Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

這里有回到了最早分析的Handler對象枢希,這個(gè)Handler對象常規(guī)使用的話是在主線程創(chuàng)建的桌吃。

mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };

可以看到又調(diào)用了handleShow方法。

public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            // If a cancel/hide is pending - no need to show - at this point
            // the window token is already invalid and no need to do any work.
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            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.hideTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                    //設(shè)置token
                mParams.token = windowToken;
                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);
                // Since the notification manager service cancels the token right
                // after it notifies us to cancel the toast there is an inherent
                // race and we may attempt to add a window after the token has been
                // invalidated. Let us hedge against that.
                try {
                //利用WindowManager將View加入
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

這里的代碼就比較簡單了苞轿,將基礎(chǔ)的屬性設(shè)置到了LayoutParams茅诱,這里比較重要的是將token設(shè)置到了LayoutParams中(關(guān)于這個(gè)屬性后面可能會(huì)有一篇博客專門講解一下這個(gè)屬性值和權(quán)限的關(guān)系,本篇博客主要分析Toast的展示原理搬卒,就不拓展分析了)瑟俭,并且利用WindowManageraddView的上,這樣最終Toast就顯示出來了契邀。
剩下了就是怎么移除這個(gè)Toast了摆寄,回到NMS,再show后坯门,使用scheduleDurationReachedLocked(record);方法微饥,就是移除操作。

private void scheduleDurationReachedLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
        //顯示耗時(shí)只有兩種
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        //通過Handler發(fā)送消息執(zhí)行
        mHandler.sendMessageDelayed(m, delay);
    }

這里第一個(gè)注意的點(diǎn)古戴,可以看到欠橘,這里delay變量只有兩種可能,LONG_DELAYSHORT_DELAY允瞧。這也就解釋了為什么我們平時(shí)使用Toast組件简软,不支持自定義顯示時(shí)長蛮拔,只能有LONGSHORT兩種時(shí)長。
然后通過Handler發(fā)送一個(gè)延時(shí)消息痹升,用于隱藏Toast組件建炫。

@Override
        public void handleMessage(Message msg)
        {
            switch (msg.what)
            {
                case MESSAGE_DURATION_REACHED:
                    handleDurationReached((ToastRecord)msg.obj);
                    break;
               ...
            }
        }
        
private void handleDurationReached(ToastRecord record)
    {
        if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
        synchronized (mToastQueue) {
        //定位消息位置
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
            //取消消息
                cancelToastLocked(index);
            }
        }
    }

這里的邏輯很簡單,就是利用Handler的消息機(jī)制疼蛾,取出顯示的消息的位置肛跌,然后進(jìn)行取消操作。

@GuardedBy("mToastQueue")
    void cancelToastLocked(int index) {
            //取出消息
        ToastRecord record = mToastQueue.get(index);
        try {
        //執(zhí)行隱藏邏輯
            record.callback.hide();
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to hide notification " + record.callback
                    + " in package " + record.pkg);
            // don't worry about this, we're about to remove it from
            // the list anyway
        }
        //移除操作
        ToastRecord lastToast = mToastQueue.remove(index);

        mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
                DEFAULT_DISPLAY);
        // We passed 'false' for 'removeWindows' so that the client has time to stop
        // rendering (as hide above is a one-way message), otherwise we could crash
        // a client which was actively using a surface made from the token. However
        // we need to schedule a timeout to make sure the token is eventually killed
        // one way or another.
        scheduleKillTokenTimeout(lastToast.token);

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            // Show the next one. If the callback fails, this will remove
            // it from the list, so don't assume that the list hasn't changed
            // after this point.
            //顯示下一個(gè)
            showNextToastLocked();
        }
    }

知道了show的邏輯后察郁,這個(gè)的原理就很相似了衍慎,這里首先取出ToastRecord變量,其實(shí)我感覺這里Google可以優(yōu)化一下皮钠,剛才先是定位稳捆,然后這里又取出,相當(dāng)于兩次遍歷麦轰,其實(shí)可以合并為一次遍歷就可以乔夯。

  • 然后利用Binder執(zhí)行hide方法。
  • 將給Toast 生成的窗口Token從WMS 服務(wù)中刪除
  • 判斷是否還有消息款侵,如果存在末荐,則繼續(xù)顯示Toast
    這里再看一下hide方法。同樣也是利用Handler,最終執(zhí)行handleHide()方法新锈。
public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                // note: checking parent() just to make sure the view has
                // been added...  i have seen cases where we get here when
                // the view isn't yet added, so let's try not to crash.
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeViewImmediate(mView);
                }


                // Now that we've removed the view it's safe for the server to release
                // the resources.
                try {
                    getService().finishToken(mPackageName, this);
                } catch (RemoteException e) {
                }

                mView = null;
            }
        }

這里還是利用WMS將View移除甲脏,這里有個(gè)地方挺有意思,這里先判斷了一下view的parent不為null,這里的注釋寫的很口語化,Google的工程師也挺有意思妹笆。

// note: checking parent() just to make sure the view has
// been added...  i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.

至此块请,整個(gè)流程就分析完畢了。

總結(jié)

這里來回顧總結(jié)一下Toast的展示原理

  1. 首先通過構(gòu)建Toast對象晾浴,內(nèi)部創(chuàng)建了TN對象负乡,這個(gè)對象是一個(gè)Binder對象。
  2. show方法的實(shí)質(zhì)是調(diào)用NMS的代理脊凰,執(zhí)行enqueueToast方法,并且傳入TN對象用于雙向通信茂腥。
  3. NMS中狸涌,將Toast的顯示構(gòu)建成了一個(gè)ToastRecord對象,并且有一個(gè)隊(duì)列用于保存最岗。
  4. NMS將ToastRecord加入隊(duì)列后帕胆,最終利用TN對象,執(zhí)行show方法
  5. TN對象的show方法般渡,最后是利用Handler發(fā)送消息懒豹,最后執(zhí)行添加芙盘,就是利用WindowManager將Toast的View加入Window。
  6. NMS中執(zhí)行完后脸秽,內(nèi)部也會(huì)利用Handler發(fā)送延時(shí)消息儒老,只有兩種LONGSHORT,消息收到后记餐,同樣也是通過TN對象驮樊,執(zhí)行hide方法
  7. 同樣的流程,TN利用Handler發(fā)送消息片酝,最終執(zhí)行囚衔,同樣利用WindowManager,移除View雕沿。
  8. NMS執(zhí)行完移除操作后练湿,會(huì)判斷隊(duì)列中是否還有消息,如果有繼續(xù)執(zhí)行展示Toast的邏輯审轮。

本篇博客主要是針對Toast組件的展示原理進(jìn)行講解鞠鲜,后面有時(shí)間會(huì)繼續(xù)分析Toast相關(guān)的問題,和Window相關(guān)的問題断国。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贤姆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子稳衬,更是在濱河造成了極大的恐慌霞捡,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,332評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件薄疚,死亡現(xiàn)場離奇詭異碧信,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)街夭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,508評論 3 385
  • 文/潘曉璐 我一進(jìn)店門砰碴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人板丽,你說我怎么就攤上這事呈枉。” “怎么了埃碱?”我有些...
    開封第一講書人閱讀 157,812評論 0 348
  • 文/不壞的土叔 我叫張陵猖辫,是天一觀的道長。 經(jīng)常有香客問我砚殿,道長啃憎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,607評論 1 284
  • 正文 為了忘掉前任似炎,我火速辦了婚禮辛萍,結(jié)果婚禮上悯姊,老公的妹妹穿的比我還像新娘。我一直安慰自己贩毕,他們只是感情好悯许,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,728評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著耳幢,像睡著了一般岸晦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上睛藻,一...
    開封第一講書人閱讀 49,919評論 1 290
  • 那天启上,我揣著相機(jī)與錄音,去河邊找鬼店印。 笑死冈在,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的按摘。 我是一名探鬼主播包券,決...
    沈念sama閱讀 39,071評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼炫贤!你這毒婦竟也來了溅固?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,802評論 0 268
  • 序言:老撾萬榮一對情侶失蹤兰珍,失蹤者是張志新(化名)和其女友劉穎侍郭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掠河,經(jīng)...
    沈念sama閱讀 44,256評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡亮元,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,576評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唠摹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爆捞。...
    茶點(diǎn)故事閱讀 38,712評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖勾拉,靈堂內(nèi)的尸體忽然破棺而出煮甥,到底是詐尸還是另有隱情,我是刑警寧澤望艺,帶...
    沈念sama閱讀 34,389評論 4 332
  • 正文 年R本政府宣布苛秕,位于F島的核電站,受9級特大地震影響找默,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吼驶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,032評論 3 316
  • 文/蒙蒙 一惩激、第九天 我趴在偏房一處隱蔽的房頂上張望店煞。 院中可真熱鬧,春花似錦风钻、人聲如沸顷蟀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸣个。三九已至,卻和暖如春布朦,著一層夾襖步出監(jiān)牢的瞬間囤萤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,026評論 1 266
  • 我被黑心中介騙來泰國打工是趴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涛舍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,473評論 2 360
  • 正文 我出身青樓唆途,卻偏偏與公主長得像富雅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子肛搬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,606評論 2 350

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