续上一篇:子线程为什么也能成功更新UI

372 阅读3分钟

上一篇中有段代码,这段子线程中可以成功更新UI,

private void newThreadChange(){
    new Thread(){
        @Override
        public void run() {
            mTv.setText("changed");
        }
    }.start();
}

我现在把代码稍稍修改一下,让他sleep一会,然后更新UI,这个时候会直接报错,报错信息如下图

private void newThreadChange(){
    new Thread(){
        @Override
        public void run() {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            mTv.setText("changed");
        }
    }.start();
}

Log日志

我们根据其调用链分析一下

- android.widget.TextView.setText
- android.widget.TextView.checkForRelayout
- android.view.View.requestLayout
- android.view.ViewRootImpl.requestLayout
- android.view.ViewRootImpl.checkThread

从TextView的setText开始,调用了checkForRelayout这个方法,简单的看下下面的这个方法,发现这里面有个两个刷新view的方法 requestLayout()和 invalidate();


private void checkForRelayout() {
    if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
            || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
            && (mHint == null || mHintLayout != null)
            && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
        ...
        ...
        
        requestLayout();
        invalidate();
    } else {
        nullLayouts();
        requestLayout();
        invalidate();
    }
}

根据上面的调用链,我们只看requestLayout()方法,这里是View的requestLayout()的方法

public void requestLayout() {
    ...
    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    ...
}

这里的mParent是一个接口,ViewGroup是其一个实现类,这里的mParent也就是包裹该View的外层ViewGroup,然后我们继续看ViewGroup中的requestLayout()

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

从这里看,发现还是调用了View里面的requestLayout(),

  • 然后View(这里的view就是ViewGroup)还是调用mParent中的requestLayout,
  • mParent还是包裹该ViewGroup的ViewGroup,
  • 就这样一直往上层调用,一直调用最外层的ViewGroup的requestLayout()

根据调用链,我们发现最后走到了ViewRootImpl的requestLayout(),然后在这里我们就发现有个checkThread()的调用, 然后就抛出异常了,

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

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

但是这还是解释不了,不加sleep的时候不抛异常呀!

现在提供给我们一个线索,Android在WindowManagerGlobal.addView中初始化了ViewRootImpl,不信看下面的代码

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ......

    ViewRootImpl root;
    View panelParentView = null;
    
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

    synchronized (mLock) {
        //这里初始化了ViewRootImpl
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        root.setView(view, wparams, panelParentView);
    }
}

现在我们继续追根溯源,找到哪里调用这个方法,以及调用的时机,我们就可以进行相应的分析了,

  • 如果是在调用这个方法之前刷新,调用requestLayout,那ViewRootImpl还没有初始化,那mParent==null,就不会调用到ViewRootImpl.requestLayout了,也就不会抛出异常了,
  • 如果在这个方法之后,调用了requestLayout,那就会调用到ViewRootImpl.requestLayout里,并且根据线程判断,非主线程刷新就会抛出异常了

那接着找哪里调用了WindowManagerGlobal.addView,

//在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是个单例的

继续向上查找,在activity中找到了蛛丝马迹

public void setVisible(boolean visible) {
    if (mVisibleFromClient != visible) {
        mVisibleFromClient = visible;
        if (mVisibleFromServer) {
            if (visible) makeVisible();
            else mDecor.setVisibility(View.INVISIBLE);
        }
    }
}
//WindowManagerImpl是WindowManager的唯一实现类,WindowManager.addView也就是WindowManagerImpl.addView
//这里调用了WindowManagerImpl的addView
void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

继续找哪里调用了makeVisible(),或setVisible() 我们发现在ActivityThread.handleResumeActivity中调用了这个方法

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        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 (r.mPreserveWindow) {
            a.mWindowAdded = true;
            r.mPreserveWindow = false;
        }
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                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);
            }
        }

    } else if (!willBeVisible) {
        if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
        r.hideForNow = true;
    }

    if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
        
        if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward);
        WindowManager.LayoutParams l = r.window.getAttributes();
        if ((l.softInputMode
                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                != forwardBit) {
            l.softInputMode = (l.softInputMode
                    & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                    | forwardBit;
            if (r.activity.mVisibleFromClient) {
                ViewManager wm = a.getWindowManager();
                View decor = r.window.getDecorView();
                wm.updateViewLayout(decor, l);
            }
        }

        r.activity.mVisibleFromServer = true;
        mNumVisibleActivities++;
        if (r.activity.mVisibleFromClient) {
            r.activity.makeVisible();
        }
    }

    r.nextIdle = mNewActivities;
    mNewActivities = r;
    Looper.myQueue().addIdleHandler(new Idler());
}

这个方法我们都知道是在acitvity的onResume回调时调用的,也就是在onResume的时候,才会有ViewRootImpl的初始化,

这个时候我们就知道了原来是这么个流程

  • onCreate的时候ViewRootImpl还没创建,调用requestLayout并不会调用ViewRootImpl的requestLayout,不会checkThread,也不会抛异常
  • 有了延时之后,已经执行到onResume了,ViewRootImpl创建完成,这个时候子线程刷新UI,就会checkThread了