Android消息机制原理——为什么不能在子线程更新UI?

序言

从Android开发的第一课开始,我们就有一个常识,即子线程不能更新UI,只能在转到主线程去更新?所以我们在编码时都遵照着这个原则,获取到数据后通过handler去转到主线程,通过Message拿到子线程发送过来的数据,具体可以看我的另一篇博客:

Android之Handler消息机制——深入理解 Looper、Handler、Message、MessageQueue

那么问题来了,为什么我们不能在子线程去更新UI呢?我们先动手做一个实验!

子线程更新UI

在XML文件中只添加一个TextView,在onCreate()方法中新建一个线程名为Thread#2的线程,并在Thread#2线程中去更新UI

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView) findViewById(R.id.tv);

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.i("TAG", Thread.currentThread().getName());
                tv.setText("随机数" + (1 + Math.random() * (100 - 1 + 1)));
            }
        });

        thread.setName("Thread#2");
        thread.start();

通过点击生成随机数,并打印当前执行更新UI是否在子线程

Android消息机制原理——为什么不能在子线程更新UI?_第1张图片

 

Android消息机制原理——为什么不能在子线程更新UI?_第2张图片

以上是执行和日志,可以看到,当前更新UI执行的线程是Thread#2,但是我们的APP并没有死掉,textView的内容也更新了!也就是说,子线程可以更新UI!但是为什么谷歌官方禁止在子线程更新UI呢?我们接下来继续做实例!

我们在子线程进行延时2秒,在看下效果!

Android消息机制原理——为什么不能在子线程更新UI?_第3张图片

竟然崩溃了,那问题来了,到底子线程能不能更新Ui呢?

​​​​2019-12-18 14:12:57.299 18904-18995/com.insigma.sdcard E/AndroidRuntime: FATAL EXCEPTION: Thread#2
    Process: com.insigma.sdcard, PID: 18904
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8000)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)
        at android.view.View.requestLayout(View.java:23147)
        at android.view.View.requestLayout(View.java:23147)
        at android.view.View.requestLayout(View.java:23147)
        at android.view.View.requestLayout(View.java:23147)
        at android.view.View.requestLayout(View.java:23147)
        at android.view.View.requestLayout(View.java:23147)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3026)
        at android.view.View.requestLayout(View.java:23147)
        at android.widget.TextView.checkForRelayout(TextView.java:8914)
        at android.widget.TextView.setText(TextView.java:5736)
        at android.widget.TextView.setText(TextView.java:5577)
        at android.widget.TextView.setText(TextView.java:5534)
        at com.insigma.sdcard.MainActivity$82.run(MainActivity.java:1336)
        at java.lang.Thread.run(Thread.java:764)

我们看一下报错日志,出错的线程是Thread#2,也就是我们创建的线程,出错的原因大概意思就是:只有创建视图层级的原始线程,有权利处理它的视图,创建视图的线程就是主线程(UI线程),那为什么第一个例子没有出现崩溃呢?

我们看一下错误日志中的处理,是在checkThread()方法出现了崩溃,大概调用的流程是:

setText() ——> checkForRelayout()——>requestLayout()——>ViewRootImpl.checkThread()

但是还是没有解释为什么一下能更新一下又不能更新,不要着急,我们跟着调用的流程继续去解析源码!

源码解析

setText()

private void setText(CharSequence text, BufferType type,
   boolean notifyBefore, int oldlen) {
 
//省略其他代码
 
if (mLayout != null) {
 checkForRelayout();
}
 
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
 
//省略其他代码
}

sendOnTextChanged()和onTextChanged()方法应该都是用来绘制文本的,我们接着看checkForRelayout()方法

checkForRelayout()

private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.
 
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
 
 //省略代码
 
 // We lose: the height has changed and we have a dynamic height.
 // Request a new view layout using our new text layout.
 requestLayout();
 invalidate();
} else {
 // Dynamic width, so we have no choice but to request a new
 // view layout with a new text layout.
 nullLayouts();
 requestLayout();
 invalidate();
}
}

invalidate()方法是用来重新绘制视图的

我们继续看requestLayout()方法

requestLayout()

public void requestLayout() {
//省略其他代码
if (mParent != null && !mParent.isLayoutRequested()) {
 mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
 mAttachInfo.mViewRequestingLayout = null;
}
}

//View获取到父View,mParent,然后调用父View的requestLayout方法,比如示例中的父View就是xml文件的
//根布局就是RelativeLayout


@Override
public void requestLayout() {
super.requestLayout();
mDirtyHierarchy = true;
}

//通过调用父类的ViewRootImp.requestLayout(),最终到checkThread()

@Override
public void requestLayout() {
 if (!mHandlingLayoutInLayoutRequest) {
  checkThread();
  mLayoutRequested = true;
  scheduleTraversals();
 }
}

最重要的是checkThread()方法了,光看名字应该是用来检查线程的

void checkThread() {
    // 如果当前线程不是主线程就抛出异常
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

最重要的方法来了,mThread线程是主线程,Thread.currentThread()是当前线程,即我们运行的子线程

假如当前更新UI不在主线程,就会导致CalledFromWrongThreadException异常

由此可见,每一次刷新View都会调用ViewRootImp的checkThread()方法去检测是否在主线程

 

问题:为什么onCreate里面可以在子线程更新UI?延时之后又不能了呢?

继续上一个问题,为什么onCreate里面可以更新UI?是因为没有调用checkThread()方法吗?

这个时候,我们可以搬出Activity的生命周期来解释一下了!

activity的声明周期经过oncreate()->onstart()->onResume()->onRestart()->onPouse()->onStop()->onDestory()

  • onCreate:Activity已经创建了,View还没有进行绘制
  • onStart:Activity处于可见状态,可以做一些初始化的操作
  • onResume:View已经绘制完成,并且已经处于可见状态

先强行解释一波:会不会是因为onCreate()时UI还没有绘制完,所以不需要通过ViewRootImp去检查是否是哪个线程,延时两秒之后,Activity回调onResume()方法,View绘制完成,刷新View需要走checkThread()方法

我们直接看onResume()方法:

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    ...省略
    // 初始化onResume方法
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    ...
    if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
        ...省略
        if (r.activity.mVisibleFromClient) {
            r.activity.makeVisible();
        }
    }
    ...省略
}

直接看 r.activity.makeVisible()

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

makeVisible方法将View添加到WindowMannager窗口中,即准备展示View

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ...
    ViewRootImpl root;
    View panelParentView = null;
    synchronized (mLock) {
        ...
        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 {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
        }
    }
}

在addView方法中,我们才会对ViewRootImpl对象进行初始化,也就是说,执行onCreate()在子线程中更新UI时ViewRootImpl还没出生,当然也不会去检查线程!

问题:如果不做这个校验,是不是我也是可以正常在子线程更新UI呢?

按理来说,这样是可以的!但是google为什么要这样去设计呢?

(1)如果在不同的线程去控制用一个控件,由于网络延时或者大量耗时操作,会使UI绘制错乱,出了问题也很难去排查到底是哪个线程更新时出了问题;

(2)主线程负责更新,子线程负责耗时操作,能够大大提高响应效率

(3)UI线程非安全线程,多线程进行并发访问有可能会导致内存溢出,降低硬件使用寿命;且非线程安全不能加Lock线程锁,否则会阻塞其他线程对View的访问,效率就会变得低下!

你可能感兴趣的:(Android消息机制原理——为什么不能在子线程更新UI?)