为什么不能在子线程中直接更新UI?

大家都知道,在子线程中直接更新UI就会奔溃,报错如下:
android.view.ViewRootImpl$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views.
报错提示:只有创建了视图层次结构的原始线程才能访问它的视图。

什么意思呢?就是说,ui在哪个线程创建的,就应该在哪个线程中更新。而我们的UI是在主线程中创建的,所以就应该在主线程中更新UI。这是奔溃的信息提示。

首先,我们应该明白UI更新的原理,就是通过不断的测量、布局和绘制的一个过程。最终都会请求view的绘制操作,即requestLayout()这个方法。我们直接来看View.java的源码( API 28)

	@CallSuper
    public void requestLayout() {
        //... 
        
        //当父视图不为空,且有请求布局,则执行父视图的requestLayout()方法
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        
        //...
    }
    
	protected ViewParent mParent; //当前视图的父视图

	//父视图的赋值方法
	void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null;
        } else {
            throw new RuntimeException("view " + this + " being added, but"
                    + " it already has a parent");
        }
    }

从上面的源码我们可以看出,当请求布局时,会调用父视图mParent的requestLayout()方法,mParent是一个接口对象,这个方法是接口方法。我们得找到实现这个接口的那个类。
我们发现,在assignParent(ViewParent parent)方法中对mParent进行了赋值,那么assignParent(ViewParent parent)在哪里被调用的呢?

通过阅读Activity的启动过程源码,可以发现:

1、在Activity的onCreate()方法中调用了我们熟悉的setContentView()方法,其实最终调用了顶层视图PhoneWindow的setContentView()方法,然后调用其内部的installDecor()初始化mDecor;
2、紧跟着在handleLaunchActivity()中调用了handleResumeActivity();
3、然后在handleResumeActivity()中可以发现调用了WindowMangerImpl的addView方法,wm.addView(decor, l),
4、然后又调用了WindowManagerGlobal类的addView()方法,在里面可以发现又调用了root.setView(view, wparams, panelParentView),这个root是ViewRootImpl一个实例,

然后我们直接看ViewRootImpl的setView()方法:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
            	//...
                view.assignParent(this);
                //...
            }
        }
    }

由此可见,最终在ViewRootImpl的setView()方法中对前面提到的mParent 进行了赋值。同时,也指明了mParent 是ViewRootImpl的一个实例,实现了ViewParent 接口,所以很显然了,前面提到的,调用了mParent 的requestLayout()方法,即是ViewRootImpl的requestLayout():

ViewRootImpl.java

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) { // 1、请求更新布局
            checkThread();   // 2、首先检查当前所在线程
            //...
        }
    }

	void checkThread() {
		// 3、直接判断view所属的线程是否为当前线程,否则抛出一坨异常
        if (mThread != Thread.currentThread()) {  
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
    
	final Thread mThread;  //存放view所属的线程
所以,在进行UI更新时,都会进行线程的检测判断。以上就是为什么不能在子线程中直接更新UI的原因原理。
发布了27 篇原创文章 · 获赞 32 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/luoyingxing/article/details/87073393