iis服务器助手广告广告
返回顶部
首页 > 资讯 > 移动开发 >【Android】CalledFromWrongThreadException 深入源码分析
  • 553
分享到

【Android】CalledFromWrongThreadException 深入源码分析

源码Android 2022-06-06 13:06:19 553人浏览 薄情痞子
摘要

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

先上结论

出现此问题的原因是:在非 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_张瑞


--结束END--

本文标题: 【Android】CalledFromWrongThreadException 深入源码分析

本文链接: https://www.lsjlt.com/news/29344.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作