Android开发经验谈Android开发面试题

Android 在子线程中直接更新ui

2020-04-26  本文已影响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.");
        }
    }

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的变量值。

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>

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

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。

上一篇 下一篇

猜你喜欢

热点阅读