Android 在子线程中直接更新ui

作者: 折剑游侠 | 来源:发表于2020-04-26 11:19 被阅读0次

老生常谈的问题了,大家都知道在子线程中更新ui会报CalledFromWrongThreadException。
实际上此异常在ViewRootImpl的checkThread()方法中抛出。换句话说,在子线程中更新ui,不触发checkThread()方法是可以正常更新的。

ViewRootImpl.checkThread()

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
  • ViewRootImpl还未创建一定不会触发checkThread(),那么问题来了,ViewRootImpl何时创建?
    Android Activity、Window、View 之前有说,在ActivityThread.handleResumeActivity()方法中先回调Activity. onResume(),然后调用Activity.makeVisible()经过一系列调用到WindowManagerGlobal.addView(),ViewRootImpl在此方法中初始化,然后调用到ViewRootImpl.performTraversals()开始绘制流程。既然ViewRootImpl在onResume()之后才初始化,那么在onResume()和onResume()之前都是可以直接更新ui的,可以验证下。

TextActivity

package com.chenxuan.jetpack

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_text.*

class TextActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_text)

        Thread(Runnable {
            tv.text = "call on Thread"
        }).start()
    }
}

activity_text.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="content"
        android:textColor="@android:color/holo_orange_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

结果自然是正常运行。严格来讲并不算更新ui,ViewRootImpl还未初始化、开启绘制流程,此处改变text只是改变了TextView的变量值。

  • ViewRootImpl初始化后仍可更新ui,有限定条件。

TextActivity

package com.chenxuan.jetpack

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_text.*

class TextActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_text)
        
        button.setOnClickListener {
            Thread(Runnable {
                tv.text = "call on Thread"
            }).start()
        }
    }
}

activity_text.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="content"
        android:textColor="@android:color/holo_orange_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="update"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv" />
</androidx.constraintlayout.widget.ConstraintLayout>
异常

不出所料。看看异常信息,触发了ViewRootImpl.requestLayout,内部调用了ViewRootImpl.checkThread()

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

下面来寻找源头,从TextView.setText()方法开始

    public final void setText(CharSequence text) {
        setText(text, mBufferType);
    }

    public void setText(CharSequence text, BufferType type) {
        setText(text, type, true, 0);

        if (mCharWrapper != null) {
            mCharWrapper.mChars = null;
        }
    }

    private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
            ...
            //重点
            checkForRelayout();
    }

TextView.checkForRelayout()

    private void checkForRelayout() {
        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
            ...
            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }
            }

            requestLayout();
            invalidate();
        } else {
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }

checkForRelayout()方法中一堆判断,归根结底,若TextView大小不改变便不会触发requestLayout()重新布局。

View.requestLayout()

    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            //调用到ViewRootImpl.requestLayout()
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

固定TextView大小
activity_text.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="content"
        android:textColor="@android:color/holo_orange_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="update"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv" />
</androidx.constraintlayout.widget.ConstraintLayout>

再运行一下,点击按钮正常更新。

  • ViewRootImpl.checkThread()中注释说明判断的是更新ui的线程和创建ui的线程是否相同,并非一直以来说的Android ui主线程。那么在子线程中创建ui,自然可以在这个子线程中进行任何操作。
package com.chenxuan.jetpack

import android.content.Context
import android.os.Bundle
import android.os.Looper
import android.view.Gravity
import android.view.WindowManager
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class TextActivity : AppCompatActivity() {
    lateinit var asyncText: TextView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val wm :WindowManager= getSystemService(Context.WINDOW_SERVICE) as WindowManager

        Thread(Runnable {
            Looper.prepare()
            asyncText = TextView(this)
            asyncText.setBackgroundResource(android.R.color.darker_gray)
            asyncText.setTextColor(resources.getColor(android.R.color.holo_orange_light))
            asyncText.text = "asyncText"
            val param = WindowManager.LayoutParams()
            param.gravity = Gravity.CENTER
            param.width = WindowManager.LayoutParams.WRAP_CONTENT
            param.height = WindowManager.LayoutParams.WRAP_CONTENT
            wm.addView(asyncText,param)
            Looper.loop()
        }).start()
    }
}

在子线程中初始化TextView,并设置各种属性,通过WindowManager添加进根布局。
ViewRootImpl中初始化了handler->ViewRootHandler,在子线程中记得手动创建Looper。

相关文章

  • 封装工具类无法使用runOnUiThread解决办法

    由于Android中不能在子线程中更新ui,所以平时在子线程中需要更新ui时可以使用Android提供的RunOn...

  • Android多线程

    1.沿用java的子线程创建 2.在子线程中不能更新UI,那么在Android中更新UI的方法 runOnUiTh...

  • Android异步消息处理机制之Handler、Looper、M

    为什么用异步消息处理机制?因为Android UI线程是线程不安全的,在子线程中更新UI会直接程序崩溃,另外当UI...

  • Android Handler探索

    在日常开发中,我们常用Handler来在子线程更新主线程UI,这是因为Android系统不允许我们在子线程更新UI...

  • 子线程更新UI的方法

    子线程中不能直接更新UI,如果直接更新的话会发生崩溃所以要在主线程中更新UI,总计三种回到主线程更新UI的方式 1...

  • 【Android】AsyncTask源码分析

    在Android中ui是非线程安全的,更新ui只能在主线程操作,所以我们平时如果遇到子线程更新UI的情况,必须要切...

  • Android 进阶之 Handler 消息机制

    众所周知,在 Android 中,非 UI 线程中是不能更新 UI 的,如果在子线程中做 UI 相关操作,可能会出...

  • Android 跨线程更新 UI

    在 Android 中,视图组件并不是线程安全的。出于线程保护,Android 会拒绝子线程对主线程 UI 的更新...

  • Handler内存泄露原理及解决方法

    前言 因为Android采取了单线程UI模型,开发者无法在子线程中更新UI,为此Android为我们提供了Hand...

  • Android多线程编程

    1.在子线程中更新UI Android中更新UI元素,必须在主线程中进行,否则就会出现异常。 运行以上程序,你会发...

网友评论

    本文标题:Android 在子线程中直接更新ui

    本文链接:https://www.haomeiwen.com/subject/icxdwhtx.html