【Android】 【Dialog】CalledFromWrongThreadException 深入分析

先上结论

因为在非 UI 线程中创建了 Dialog,而在 UI 线程中调用了 show() 方法

问题还原

在使用 dialog 的时候,因为线程问题,在调用 dismiss() 方法的时候,出现 crash,即 CalledFromWrongThreadException,堆栈信息如下:

threadName:main,threadPriority:5,threadState:RUNNABLE
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7020)
	at android.view.ViewRootImpl.doDie(ViewRootImpl.java:6035)
	at android.view.ViewRootImpl.die(ViewRootImpl.java:6020)
	at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:486)
	at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:424)
	at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
	at android.app.Dialog.dismissDialog(Dialog.java:363)
	....省略部分堆栈

关键:Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. 显然,大家的第一反应,觉得开发在非 ui 线程进行了 ui 操作。

Demo 还原

public class MainActivity extends BaseActivity {
    private static final String TAG = "MainActivity";
    private ProgressDialog dialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();

                // 子线程中创建 Dialog
                dialog = new ProgressDialog(MainActivity.this);
                dialog.setCanceledOnTouchOutside(true);
                dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialog) {
                        Log.d("zhangrr", "Dialog onCancel thread: " + getThreadInfo());
                    }
                });
                dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
                    @Override
                    public void onDismiss(DialogInterface dialog) {
                        Log.d("zhangrr", "Dialog onDismiss thread: " + getThreadInfo());
                    }
                });
                dialog.setMessage("dialog test");
                Log.d("zhangrr", "Dialog create thread: " + getThreadInfo());

                Looper.loop();
            }
        }).start();


        Button btn = (Button) findViewById(R.id.btn_helloworld);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // UI 线程中 show,然后点击空白区域 dismiss
                dialog.show();
                Log.d("zhangrr", "Dialog show thread: " + getThreadInfo());
            }
        });
    }

    private String getThreadInfo(){
        return "[" + Thread.currentThread().getId() + "]" +
                ((Looper.myLooper() == Looper.getMainLooper())? " is UI-Thread" : "");
    }
} 

demo 很简单,在 Activity 创建的时候,在子线程中创建了一个 ProgressDialog 实例,在 UI 线程显示 Dialog。再点击界面空白处,dismiss Dialog 结果直接 crash 了,抛出了 CalledFromWrongThreadException 的异常。我们来看看打印的 log:

Dialog create thread: [3953] 
Dialog show thread: [1] is UI-Thread
Dialog onCancel thread: [3953]
..crash...如上堆栈信息

如果说只有创建这个控件的线程才能去更新该控件的内容,那么在调用 show() 方法的时候为什么不会 crash,然后 dismiss() 的时候才会崩溃?

再来根据堆栈信息,我们将深入源码一步步的一探究竟

threadName:main,threadPriority:5,threadState:RUNNABLE
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7020)
	at android.view.ViewRootImpl.doDie(ViewRootImpl.java:6035)
	at android.view.ViewRootImpl.die(ViewRootImpl.java:6020)
	at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:486)
	at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:424)
	at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
	at android.app.Dialog.dismissDialog(Dialog.java:363)
	....省略部分堆栈

源码还原

根据上面的出错堆栈信息,我们从下往上一步步追踪源码:

1、Dialog.dismiss()

	/**
     * Dismiss this dialog, removing it from the screen. This method can be
     * invoked safely from any thread.  Note that you should not override this
     * method to do cleanup when the dialog is dismissed, instead implement
     * that in {@link #onStop}.
     */
    @Override
    public void dismiss() {
        if (Looper.myLooper() == mHandler.getLooper()) {
            dismissDialog();
        } else {
            mHandler.post(mDismissAction);
        }
    }

官方的解释是,dismiss() 方法可以在任意线程中调用,不用担心线程安全问题。也就是说,dialog 对于 ui 操作做了特别处理,如果当前执行 dismiss 操作的线程和 mHandler 所依附的线程不一致的话,那么就会将 dismiss 操作丢到对应的 mHandler 所在的线程队列中等待执行。那么这个 mHandler 又是哪里来的呢?

public class Dialog implements DialogInterface, Window.Callback,
        KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {

		private final Handler mHandler = new Handler();
}

可以看到 mHandler 对象是 Dialog 类中私有的,会在 new Dialog 的时候自动初始化。因此,可以断定,该 mHandler 直接关联的就是我们 new Dialog() 即创建 Dialog 的线程。也就是说,最终真正执行 dismissDialog() 方法销毁 Dialog 的线程也是 new Dialog() 的线程。

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

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

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

            sendDismissMessage();
        }
    }

可以发现最终调用了 mWindowManager.removeViewImmediate(mDecor); 来销毁 Dialog,继续跟进 removeViewImmediate() 这个方法

2、WindowManagerImpl.removeViewImmediate()

mWindowManager 这个对象的初始化是在 Dialog 的构造函数中:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        /* 省略部分代码 */
        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);
    }

mWindowManager 是通过 context.getSystemService(Context.WINDOW_SERVICE); 得到的,再看 Activity 中的 getSystemService() 函数:

@Override
    public Object getSystemService(@ServiceName @NonNull String name) {
        if (getBaseContext() == null) {
            throw new IllegalStateException(
                    "System services not available to Activities before onCreate()");
        }

        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ensureSearchManager();
            return mSearchManager;
        }
        return super.getSystemService(name);
    }
	
	final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, IVoiceInteractor voiceInteractor) {

    	mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);

   	    mWindowManager = mWindow.getWindowManager();

    //...省略其他代码...
}

可以发现 mWindowManager 这个对象是在 Activity 被创建之后调用 attach() 函数的时候通过 mWindow.setWindowManager()初始化的。

/**
     * Set the window manager for use by this Window to, for example,
     * display panels.  This is <em>not</em> used for displaying the
     * Window itself -- that must be done by the client.
     *
     * @param wm The window manager for adding new windows.
     */
    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);
    }

发现 mWindowManager 这个对象最终来源于 WindowManagerImpl 类:

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;
    private final Window mParentWindow;

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

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

    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

    @Override
    public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }
}

终于!!!看到了 removeViewImmediate() 函数的身影。因此,可以得出结论,在执行 Dialog 销毁的函数 dismissDialog() 时,最终调用了 mWindowManager.removeViewImmediate(mDecor); 来销毁Dialog,即 WindowManagerImpl 实例中的 removeViewImmediate() 方法。

再来插播一下之前的堆栈信息

threadName:main,threadPriority:5,threadState:RUNNABLE
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7020)
	at android.view.ViewRootImpl.doDie(ViewRootImpl.java:6035)
	at android.view.ViewRootImpl.die(ViewRootImpl.java:6020)
	at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:486)
	at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:424)
	at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
	at android.app.Dialog.dismissDialog(Dialog.java:363)
	....省略部分堆栈

3、WindowManagerGlobal.removeView()

现在到了第3步,WindowManagerImpl 实例中的 removeViewImmediate() 方法,实际上调用的是 WindowManagerGlobal 的 removeView() 函数。

public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }

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

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

4、WindowManagerGlobal.removeViewLocked()

马不停蹄,我们追踪到第4步了,

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

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

注意这两行代码:root 对象其实是个 ViewRootImpl 的实例,因此我们将追一下它的 die() 方法:

ViewRootImpl root = mRoots.get(index);
boolean deferred = root.die(immediate);

再来插播一下之前的堆栈信息

threadName:main,threadPriority:5,threadState:RUNNABLE
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7020)
	at android.view.ViewRootImpl.doDie(ViewRootImpl.java:6035)
	at android.view.ViewRootImpl.die(ViewRootImpl.java:6020)
	at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:486)
	at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:424)
	at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
	at android.app.Dialog.dismissDialog(Dialog.java:363)
	....省略部分堆栈

5、ViewRootImpl.die()

	/**
     * @param immediate True, do now if not in traversal. False, put on queue and do later.
     * @return True, request has been queued. False, request has been completed.
     */
    boolean die(boolean immediate) {
        // Make sure we do execute immediately if we are in the middle of a traversal or the damage
        // done by dispatchDetachedFromWindow will cause havoc on return.
        if (immediate && !mIsInTraversal) {
            doDie();
            return false;
        }
    }

最终,执行到了 ViewRootImpl 类的 doDie() 方法。

6、ViewRootImpl.doDie()

马上就要拨开云雾见天日了,顶住顶住!!!

	void doDie() {
        checkThread();
        // 省略代码
    }

根据堆栈信息,我们知道最终抛出异常的位置就在 ViewRootImpl 代码中的 checkThread() 函数。

再来插播一下之前的堆栈信息

threadName:main,threadPriority:5,threadState:RUNNABLE
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7020)
	at android.view.ViewRootImpl.doDie(ViewRootImpl.java:6035)
	at android.view.ViewRootImpl.die(ViewRootImpl.java:6020)
	at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:486)
	at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:424)
	at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
	at android.app.Dialog.dismissDialog(Dialog.java:363)
	....省略部分堆栈

7、ViewRootImpl.checkThread()

终于来到了关键之处

	void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

初步结论:根据文章开始的介绍,当调用 dialog 的 dismiss() 时,dialog 会自动抛到我们 new Dialog() 的线程中执行,也就是 Thread.currentThread 。之所以 crash,抛出该异常,是因为,ViewRootImpl 本身的 mThread 和 我们 new Dialog() 的线程不一致。

因此,接下来问题的元凶就是 mThread 是怎么来的了!!!

在 ViewRootImpl 中寻找 mThread 赋值的地方:

public ViewRootImpl(Context context, Display display) {
    mThread = Thread.currentThread();

    // 省略部分代码 
}

那现在的问题就是,ViewRootImpl 什么时候调用这个构造函数创建实例的呢?

回到第4小部分,WindowManagerGlobal 的 removeView() 方法

ViewRootImpl root = mRoots.get(index);
boolean deferred = root.die(immediate);

发现 root 对象是从 mRoots 对象中取出来的,而 mRoots 是一个 ArrayList<ViewRootImpl>,因此,我们看看 mRoots 的生成

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

    //...省略其余代码.....

    ViewRootImpl root;
    synchronized (mLock) {

       //...省略其余代码.....

        root = new ViewRootImpl(view.getContext(), display);
        mRoots.add(root);
    }
}

那 addView() 方法什么时候会调用呢?就是 WindowManagerImpl.addView()

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

我们在 Dialog 的源码中找一下 mWindowManager 对象调用 addView() 方法的地方,真的是,蓦然回首,那人却在灯火阑珊处啊:

	/**
     * Start the dialog and display it on screen.  The window is placed in the
     * application layer and opaque.  Note that you should not override this
     * method to do initialization when the dialog is shown, instead implement
     * that in {@link #onStart}.
     */
    public void show() {
        // 省略部分代码
        onStart();
        mDecor = mWindow.getDecorView();

        mWindowManager.addView(mDecor, l);

        sendShowMessage();
    }

看到这,估计也都看明白了,调用 Dialog 的 show() 方法,会通过 mWindowManager.addView(mDecor, l); 创建一个 ViewRootImpl 的对象,这个对象会在创建的时候保存一个当前线程的 Thread 对象,也就是调用 Dialog 的 show() 方法的线程。

而在调用 Dialog 的 dismiss() 方法时,会首先把它抛到 new Dialog 的线程中执行,最后通过调用mWindowManager.removeViewImmediate() 来销毁 View,此时也就自然调用到了 ViewRootImpl 对象的 doDie() 方法,这个方法中会 checkThread(),此时会检查当前线程(也就是调用 new Dialog 的线程)是不是创建 ViewRootImpl 的对象的线程(也就是 调用 Dialog 的 show() 方法的线程)。

再来插播一下之前的堆栈信息

threadName:main,threadPriority:5,threadState:RUNNABLE
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7020)
	at android.view.ViewRootImpl.doDie(ViewRootImpl.java:6035)
	at android.view.ViewRootImpl.die(ViewRootImpl.java:6020)
	at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:486)
	at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:424)
	at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
	at android.app.Dialog.dismissDialog(Dialog.java:363)
	....省略部分堆栈

跟着堆栈信息,我们一步步走到了问题的根源,再来简单总结一下:

  • new Dialog:非 UI 线程(Thread.currentThread())
  • show():UI 主线程(mThread)
  • cancel():非 UI 线程
  • dismiss():非 UI 线程

现在就明确了,执行 show() 方法的时候 ViewRootImpl 没有 checkThread(),所以不会出现 crash。而在执行 dismiss() 的时候,它首先被抛到 new Dialog 的线程中执行,而后真正销毁 View 时 ViewRootImpl 会 checkThread(),保证 addView() 的线程才能 removeView()。

而在例子中,Dialog 的 show() 是在主线程执行,new Dialog() 是在 非 UI 线程中执行的,所以抛出了 CalledFromWrongThreadException 的异常。

结论

  1. Dialog 的 dismiss() 会首先被抛到 new Dialog() 的线程中执行。
  2. 只要保证创建 Dialog 和调用 show() 方法在同一个线程中执行,无论是否放到 UI 线程都可以。

参考文章

https://my.oschina.net/qixiaobo025/blog/195396

------至所有正在努力奋斗的程序猿们!加油!!
有码走遍天下 无码寸步难行
1024 - 梦想,永不止步!
爱编程 不爱Bug
爱加班 不爱黑眼圈
固执 但不偏执
疯狂 但不疯癫
生活里的菜鸟
工作中的大神
身怀宝藏,一心憧憬星辰大海
追求极致,目标始于高山之巅
一群怀揣好奇,梦想改变世界的孩子
一群追日逐浪,正在改变世界的极客
你们用最美的语言,诠释着科技的力量
你们用极速的创新,引领着时代的变迁

——乐于分享,共同进步,欢迎补充
——Treat Warnings As Errors
——Any comments greatly appreciated
——Talking is cheap, show me the code
——诚心欢迎各位交流讨论!QQ:1138517609
——CSDN:https://blog.csdn.net/u011489043
——简书:https://www.jianshu.com/u/4968682d58d1
——GitHub:https://github.com/selfconzrr

发布了79 篇原创文章 · 获赞 207 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/u011489043/article/details/100114366
今日推荐