Android 为什么不能再子线程更新UI

作为android开发人员,总是被要求着不能再子线程去更新UI,必须得再主线程更新UI,由于好奇,也由于看这些源码也可以提升自己,就去查了相关资料来学习(本文是自我学习记录的文章,欢迎讨论,若有不对还麻烦指正出来)
先来看看下面的代码

class PracticeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_practice)

        Thread(Runnable {
            tv_text.text = "报错"
        }).start()
    }
}

在onCreate里面,实例化了一个Thread,并在里面进行了更新TextView的操作,按照常理来说,不能再子线程更新UI,那么会不会报错呢?
运行一下


onCreate里子线程更新UI

并没有报错..这不符合常理啊?再试试在onResume里运行

class PracticeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_practice)
    }

    override fun onResume() {
        super.onResume()
        Thread(Runnable {
            tv_text.text = "报错吗"
        }).start()
    }
}
onResume里子线程更新UI

啊这..依旧完美运行,难道我们以前报错都是假的吗?我学那么久的android都是白学的?
不信邪,onPause里再试试!

override fun onPause() {
        super.onPause()
        Thread(Runnable {
            tv_text.text = "还不报错?"
        }).start()
    }
 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8052)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)

舒服了,这熟悉的异常,还是那个味道。
那么这是为什么呢?

先说结论
在activity的onResume(包括)之前里的子线程是可以在子线程更新UI的,但是不能用耗时操作去更新,在onResume以后则无法在子线程更新UI(20.11.13 更正:因为activity的页面上的view,是在handleResumeactivity的方法里创建的,抛出 "Only the original thread that created a view hierarchy can touch its views."的原因是当前的Thread和创建view(ViewRootImpl)的Thread不是同一个,所以在activity的onResume之前,view还没创建,所以可以随意修改;因此,若在子线程创建一个视图,然后在主线程修改显示也是会报错的,比如在子线程创建一个dialog,然后在主线程show就会报错,比如在子线程创建一个Toast然后子线程show,也是可以显示且不会保存,不过在子线程创建的时候需要加Looper.prepare()和Looper.loop())。
原因:不能再子线程更新UI的具体表现为,会抛出一个

CalledFromWrongThreadException ("Only the original thread that created a view hierarchy can touch its views.")

这个异常的抛出点在ViewRootImpl 里的(此处是在监测当前的所在的线程是否为创建此view的线程)

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

这个ViewRootImpl是在哪里调用这个checkThread()方法的呢?

public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();//检查线程
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }
@Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();//检查线程
        ···
    }

那么这些方法又是在哪里调用了呢?
这得从更新View说起,就拿TextView.setTextView()开始
在TextView.java里的setText方法里

@UnsupportedAppUsage
private void setText(CharSequence text, BufferType type,
                     boolean notifyBefore, int oldlen) {
    ···//省略设置文本的方法
      if (mLayout != null) {  checkForRelayout(); }//接着看这个方法

}

private void checkForRelayout() {             
        ···
        if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
            // In a fixed-height view, so use our new text layout.
            if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                    && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                autoSizeText();
                invalidate();
                return;
            }

            // Dynamic height, but height has stayed the same,
            // so use our new text layout.
            if (mLayout.getHeight() == oldht
                    && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                autoSizeText();
                invalidate();
                return;
            }
        }

        // 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();
    }
}

在checkForRelayout()里无论走哪里的判断,最后都会走invalidate()方法,所以我们先来看看这个方法

public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
         ···
         // Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo;
         final ViewParent p = mParent;//ViewParent是一个接口

        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);//着重看这个方法
        }

        ···         
 }

这里的p也就是ViewParent,是一个接口类

/**
 * Defines the responsibilities for a class that will be a parent of a View.
 * This is the API that a view sees when it wants to interact with its parent.
 * 
 */
public interface ViewParent {
    public void requestLayout();
    ···
    public void invalidateChild(View child, Rect r);
}

这个接口主要就是为了当前view和父view进行交互的;那么接着看,这个mParent,到底是谁去实现这个接口的呢?
在View.java里我们找到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");
    }
}

这里是通过assignParent进行赋值的,那么又是什么时候、谁去赋值的呢?
直接给出答案,ViewRootImpl;那么这个ViewRootImpl又是什么?

/**
 * The top of a view hierarchy, implementing the needed protocol between View
 * and the WindowManager.  This is for the most part an internal implementation
 * detail of {@link WindowManagerGlobal}.
 *
 * {@hide}
 */
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks

根据头部信息翻译(机翻)一下:

视图层次结构的顶部,实现视图之间所需的协议
和WindowManager。这在很大程度上是一个内部实现
{@link WindowManagerGlobal}的详细信息。

简单的说就是ViewRootImpl实现了View和WindowManager之间的通讯协议(小声BB:这个也是绘制View三大流程的幕后黑手)。
那么这个ViewRootImpl是在哪里初始化呢?
在ActivityThread里的handleResumeActivity()里,简单的说,就是在Activity的onResume的时候。

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
···
//此处的方法里面调用了Activity.onResume()
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
   ···
if (r.window == null && !a.mFinished && willBeVisible) {
    r.window = r.activity.getWindow();
    //获取DecorView并添加到PhoneWindow上
    View decor = r.window.getDecorView();
    decor.setVisibility(View.INVISIBLE);
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    a.mDecor = decor;
    l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
    l.softInputMode |= forwardBit;
    ···
    if (a.mVisibleFromClient) {
        if (!a.mWindowAdded) {
            a.mWindowAdded = true;
            //添加到WindowManager里
            wm.addView(decor, l);
        } else {
            // The activity will get a callback for this {@link LayoutParams} change
            // earlier. However, at that time the decor will not be set (this is set
            // in this method), so no action will be taken. This call ensures the
            // callback occurs with the decor set.
            a.onWindowAttributesChanged(l);
        }
    }
 }

在activity的setContentView时,DecorView 还没有被 WindowManager 正式添加到 Window 中,接着会调用到 ActivityThread 类的 handleResumeActivity 方法将顶层视图 DecorView 添加到 PhoneWindow 窗口,activity 的视图才能被用户看到。(补充知识:在activity.setContentView的时候创建了DecorView,但此时还未将DecorView 于WindowManager关联起来,是在这个流程里进行关联的)
接着看wm,addView()

ViewManager wm = a.getWindowManager();
···
public WindowManager getWindowManager() {
    return mWindowManager;
}

mWindowManager = mWindow.getWindowManager();
···
mWindow = new PhoneWindow(this, window, activityConfigCallback);

然后进入PhoneWindow并没有getWindowManager()方法,所以进去父类Window.java查找

public WindowManager getWindowManager() {
    return mWindowManager;
}


public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
        ···
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    return new WindowManagerImpl(mContext, parentWindow);
}

所以此处也就是最终拿到的WindowManagerImpl,进去WindowManagerImpl看addView方法

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

最终调用的是WindowManagerGlobal 的addView方法

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ···
        ViewRootImpl root;
        ···
            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;
            }
        }
    }

终于!!在此方法里创建了ViewRootImpl,并把相应的View设置到ViewRootImpl 里面去。
然后进入ViewRootImpl的setView方法里

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ···
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
    ···
view.assignParent(this);//将对应的view关联上相应的ViewRootImpl
    ···
}

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();//此方法里会执行view的三大绘制流程:测量、布局、绘制,不过不在本文讨论范围
    }
}

View.java
@UnsupportedAppUsage
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");
    }
}

看到这里,是不是就和前面对应上了。
总结:
1.当View更新重绘时,也就是调用invalidate()的时候回去调用ViewParent的invalidateChild()方法。
2.而这个ViewParent就是在Activity的OnResume的时候通过WindowManager(WindowManagerGlobal )创建的ViewRootImpl。
3.所以在onCreate或者onStrat的时候,通过子线程去更新View是可以的,但是不能做耗时操作(比如sleep了2s,然后在setText,同样会报错,因为ViewParent为null的时候,就不会去调用invalidateChild()方法。
4.在OnResume后,绑定了DecorView,并且为每个view都关联了 相应的ViewRootImpl后,invalidateChild()时就会判断是否在主线程。
5.总的来说,为什么能在onCreate、onStart、onResume里面的子线程里直接进行UI更新,是因为此时还未创建ViewRootImpl,DecorView 还未与WindowManager绑定,所以无法进行ViewRootImpl的checkThread()操作。
6.这些绑定创建流程不都是在resume里发送的吗?为毛onResume也可以在子线程更新?
因为handleResumeActivity里的performResumeActivity()方法先与WindowManager.addView(decor, l)方法...也就是说onResume过后再进行创建ViewRootImpl。

20.11.13 更正:现在发现写文章的时候说法有误,特此更正
因为activity的页面上的view,是在handleResumeactivity的方法里创建的,抛出 "Only the original thread that created a view hierarchy can touch its views."的原因是当前的Thread和创建view(ViewRootImpl)的Thread不是同一个,所以在activity的onResume之前,view还没创建,所以可以随意修改;因此,若在子线程创建一个视图,然后在主线程修改显示也是会报错的,比如在子线程创建一个dialog,然后在主线程show就会报错,比如在子线程创建一个Toast然后子线程show,也是可以显示且不会保存,不过在子线程创建的时候需要加Looper.prepare()和Looper.loop()

//此方式可行,且不会报错
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();
        Toast.makeText(MainActivity.this,"子线程弹出Toast",Toast.LENGTH_SHORT).show();
        Looper.loop();
    }
});
thread.start();
//子线程中调用    
public void showDialog(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                //创建Looper,MessageQueue
                Looper.prepare();
                new Handler().post(new Runnable() {
                    @Override
                    public void run() {
                        builder = new AlertDialog.Builder(HandlerActivity.this);
                        builder.setTitle("子线程");
                        alertDialog = builder.create();
                        alertDialog.show();
                        alertDialog.hide();
                    }
                });
                //开始处理消息
                Looper.loop();
            }
        }).start();
    }

在子线程中调用showDialog方法,先调用alertDialog.show()方法,再调用alertDialog.hide()方法,hide方法只是将Dialog隐藏,并没有做其他任何操作(没有移除Window),然后再在主线程调用alertDialog.show();便会抛出Only the original thread that created a view hierarchy can touch its views异常了

 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8052)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)

本文是自我学习记录的文章,欢迎讨论,若有不对还麻烦指正出来,谢谢~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342