深入理解WMS(二):Dialog與Toast源碼解析

作者:ScottStone
鏈接:http://www.itdecent.cn/p/1090d6c33dec

通過上面的分析可以看出,View是Android中的視圖呈現(xiàn)方式,但是View并不能單獨的存在,需要依附在Window這個抽象的概念上,也就是說有界面的地方就有Window,線面我們就通過Activity、Dialog跟Toast來深入的了解下Window的創(chuàng)建過程到底是怎樣的。

1. Activity中Window的創(chuàng)建過程

在介紹Activity中的Window的創(chuàng)建過程之前,我們先得了解下Activity的啟動過程,后面會專門的寫文章介紹Activity的啟動過程,這里先簡單介紹下,還是先上源碼:

......
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }
......
                Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    window = r.mPendingRemoveWindow;
                    r.mPendingRemoveWindow = null;
                    r.mPendingRemoveWindowManager = null;
                }
                appContext.setOuterContext(activity);
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);

                if (customIntent != null) {
                    activity.mIntent = customIntent;
                }
......

Activity的啟動過程很復(fù)雜,最終是有ActivityThread中的performLaunchActivity方法來完成的,看上圖源碼可以看出performLaunchActivity是通過類加載器獲得Activity的實例的。然后調(diào)動Activity的attach方法為其關(guān)聯(lián)運行過程中所依賴的一系列上下文環(huán)境變量。

在Activity的attach方法里,

  • 系統(tǒng)會創(chuàng)建Activity所屬的Window對象并為其設(shè)置回調(diào)接口,這里Window對象實際上是PhoneWindow。
  • 給Activity初始化各種參數(shù),如mUiThread等
  • 給PhoneWindow設(shè)置WindowManager,實際上設(shè)置的是WindowManagerImpl:
    下圖給出一部分源碼,有興趣的同學(xué)還是直接看源碼。
......
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mReferrer = referrer;
        mComponent = intent.getComponent();
        mActivityInfo = info;
        mTitle = title;
        mParent = parent;
        mEmbeddedID = id;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        if (voiceInteractor != null) {
            if (lastNonConfigurationInstances != null) {
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
            } else {
                mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                        Looper.myLooper());
            }
        }
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;\
        mWindow.setColorMode(info.colorMode);
......

由于Activity實現(xiàn)了Window的Callback接口,因此當Window接收到外界的狀態(tài)改變時就會回調(diào)Activity的方法。Callback接口中的方法很多,但是有幾個卻是我們都非常熟悉的,比如onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent,等等。

    public interface Callback {
        public boolean dispatchKeyEvent(KeyEvent event);
        public boolean dispatchKeyShortcutEvent(KeyEvent event);
        public boolean dispatchTouchEvent(MotionEvent event);
        public boolean dispatchTrackballEvent(MotionEvent event);
        public boolean dispatchGenericMotionEvent(MotionEvent event);
......

到這里Window已經(jīng)創(chuàng)建完成了,但是像之前文章說過的一樣,只有Window其實只是一個空的架子,還需要View才能真正是出現(xiàn)視圖。Activity的視圖是怎么加到Window中的呢?這里就得說道一個我們很熟悉的方法setContentView。

......
    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     * @param layoutResID Resource ID to be inflated.
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
......

從Activity的setContentView方法我們可以清楚的看到,getWindow()返回的實際上是上面創(chuàng)建的PhoneWindow,也就是它會調(diào)用PhoneWindow的setContentView,在該方法中會創(chuàng)建DecorView并完成布局視圖的填充。下面我們看下PhoneWindow的setContentView的源碼。

 @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

通過上面的源碼我們能清楚的看到大概分為以幾個步驟:

  1. 如果沒有DecorView,則需要創(chuàng)建,否則移除其中的mContentParent中所有的View。
  2. 將View添加到DecorView的mContentParent中。
  3. 回調(diào)Activity的onContentChanged方法通知Activity視圖已經(jīng)發(fā)生改變。

經(jīng)過上面幾個步驟,DecorView就創(chuàng)建完并初始化成功了。Activity的布局文件也已經(jīng)成功添加到了DecorView的mContentParent中,但是這個時候DecorView還沒有被WindowManager正式添加到Window中。這里需要正確理解Window的概念,Window更多表示的是一種抽象的功能集合,雖然說早在Activity的attach方法中Window就已經(jīng)被創(chuàng)建了,但是這個時候由于DecorView并沒有被WindowManager識別,所以這個時候的Window無法提供具體功能,因為它還無法接收外界的輸入信息。在ActivityThread的handleResumeActivity方法中,首先會調(diào)用Activity的onResume方法,接著會調(diào)用Activity的makeVisible(),正是在makeVisible方法中,DecorView真正地完成了添加和顯示這兩個過程,到這里Activity的視圖才能被用戶看到。

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

2. Dialog中的Window的創(chuàng)建過程

Dialog的Window的創(chuàng)建過程跟Activity的很相似,大體有以下幾個步驟。
-1. 創(chuàng)建Window
Dialog的Window的創(chuàng)建同樣是PhoneWindow,這個剩下的跟Activity還是很類似的。具體看下下面的源碼。

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == ResourceId.ID_NULL) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        mListenersHandler = new ListenersHandler(this);
    }

-2. 初始化DecorView并將Dialog的界面添加到DecorView中
這個過程跟Activity也是類似的,也是通過Window去添加指定的布局。

    /**
     * Set the screen content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the screen.
     * @param layoutResID Resource ID to be inflated.
     */
    public void setContentView(@LayoutRes int layoutResID) {
        mWindow.setContentView(layoutResID);
    }

-3. 將DecorView添加到Window中并顯示
Dialog的show方法中,會通過WindowManager將DecorView添加到Window中,源碼如下

......
 mDecor = mWindow.getDecorView();
 ......
 mWindowManager.addView(mDecor, l);
 mShowing = true;
......

其實從上面的三個步驟能看出,Dialog的Window創(chuàng)建過程跟Activity的很類似,幾乎沒有多少區(qū)別。當Dialog關(guān)閉時,會通過WindowManager來移除DecorView。
普通的Dialog有個不同之處,就是必須要使用Activity的Context,如果使用Application的Context會報錯。這個地方是因為普通的Dialog需要token,而token一般是Activity才會有,這個時候如果一定要用Application的Context,需要Dialog是系統(tǒng)的Window才行,這就需要一開始設(shè)置Window的type,一般選擇TYPE_SYSTEM_OVERLAY指定Window的類型為系統(tǒng)Window。

3 Toast的Window創(chuàng)建過程

Toast和Dialog不同,它的工作過程就稍顯復(fù)雜。首先Toast也是基于Window來實現(xiàn)的,但是由于Toast具有定時取消這一功能,所以系統(tǒng)采用了Handler。在Toast的內(nèi)部有兩類IPC過程,第一類是Toast訪問NotificationManagerService,第二類是Notification-ManagerService回調(diào)Toast里的TN接口。關(guān)于IPC的一些知識,可以移步Android中的IPC方式。為了便于描述,下面將NotificationManagerService簡稱為NMS。
Toast屬于系統(tǒng)Window,它內(nèi)部的視圖由兩種方式指定,一種是系統(tǒng)默認的樣式,另一種是通過setView方法來指定一個自定義View,不管如何,它們都對應(yīng)Toast的一個View類型的內(nèi)部成員mNextView。Toast提供了show和cancel分別用于顯示和隱藏Toast,它們的內(nèi)部是一個IPC過程,下面我們看下show方法跟cancel方法。

     /**
     * Show the view for the specified duration.
     */
    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
        }
    }
/**
     * Close the view if it's showing, or don't show it if it isn't showing yet.
     * You do not normally have to call this.  Normally view will disappear on its own
     * after the appropriate duration.
     */
    public void cancel() {
        mTN.cancel();
    }

從上面的代碼可以看到,顯示和隱藏Toast都需要通過NMS來實現(xiàn),由于NMS運行在系統(tǒng)的進程中,所以只能通過遠程調(diào)用的方式來顯示和隱藏Toast。需要注意的是TN這個類,它是一個Binder類,在Toast和NMS進行IPC的過程中,當NMS處理Toast的顯示或隱藏請求時會跨進程回調(diào)TN中的方法,這個時候由于TN運行在Binder線程池中,所以需要通過Handler將其切換到當前線程中。這里的當前線程是指發(fā)送Toast請求所在的線程。注意,由于這里使用了Handler,所以這意味著Toast無法在沒有Looper的線程中彈出,這是因為Handler需要使用Looper才能完成切換線程的功能.
從上面源碼show方法我們可以看到,Toast的顯示調(diào)用了NMS的enqueueToast方法。enqueueToast方法有三個參數(shù),分別是:pkg當前應(yīng)用包名、tn遠程回調(diào)和mDuration顯示時長。
enqueueToast首先將Toast請求封裝為ToastRecord對象并將其添加到一個名為mToastQueue的隊列中。mToastQueue其實是一個ArrayList。對于非系統(tǒng)應(yīng)用來說,mToastQueue中最多能同時存在50個ToastRecord,這樣做是為了防止DOS(DenialofService)。如果不這么做,試想一下,如果我們通過大量的循環(huán)去連續(xù)彈出Toast,這將會導(dǎo)致其他應(yīng)用沒有機會彈出Toast,那么對于其他應(yīng)用的Toast請求,系統(tǒng)的行為就是拒絕服務(wù),這就是拒絕服務(wù)攻擊的含義,這種手段常用于網(wǎng)絡(luò)攻擊中。

                        // Limit the number of toasts that any given package except the android
                        // package can enqueue.  Prevents DOS attacks and deals with leaks.
                        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;
                                     }
                                 }
                            }
                        }
                    // 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) {
                        showNextToastLocked();
                    }

正常情況下,一個應(yīng)用不可能達到上限,當ToastRecord被添加到mToastQueue中后,NMS就會通過showNextToastLocked方法來顯示當前的Toast。下面的代碼很好理解,需要注意的是,Toast的顯示是由ToastRecord的callback來完成的,這個callback實際上就是Toast中的TN對象的遠程Binder,通過callback來訪問TN中的方法是需要跨進程來完成的,最終被調(diào)用的TN中的方法會運行在發(fā)起Toast請求的應(yīng)用的Binder線程池中。

 @GuardedBy("mToastQueue")
    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                record.callback.show(record.token);
                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);
                }
                keepProcessAliveIfNeededLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

從上面的源碼可以看到,Toast顯示之后,通過scheduleTimeoutLocked來發(fā)送一個延時消息,時長當然是根據(jù)一開始設(shè)置的時間。具體看下代碼:

 @GuardedBy("mToastQueue")
    private void scheduleTimeoutLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        mHandler.sendMessageDelayed(m, delay);
    }

上面LONG_DELAY是3.5s,SHORT_DELAY是2s。延時過后,NMS會通過cancelToastLocked來隱藏Toast并從mToastQueue中移除,我們看下源碼就能清楚的了解這個過程,下面是cancelToastLocked方法,可以看到移除Toast之后如果mToastQueue有Toast又調(diào)用了showNextToastLocked方法。

 @GuardedBy("mToastQueue")
    void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            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, true, DEFAULT_DISPLAY);

        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.
            showNextToastLocked();
        }
    }

經(jīng)過上面的分析,我們了解到Toast的顯示和隱藏過程實際上是通過Toast中的TN這個類來實現(xiàn)的,它有兩個方法show和hide,分別對應(yīng)Toast的顯示和隱藏。由于這兩個方法是被NMS以跨進程的方式調(diào)用的,因此它們運行在Binder線程池中。為了將執(zhí)行環(huán)境切換到Toast請求所在的線程,在它們的內(nèi)部使用了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;
                        }
                    }
                }
            };
......
         /**
         * schedule handleShow into the right thread
         */
        @Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }
        /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.obtainMessage(HIDE).sendToTarget();
        }

上述代碼中,mShow和mHide是兩個Runnable,它們內(nèi)部分別調(diào)用了handleShow和handleHide方法。由此可見,handleShow和handleHide才是真正完成顯示和隱藏Toast的地方。TN的handleShow中會將Toast的視圖添加到Window中。代碼如下。

......
                // 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 {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
......

上面的handleShow代碼段,我們能清楚的看到,mWM將Toast添加了進去。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);
                }

                mView = null;
            }
        }

到這里Toast的Window創(chuàng)建就介紹完了,相信大家看后應(yīng)該有了新的理解。
當然還有很多其他的通過Window實現(xiàn)的組件,諸如PopWindow、菜單欄和狀態(tài)欄能,這里不再一一介紹了,還是那句話,看源碼,里面的注釋寫的也是比較詳細的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容