文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

【Android】CalledFromWrongThreadException 深入源码分析

2022-06-06 13:43

关注

先上结论

出现此问题的原因是:在非 UI 线程中创建了 Dialog,而在 UI 线程中调用了 show() 方法

问题还原

在使用 dialog 的时候,因为线程问题,在调用 dismiss() 方法的时候,出现如下常见的 crash–Only the original thread that created a view hierarchy can touch its views.,堆栈信息如下:

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 线程调用 show() 去显示 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()

@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()
初始化的。


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()

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()

经历了山重水复,马上就要柳暗花明了!!!hold on

void doDie() {
    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)
	....省略部分堆栈

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

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
,因此,我们看看 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() 方法的地方,真的是,蓦然回首,那人却在灯火阑珊处啊:


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 的异常。

结论 Dialog 的 dismiss() 会首先被抛到 new Dialog() 的线程中执行。 只要保证创建 Dialog 和调用 show() 方法在同一个线程中执行,无论是否放到 UI 线程都可以。 执行 show() 方法的时候 ViewRootImpl 没有 checkThread(),所以不会出现 crash 参考文章

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

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

——乐于分享,共同进步,欢迎留言交流
——Treat Warnings As Errors
——Any comments greatly appreciated
——Talking is cheap, show me the code
——CSDN:https://blog.csdn.net/u011489043
——GitHub:https://github.com/selfconzrr
——微信公众号:张氏文画


作者:BugFree_张瑞


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯