Dialog表示と消去プロセス分析


本明細書で参照するコードは、Android 5.0(API 22)バージョンです.
DialogクラスはDialogInterface,Windowを実現した.Callback, KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallbackという5つのインタフェース.よく使われる3つのインタフェース:Window.Callbackは受信画面touchイベントである.KeyEvent.Callbackはキーボードキーイベントとエンティティキーメッセージを受信する.Window.OnWindowDismissedCallbackは、受信ウィンドウのコールバックを消去します.
この文書では、Dialogの作成、非表示、表示、削除手順から、関連するコードフローを分析します.最後にいくつかのよく見られる異常分析を補充した.
ブロガーの労働成果を尊重し、転載して出典を明記してください.
作成
構築方法コードを見てみましょう.
    //         
    public Dialog(Context context) {
        this(context, 0, true);
    }

    //         
    public Dialog(Context context, int theme) {
        this(context, theme, true);
    }

    //            
    Dialog(Context context, int theme, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (theme == 0) {
                TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme, outValue, true);
                theme = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, theme);
        } else {
            mContext = context;
        }

        //  WindowManagerImpl  ,       Activity windowManger
        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        //  PhoneWindow  
        Window w = PolicyManager.makeNewWindow(mContext);
        //  ,mWindow PhoneWindow  ,  WindowManger
        mWindow = w;
          Window.Callback  
        w.setCallback(this);
        //window dismiss  
        w.setOnWindowDismissedCallback(this);
        //  Window setWindowManager  PhoneWindow   WindowMangerImpl
        w.setWindowManager(mWindowManager, null, null);
        //       
        w.setGravity(Gravity.CENTER);
        //       Handler
        mListenersHandler = new ListenersHandler(this);
    }

PolicyManager#makeNewWindow関連コード:パス:/sources/android-22/com/android/internal/policy/PolicyManager.java
    public static Window makeNewWindow(Context context) {
        return sPolicy.makeNewWindow(context);
    }

sPolicyがPolicyクラスのオブジェクト:パス:/sources/android-22/com/android/internal/policy/impl/Policy.java
    public Window makeNewWindow(Context context) {
        return new PhoneWindow(context);
    }

WindowのsetWindowManagerメソッド関連コード:
    public void setWindowManager(WindowManager wm, IBinder appToken, String appName) {
        setWindowManager(wm, appToken, appName, false);
    }

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

表示
showメソッドコード:
    public void show() {
        if (mShowing) {//dialog    show  ,  dismiss    false
            if (mDecor != null) {
                if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                    mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
                }
                //  Dialog  View, Dialog   
                mDecor.setVisibility(View.VISIBLE);
            }
            return;
        }

        mCanceled = false;

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

        onStart();
        //  DecorView
        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 {
            //          ,     dialog
            mWindowManager.addView(mDecor, l);
            mShowing = true;
            //  dialog    
            sendShowMessage();
        } finally {
        }
    }

mDecorはDecorViewオブジェクトで、DecorViewはFrameLayoutを継承します.Android 6.0以下のバージョンでは、DecorViewはPhoneWindowの内部クラスです.6.0以上のバージョンでは、抽出され、独立したクラスとなります.
Dialogを使用してfindViewメソッドとsetContentViewメソッドを呼び出していますが、実際にはPhoneWindowのメソッドが対応する操作を実行しています.Dialogの関連メソッドのコード:
    public View findViewById(int id) {
        return mWindow.findViewById(id);
    }

    public void setContentView(int layoutResID) {
        mWindow.setContentView(layoutResID);
    }

    public void setContentView(View view) {
        mWindow.setContentView(view);
    }

    public void setContentView(View view, ViewGroup.LayoutParams params) {
        mWindow.setContentView(view, params);
    }

    public void addContentView(View view, ViewGroup.LayoutParams params) {
        mWindow.addContentView(view, params);
    }

PhoneWindowの関連コードを見てみましょう.
    //  DecorView
    @Override
    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            //    ,   
            installDecor();
        }
        return mDecor;
    }

    //      DecorView
    private void installDecor() {
        if (mDecor == null) {//   null,   
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        ...//      
    }

    //  DecorView  
    protected DecorView generateDecor() {
        //  DecorView,-1     id,    xml    
        return new DecorView(getContext(), -1);
    }

    @Override
    public void setContentView(int layoutResID) {
        // 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)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        // Dialog mWindow ,cb Dialog  
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }

    @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);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

    @Override
    public void addContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            // TODO Augment the scenes/transitions API to support this.
            Log.v(TAG, "addContentView does not support content transitions");
        }
        mContentParent.addView(view, params);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

ここではDecorViewがどのように作成されたのかを詳しく説明しません.興味のあるのはソースコードを見たり、このブログを見たりすることができます.AndroidアプリケーションsetContentViewとLayoutInflaterのロード解析メカニズムのソースコード分析を見たりすることができます.
mWindowManagerを見てみましょうaddView(mDecor,l)の操作.このセクションの作成の解析から、mWindowManagerはWindowMangerImplのオブジェクトであることがわかります.まず、WindowMangerImplコードについて説明します.
public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    //
    private final Display mDisplay;
    private final Window mParentWindow;

    private IBinder mDefaultToken;

    ...//      

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

    private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) {
        // Only use the default token if we don't have a parent window.
        if (mDefaultToken != null && mParentWindow == null) {
            if (!(params instanceof WindowManager.LayoutParams)) {
                throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
            }

            // Only use the default token if we don't already have a token.
            final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
            if (wparams.token == null) {
                wparams.token = mDefaultToken;
            }
        }
    }

    ...//      

    //  View,   dimiss        
    public void removeViewImmediate(View view) {
        //true     
        mGlobal.removeView(view, true);
    }

    ...//      
}

WindowManagerGobalのaddViewメソッドコードを見続けます.
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...//  

        //ViewRootImpl                  ,          DecorView,
        //     (true false)     。
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            // Start watching for system property changes.
            if (mSystemPropertyUpdater == null) {
                mSystemPropertyUpdater = new Runnable() {
                    @Override public void run() {
                        synchronized (mLock) {
                            for (int i = mRoots.size() - 1; i >= 0; --i) {
                                mRoots.get(i).loadSystemProperties();
                            }
                        }
                    }
                };
                SystemProperties.addChangeCallback(mSystemPropertyUpdater);
            }

            int index = findViewLocked(view, false);
            if (index >= 0) {
                if (mDyingViews.contains(view)) {
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                } else {
                    throw new IllegalStateException("View " + view
                            + " has already been added to the window manager.");
                }
                // The previous removeView() had not completed executing. Now it has.
            }

            // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }

            //    
            root = new ViewRootImpl(view.getContext(), display);
            //      
            view.setLayoutParams(wparams);

            //       ,         
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            //  DecorView ViewRootView,      ,dialog        
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

ViewRootImplがスクリーンイベントを受信および転送する方法については、Activity touchイベント転送プロセス分析を参照してください.setContentとshowを経験すると、きれいなDialogが携帯電話に表示されます.
隠して消える
呼び出しを隠すのはhideメソッドです.dialogが現在のページで頻繁に呼び出される場合は、この方法を使用します.DialogのルートビューmDecorは現在から削除されず、表示されないように設定されています.hideメソッドコード
    public void hide() {
        if (mDecor != null) {
            mDecor.setVisibility(View.GONE);
        }
    }

コードdismissを呼び出し、backキーをクリックしたり、dialogの外部領域をクリックしたりすると、dialogを非表示にすることができます.
    //          
    public void dismiss() {
        if (Looper.myLooper() == mHandler.getLooper()) {
            //    ,     
            dismissDialog();
        } else {
            //    ,     , mListenersHandler  
            mHandler.post(mDismissAction);
        }
    }

    //    dismiss   
    void dismissDialog() {
        //  mDecor null dismiss ,   
        if (mDecor == null || !mShowing) {
            return;
        }

        //  PhoneWindow   ,   
        if (mWindow.isDestroyed()) {
            Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
            return;
        }

        try {
            //  mDecor,   mWindowManager   WindowManagerGlobal        
            mWindowManager.removeViewImmediate(mDecor);
        } finally {
            if (mActionMode != null) {
                mActionMode.finish();
            }
            mDecor = null;//  null,  
            mWindow.closeAllPanels();
            onStop();
            mShowing = false;//     false

            //  dismiss  
            sendDismissMessage();
        }
    }

この方法で最も重要な操作はmWindowManagerです.removeViewImmediate(mDecor)では、Dialogを現在のビューから直接削除し、破棄して解放します.したがって、ページに頻繁に表示されるdialogはdimissよりもhideを採用する方が効率的である.
WindowManagerGlobalコードを見続けます.
    public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }

        synchronized (mLock) {
            //    View         
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            //  ViewRootImpl
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }

            //      View       ,     
            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }

    //    
    private int findViewLocked(View view, boolean required) {
        final int index = mViews.indexOf(view);
        if (required && index < 0) {
            //   View    ,     
            throw new IllegalArgumentException("View=" + view + " not attached to window manager");
        }
        return index;
    }

    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();

        if (view != null) {//       View   
            InputMethodManager imm = InputMethodManager.getInstance();
            if (imm != null) {
                imm.windowDismissed(mViews.get(index).getWindowToken());
            }
        }
        //ViewRootImpl  die  
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

以上の3つのステップの解析から,Dialogがどのように作成され,表示され,消去されたかが明らかになった.
補足:一般的な例外分析
仕事中に発生する可能性のあるいくつかの異常について、原因を分析します.
WindowLeaked
例外情報:
android.view.WindowLeaked: Activity xx.xxActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{3e7fbc08 V.E..... R.....I. 0,0-960,231} that was originally added here
at android.view.ViewRootImpl.(ViewRootImpl.java:462)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:278)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
at android.app.Dialog.show(Dialog.java:311)

これはActivityがdestroyを実行している間にdialogがdismiss操作を実行されずに投げ出されたものである.onPause()またはonStop()でdismissを呼び出してdialogウィンドウを解放する必要があります.
ログに表示されているクラスを検索すると、めまいがすると思います.実際には、Activity ThreadがhandleDestroyActivity操作を実行するときに投げ出されます.コードに表示されたログが続く場合、この例外はonDestroyのログの後ろに表示されます.関連コード:
    //ActivityThread handleDestroyActivity    
    private void handleDestroyActivity(IBinder token, boolean finishing,
            int configChanges, boolean getNonConfigInstance) {
        ActivityClientRecord r = performDestroyActivity(token, finishing,
                configChanges, getNonConfigInstance);
        if (r != null) {
            cleanUpPendingRemoveWindows(r);
            WindowManager wm = r.activity.getWindowManager();
            View v = r.activity.mDecor;
            if (v != null) {
                if (r.activity.mVisibleFromServer) {
                    mNumVisibleActivities--;
                }
                //  Activity window token,
                IBinder wtoken = v.getWindowToken();
                if (r.activity.mWindowAdded) {
                    if (r.onlyLocalRequest) {
                        // Hold off on removing this until the new activity's
                        // window is being added.
                        r.mPendingRemoveWindow = v;
                        r.mPendingRemoveWindowManager = wm;
                    } else {
                        wm.removeViewImmediate(v);
                    }
                }
                if (wtoken != null && r.mPendingRemoveWindow == null) {
                    //     wtoken     DecorView
                    WindowManagerGlobal.getInstance().closeAll(wtoken,
                            r.activity.getClass().getName(), "Activity");
                }
                r.activity.mDecor = null;
            }
            if (r.mPendingRemoveWindow == null) {
                // If we are delaying the removal of the activity window, then
                // we can't clean up all windows here.  Note that we can't do
                // so later either, which means any windows that aren't closed
                // by the app will leak.  Well we try to warning them a lot
                // about leaking windows, because that is a bug, so if they are
                // using this recreate facility then they get to live with leaks.
                //     token     DecorView
                WindowManagerGlobal.getInstance().closeAll(token,
                        r.activity.getClass().getName(), "Activity");
            }

            …//    
        }
        …//    
    }

    //WindowManagerGlobal closeAll  
    public void closeAll(IBinder token, String who, String what) {
        synchronized (mLock) {
            int count = mViews.size();
            //Log.i("foo", "Closing all windows of " + token);
            //  DecorView,  token
            for (int i = 0; i < count; i++) {
                //Log.i("foo", "@ " + i + " token " + mParams[i].token
                //        + " view " + mRoots[i].getView());
                //       token null, DecorView token    
                if (token == null || mParams.get(i).token == token) {
                    ViewRootImpl root = mRoots.get(i);

                    //Log.i("foo", "Force closing " + root);
                    //     who ‘Activity’
                    if (who != null) {
                        //          ,     
                        WindowLeaked leak = new WindowLeaked(
                                what + " " + who + " has leaked window "
                                + root.getView() + " that was originally added here");
                        leak.setStackTrace(root.getLocation().getStackTrace());
                        Log.e(TAG, "", leak);
                    }

                    //    
                    removeViewLocked(i, false);
                }
            }
        }
    }

IllegalArgumentException
この異常はdismissを呼び出したときに発生し,WindowLeakedの後によく現れる.長時間の非同期操作の後、Activityはfinishリリースを回収または呼び出し、メインスレッドがdismissを呼び出すとこの例外が発生する可能性があります.主に、ネットワーク要求が終了してロードボックスを閉じるときに表示されます.解決方法:dismissの前にisFinishing()を使用して、現在のActivityがfinishされているかどうかを判断したり、onStop()またはonPaus()メソッドでdismissを呼び出したりして、onStartメソッドで表示する必要があるかどうかを判断します.例外情報:
java.lang.IllegalArgumentException: View=com.android.internal.policy.impl.PhoneWindow$DecorView{3e7fbc08 V.E..... R.....I. 0,0-960,231} not attached to window manager
at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:416)
at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:342)
at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:116)
at android.app.Dialog.dismissDialog(Dialog.java:354)
at android.app.Dialog.dismiss(Dialog.java:337)

異常なコードを投げ出すのはこの節のWindowManagerGlobalのfindView Locked方法の中にあります.mDecorは削除され、dialogはクローズメッセージを受信していないため、dialogはmDecorが現在のビューにあると判断したため、再びremoveがエラーを報告した.
BadTokenException
この例外はnewを使用してDialogを作成するのはActivityオブジェクトに転送されていないのではなく、他のContextです.token検証が失敗し、異常が放出されました.システム権限がある場合は、他のContextで作成できます.
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:685)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:289)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
at android.app.Dialog.show(Dialog.java:311)

ViewRootImpl.setViewのコード:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

                …//  
                int res; /* = WindowManagerImpl.ADD_OKAY; *///  
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    //  token
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mInputChannel);
                } catch (RemoteException e) {
                    …//  
                } finally {
                    …//  
                }

                …//  

                //  token    
                if (res < WindowManagerGlobal.ADD_OKAY) {
                    mAttachInfo.mRootView = null;
                    mAdded = false;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    switch (res) {
                        case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                        case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not valid; is your activity running?");
                        …//  
                    }
                    throw new RuntimeException(
                            "Unable to add window -- unknown error code " + res);
                }

                …//  
            }
        }
    }