1. 程式人生 > >Android原始碼解析(十九)-->Dialog載入繪製流程

Android原始碼解析(十九)-->Dialog載入繪製流程

前面兩篇文章,我們分析了Activity的佈局檔案載入、繪製流程,算是對整個Android系統中介面的顯示流程有了一個大概的瞭解,其實Android系統中所有的顯示控制元件(注意這裡是控制元件,而不是元件)的載入繪製流程都是類似的,包括:Dialog的載入繪製流程,PopupWindow的載入繪製流程,Toast的顯示原理等,上一篇文章中,我說在介紹了Activity介面的載入繪製流程之後,就會分析一下剩餘幾個控制元件的顯示控制流程,這裡我打算先分析一下Dialog的載入繪製流程。

可能有的同學問這裡為什麼沒有Fragment?其實嚴格意義上來說Fragment並不是一個顯示控制元件,而只是一個顯示元件。為什麼這麼說呢?其實像我們的Activity,Dialog,PopupWindow以及Toast類的內部都管理維護著一個Window物件,這個Window物件不但是一個View元件的集合管理物件,它也實現了元件的載入與繪製流程,而我們的Fragment元件如果看過原始碼的話,嚴格意義上來說,只是一個View元件的集合並通過控制變數實現了其特定的生命週期,但是其由於並沒有維護Window型別的成員變數,所以其不具備元件的載入與繪製功能,因此其不能單獨的被繪製出來,這也是我把它稱之為元件而不是控制元件的原因。(在分析完這幾個控制元件的載入繪製流程之後,有時間的話,也會分析一下Fragment的相關原始碼)

好吧,開始我們今天關於Dialog的講解,相信大家在平時的開發過程中經常會使用到Dialog彈窗,使用Dialog可以在Activity彈出彈窗,確認訊息等。為了更好的分析Dialog的原始碼,我們這裡暫時寫一個簡單的demo,看一下Dialog的使用例項。

title.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                AlertDialog.Builder builder = new AlertDialog.Builder
(MainActivity.this); builder.setIcon(R.mipmap.ic_launcher); builder.setMessage("this is the content view!!!"); builder.setTitle("this is the title view!!!"); builder.setView(R.layout.activity_second); builder.setPositiveButton
("知道了", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { alertDialog.dismiss(); } }); alertDialog = builder.create(); alertDialog.show(); } });

我們在Activity中獲取一個textView元件,並監聽TextView的點選事件,並在點選事件中,初始化一個AlertDialog彈窗,並執行AlertDialog的show方法展示彈窗,在彈窗中定義一個按鈕,並監聽彈窗按鈕的點選事件,若使用者點選了彈窗的按鈕,則執行AlertDialog的dismiss方法,取消展示AlertDialog。好吧,我們來看一下這個彈窗彈出的展示結果:
這裡寫圖片描述
可以看到我們定義的icon,title,message和button都已經顯示出來了,這時候我們點選彈窗按鈕知道了,這時候彈窗就會消失了。

一般我們使用Dialog的大概流程都是這樣的,可能定製Dialog的時候有一些定製化的操作,但是基本操作流程還是這樣的。

那麼我們先來看一下AlertDialog.Builder的構造方法,這裡的Builder是AlertDialog的內部類,用於封裝AlertDialog的構造過程,看一下Builder的構造方法:

public Builder(Context context) {
            this(context, resolveDialogTheme(context, 0));
        }

好吧,這裡呼叫的是Builder的過載構造方法:

public Builder(Context context, int themeResId) {
            P = new AlertController.AlertParams(new ContextThemeWrapper(
                    context, resolveDialogTheme(context, themeResId)));
        }

那麼這裡的P是AlertDialog.Builder中的一個AlertController.AlertParams型別的成員變數,可見在這裡執行了P的初始化操作。

public AlertParams(Context context) {
            mContext = context;
            mCancelable = true;
            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

可以看到這裡主要執行了AlertController.AlertParams的初始化操作,初始化了一些成員變數。這樣執行了一系列操作之後我們的程式碼:

AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);

就已經執行完成了,然後我們呼叫了builder.setIcon方法,這裡看一下setIcon方法的具體實現:

public Builder setIcon(@DrawableRes int iconId) {
            P.mIconId = iconId;
            return this;
        }

可以看到AlertDialog的Builder的setIcon方法,這裡執行的就是給型別為AlertController.AlertParams的P的mIconId賦值為傳遞的iconId,並且這個方法返回的型別就是Builder。

然後我們呼叫了builder.setMessage方法,可以看一下builder.setMessage方法的具體實現:

public Builder setMessage(CharSequence message) {
            P.mMessage = message;
            return this;
        }

好吧,這裡跟setIcon方法的實現邏輯類似,都是給成員變數的mMessage賦值為我們傳遞的Message值,且和setIcon方法類似的,這個方法返回值也是Builder。

再看一下builder.setTitle方法:

public Builder setTitle(CharSequence title) {
            P.mTitle = title;
            return this;
        }

可以發現builder的setIcon、setMessage、setTitle等方法都是給Builder的成員變數P的icon,message,title賦值。

然後我們看一下builder.setView方法:

public Builder setView(int layoutResId) {
            P.mView = null;
            P.mViewLayoutResId = layoutResId;
            P.mViewSpacingSpecified = false;
            return this;
        }

可以發現這裡的setView和setIcon,setMessage,setTitle等方法都是類似的,都是將我們傳遞的資料值賦值給Builder的成員變數P。

然後我們呼叫了builder.setPositiveButton方法:

builder.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        alertDialog.dismiss();
                    }
                });

好吧,這裡我們看一下builder的setPositiveButton的原始碼:

public Builder setPositiveButton(CharSequence text, final OnClickListener listener) {
            P.mPositiveButtonText = text;
            P.mPositiveButtonListener = listener;
            return this;
        }

好吧,可以發現跟上面幾個方法還是類似的,都是為Builder的成員變數P的相應成員變數賦值。。。

上面的幾行程式碼我們都是呼叫的builder.setXXX等方法,主要就是為Builder的成員變數P的相應成員變數值賦值。並且setXX方法返回值都是Builder型別的,因此我們可以通過訊息璉的方式連續執行:

builder.setIcon().setMessage().setTitle().setView().setPositiveButton()...

這樣程式碼顯得比較簡潔,set方法的執行順序是沒有固定模式的,這裡多說一下,這種程式設計方式很優秀,平時我們在設計構造類工具類的時候也可以參考這種模式,構造類有不同的功能或者特性,並且都不是必須的,我們可以通過set方法設定不同的特性值並返回構造類本身。

然後我們呼叫了builder.create方法,並且這個方法返回了AlertDialog。

public AlertDialog create() {
            // Context has already been wrapped with the appropriate theme.
            final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }

可以看到這裡首先構造了一個AlertDialog,我們可以看一下這個構造方法的具體實現:

AlertDialog(Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        super(context, createContextThemeWrapper ? resolveDialogTheme(context, themeResId) : 0,
                createContextThemeWrapper);

        mWindow.alwaysReadCloseOnTouchAttr();
        mAlert = new AlertController(getContext(), this, getWindow());
    }

可以看到這裡首先呼叫了super的構造方法,而我們的AlertDialog繼承於Dialog,所以這裡執行的就是Dialog的構造方法,好吧,繼續看一下Dialog的構造方法:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == 0) {
                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.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }

可以發現在Dialog的構造方法中直接直接構造了一個PhoneWindow,並賦值給Dialog的成員變數mWindow,從這裡可以看出其實Dialog和Activity的顯示邏輯都是類似的,都是通過對應的Window變數來實現視窗的載入與顯示的。然後我們執行了一些Window物件的初始化操作,比如設定回撥函式為本身,然後呼叫了Window類的setWindowManager方法,並傳入了WindowManager,可以發現這裡的WindowManager物件是通過方法:

mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

獲取的,而我們的context傳入的是Activity物件,所以這裡的WindowManager物件其實和Activity獲取的WindowManager物件是一致的。然後我們看一下window類的setWindowManager方法:

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

可以看到跟Activity的Window物件的windowManager的獲取方式是相同的,都是通過new的方式建立一個新的WindowManagerImpl物件。好吧,繼續回到我們的AlertDialog的構造方法中,在構造方法中,我們除了呼叫Dialog的構造方法之外還執行了:

mAlert = new AlertController(getContext(), this, getWindow());

相當於初始化了AlertDiaog的成員變數mAlert。

繼續回到我們的AlertDialog.Builder.create方法,在建立了一個AlertDialog之後,又執行了P.apply(dialog.mAlert);
我們知道這裡的P是一個AlertController.AlertParams的變數,而dialog.mAlert是我們剛剛建立的AlertDialog中的一個AlertController型別的變數,我們來看一下apply方法的具體實現:

ublic void apply(AlertController dialog) {
            if (mCustomTitleView != null) {
                dialog.setCustomTitle(mCustomTitleView);
            } else {
                if (mTitle != null) {
                    dialog.setTitle(mTitle);
                }
                if (mIcon != null) {
                    dialog.setIcon(mIcon);
                }
                if (mIconId != 0) {
                    dialog.setIcon(mIconId);
                }
                if (mIconAttrId != 0) {
                    dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
                }
            }
            if (mMessage != null) {
                dialog.setMessage(mMessage);
            }
            if (mPositiveButtonText != null) {
                dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
                        mPositiveButtonListener, null);
            }
            if (mNegativeButtonText != null) {
                dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
                        mNegativeButtonListener, null);
            }
            if (mNeutralButtonText != null) {
                dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
                        mNeutralButtonListener, null);
            }
            if (mForceInverseBackground) {
                dialog.setInverseBackgroundForced(true);
            }
            // For a list, the client can either supply an array of items or an
            // adapter or a cursor
            if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
                createListView(dialog);
            }
            if (mView != null) {
                if (mViewSpacingSpecified) {
                    dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
                            mViewSpacingBottom);
                } else {
                    dialog.setView(mView);
                }
            } else if (mViewLayoutResId != 0) {
                dialog.setView(mViewLayoutResId);
            }
        }

看到了麼?就是我們在初始化AlertDialog.Builder的時候設定的icon、title、message賦值給了AlertController.AlertParams,這裡就是將我們初始化時候設定的屬性值賦值給我們建立的Dialog物件的mAlert成員變數。。。。

繼續我們的AlertDialog.Builder.create方法,在執行了AlertController.AlertParams.apply方法之後又呼叫了:

dialog.setCancelable(P.mCancelable);

可以發現這個也是AertController.AlertParams的一個成員變數,我們在初始化AlertDialog.Builder的時候也可以通過設定builder.setCancelable賦值,由於該屬性為成員變數,所以預設值為false,而我們並沒有通過builder.setCancelable修改這個屬性值,所以這裡設定的dialog的cancelable的值為false。然後我們的create方法有設定了dialog的cancelListener和dismissListener並返回了我們建立的Dialog物件。這樣我們就獲取到了我們的Dialog物件,然後我們呼叫了dialog的show方法用於顯示dialog,好吧,這裡我們看一下show方法的具體實現:

public void show() {
        if (mShowing) {
            if (mDecor != null) {
                if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                    mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
                }
                mDecor.setVisibility(View.VISIBLE);
            }
            return;
        }

        mCanceled = false;

        if (!mCreated) {
            dispatchOnCreate(null);
        }

        onStart();
        mDecor = mWindow.getDecorView();

        if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
            final ApplicationInfo info = mContext.getApplicationInfo();
            mWindow.setDefaultIcon(info.icon);
            mWindow.setDefaultLogo(info.logo);
            mActionBar = new WindowDecorActionBar(this);
        }

        WindowManager.LayoutParams l = mWindow.getAttributes();
        if ((l.softInputMode
                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
            WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
            nl.copyFrom(l);
            nl.softInputMode |=
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
            l = nl;
        }

        try {
            mWindowManager.addView(mDecor, l);
            mShowing = true;

            sendShowMessage();
        } finally {
        }
    }

方法體的內容比較多,我們慢慢看,由於一開始mShowing變數用於表示當前dialog是否正在顯示,由於我們剛剛開始呼叫執行show方法,所以這裡的mShowing變數的值為false,所以if分支的內容不會被執行,繼續往下看:

if (!mCreated) {
            dispatchOnCreate(null);
        }

mCreated這個控制變數控制dispatchOnCreate方法只被執行一次,由於我們是第一次執行,所以這裡會執行dispatchOnCreate方法,好吧,我們看一下dispatchOnCreate方法的執行邏輯:

void dispatchOnCreate(Bundle savedInstanceState) {
        if (!mCreated) {
            onCreate(savedInstanceState);
            mCreated = true;
        }
    }

好吧,可以看到程式碼的執行邏輯很簡單就是回調了Dialog的onCreate方法,那麼onCreate方法內部又執行了那些邏輯呢?由於我們建立的是AlertDialog物件,該物件繼承於Dialog,所以我們這時候需要看一下AlertDialog的onCreate方法的執行邏輯:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAlert.installContent();
    }

可以看到這裡面除了呼叫super.onCreate方法之外就是呼叫了mAlert.installContent方法,而這裡的super.onCreate方法就是呼叫的Dialog的onCreate方法,Dialog的onCreate方法只是一個空的實現邏輯,所以我們具體來看一下mAlert.installContent的實現邏輯。

public void installContent() {
        /* We use a custom title so never request a window title */
        mWindow.requestFeature(Window.FEATURE_NO_TITLE);
        int contentView = selectContentView();
        mWindow.setContentView(contentView);
        setupView();
        setupDecor();
    }

可以看到這裡實現Window視窗的頁面設定佈局初始化等操作,這裡設定了mWindow物件為NO_TITLE,然後通過呼叫selectContentView設定Window物件的佈局檔案。

private int selectContentView() {
        if (mButtonPanelSideLayout == 0) {
            return mAlertDialogLayout;
        }
        if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) {
            return mButtonPanelSideLayout;
        }
        // TODO: use layout hint side for long messages/lists
        return mAlertDialogLayout;
    }

可以看到這裡通過執行selectContentView方法返回佈局檔案的id值,這裡的預設值是mAlertDialogLayout。看過Activity佈局載入流程(android原始碼解析(十七)–>Activity佈局載入流程)的童鞋應該知道,從這個方法開始我們就把指定佈局檔案的內容載入到記憶體中的Window物件中。我們這裡看一下具體的佈局檔案。

mAlertDialogLayout = a.getResourceId(
                R.styleable.AlertDialog_layout, R.layout.alert_dialog);

也就是R.layout.alert_dialog的佈局檔案,有興趣的同學可以看一下該佈局檔案的原始碼,O(∩_∩)O哈哈~

繼續回到我們的installContent方法,在執行了mWindow.setContentView方法之後,又呼叫了setupView方法和setupDector方法,這兩個方法的主要作用就是初始化佈局檔案中的元件和Window物件中的mDector成員變數,這裡就不在詳細的說明。

然後回到我們的show方法,在執行了dispatchOnCreate方法之後我們又呼叫了onStart方法,這個方法主要用於設定ActionBar,這裡不做過多的說明,然後初始化WindowManager.LayoutParams物件,並最終呼叫我們的mWindowManager.addView()方法。

O(∩_∩)O哈哈~,到了這一步大家如果看了上一篇Acitivty佈局繪製流程的話,就應該知道順著這個方法整個Dialog的介面就會被繪製出來了。

最後我們呼叫了sendShowMessage方法,可以看一下這個方法的實現:

private void sendShowMessage() {
        if (mShowMessage != null) {
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mShowMessage).sendToTarget();
        }
    }

這裡會傳送一個Dialog已經顯示的非同步訊息,該訊息最終會在ListenersHandler中的handleMessage方法中被執行:

private static final class ListenersHandler extends Handler {
        private WeakReference<DialogInterface> mDialog;

        public ListenersHandler(Dialog dialog) {
            mDialog = new WeakReference<DialogInterface>(dialog);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case DISMISS:
                    ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
                    break;
                case CANCEL:
                    ((OnCancelListener) msg.obj).onCancel(mDialog.get());
                    break;
                case SHOW:
                    ((OnShowListener) msg.obj).onShow(mDialog.get());
                    break;
            }
        }
    }

由於我們的msg.what = SHOW,所以會執行OnShowListener.onShow方法,那麼這個OnShowListener是何時賦值的呢?還記得我們構造AlertDialog.Builder麼?

alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
                    @Override
                    public void onShow(DialogInterface dialog) {

                    }
                });

這樣就為我們的AlertDialog.Builder設定了OnShowListener,可以看一下setOnShowListener方法的具體實現:

public void setOnShowListener(OnShowListener listener) {
        if (listener != null) {
            mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
        } else {
            mShowMessage = null;
        }
    }

這樣就為我們的Dialog中的mListenersHandler構造了Message物件,並且當我們在Dialog中傳送showMessage的時候被mListenersHandler所接收。。。。

注:
這裡說一下我們平時開發中若建立的Dialog使用的Context物件不是Activity,就會報出:

Process: com.example.aaron.helloworld, PID: 11948                                                                         android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
at android.view.ViewRootImpl.setView(ViewRootImpl.java:690)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:282)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
at android.app.Dialog.show(Dialog.java:298)
at com.example.aaron.helloworld.MainActivity$1.onClick(MainActivity.java:59)
at android.view.View.performClick(View.java:4811)
at android.view.View$PerformClick.run(View.java:20136)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:194)
at android.app.ActivityThread.main(ActivityThread.java:5552)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:964)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:759)

的異常,這是由於WindowManager.addView方法最終會呼叫ViewRootImpl.setView方法,而這時候會有mToken的檢查,若我們傳入的Context物件不是Activity,這時候的mToken為空,就會出現上述問題。。。

總結:

  • Dialog和Activity的顯示邏輯是相似的都是內部管理這一個Window物件,用WIndow物件實現介面的載入與顯示邏輯;

  • Dialog中的Window物件與Activity中的Window物件是相似的,都對應著一個WindowManager物件;

  • Dialog相關的幾個類:Dialog,AlertDialog,AlertDialog.Builder,AlertController,AlertController.AlertParams,其中Dialog是視窗的父類,主要實現Window物件的初始化和一些共有邏輯,而AlertDialog是具體的Dialog的操作實現類,AlertDialog.Builder類是AlertDialog的內部類,主要用於構造AlertDialog,AlertController是AlertDialog的控制類,AlertController.AlertParams類是控制引數類;

  • 構造顯示Dialog的一般流程,構造AlertDialog.Builder,然後設定各種屬性,最後呼叫AlertDialog.Builder.create方法獲取AlertDialog物件,並且create方法中會執行,構造AlertDialog,設定dialog各種屬性的操作。最後我們呼叫Dialog.show方法展示視窗,初始化Dialog的佈局檔案,Window物件等,然後執行mWindowManager.addView方法,開始執行繪製View的操作,並最終將Dialog顯示出來;