likes
comments
collection
share

android 子线程更新 view

作者站长头像
站长
· 阅读数 4

QA

  • CalledFromWrongThreadException 触发的判断逻辑是什么?
  • 子线程可以更新 ui 线程创建的 view 吗?注意这里说的 ui 线程创建的 view。
  • 如何在子线程创建 view 并能执行 view 的各种操作?

为什么不建议在子线程访问UI?

为了效率, UI 控件的实现是单线程的。正常情况下非UI线程访问会抛出 CalledFromWrongThreadException 异常。ViewRootImpl 在执行 view 的刷新操作时会进行线程的判断:

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        //检查当前执行的线程是不是UI线程
        checkThread();
        scheduleTraversals();
    }
}

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,这里的判断只是检测 mThread 是否是当前线程,并不是使用的 main 线程。那是不是说在子线程创建的 view 只要在本 view 访问就可以?

非UI线程真的不能更新UI吗?

override fun onResume() {
    super.onResume()
    val textView = findViewById(R.id.text_view)
    thread { textView.setTextColor(Color.RED) }
}

这里更新 ui 线程创建的 textView 并没有报 CalledFromWrongThreadException,什么原因哪?

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
    //处理Activity的onRestart onResume生命周期。
    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if (r != null) {
        if (r.window == null && !a.mFinished) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            //设置DecorView不可见
            decor.setVisibility(View.INVISIBLE);
           
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
          
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                //利用WindowManager添加DecorView。
                wm.addView(decor, l);
            }
        }
        ...
        //IPC调用,通知AMS Activity启动完成。
        ActivityManagerNative.getDefault().activityResumed(token);
    }
}

//上面的 addView 最终会调用到这里
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    //省略代码....
    root = new ViewRootImpl(view.getContext(), display); // 现在才创建 ViewRootImpl

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    
    root.setView(view, wparams, panelParentView); //关联 decorView
}

刷新调用过程中具体是在哪部分被中止的哪?

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
    final AttachInfo ai = mAttachInfo; // mAttachInfo 是在 ViewRootImpl 初始化时才创建
    final ViewParent p = mParent;
    if (p != null && ai != null && l < r && t < b) { // 此处条件不成立,不会触发invalidate
        final Rect damage = ai.mTmpInvalRect;
        damage.set(l, t, r, b);
        p.invalidateChild(this, damage);
    }
}

mAttachInfo 的创建参见 ViewRootImpl 的构造函数。

通过 WindowManager 创建子线程 View

线程检查只是判断当前线程与 view 创建的线程是否一致,如果在子线程创建 view,在子线程更新 view 哪。

thread {
    val textView = TextView(activity).apply { text = "Thread" }
    Looper.prepare()
    windowManager.addView(textView, WindowManager.LayoutParams())
    SystemClock.sleep(3000)
    textView.setBackgroundColor(Color.RED)
    Looper.loop()
}

在 activity 里执行上述代码,add 的 view 是主 window 的子窗口,这段代码就没有问题。为什么这里就可以触发 view 的正常绘制流程哪?

addView 时创建 ViewRootImpl:

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

    ViewRootImpl root;
    View panelParentView = null;
	...
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
	...
    }
}

而 ViewRootImpl 已经具备了屏幕/view刷新各种响应条件:

public class ViewRootImpl{
    View mView; 
	final ViewRootHandler mHandler = new ViewRootHandler(); // 接收view、各种事件,

  	public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session, WindowLayout windowLayout) {
        mContext = context;
        // 用于 view 刷新时线程检查,也就是说子线程也能创建并新view,只要view创建与更新是同一线程即可
        mThread = Thread.currentThread(); 
        // view 刷新时会判断 mAttachInfo 是否为空,如果为空其实就没走到检查线程阶段,所以也不会报子线程刷新ui问题
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);
        mChoreographer = Choreographer.getInstance();// 接收帧同步信号,触发 doTraversal
    }

    final class ViewRootHandler extends Handler {
        @Override
        public String getMessageName(Message message) {
            switch (message.what) {
                case MSG_INVALIDATE:
                    return "MSG_INVALIDATE";
                case MSG_INVALIDATE_RECT:
                    return "MSG_INVALIDATE_RECT";
                ......
            }
        }
    }
}

只不过这些实例现在是在子线程创建的而已。

使用这种方式在子线程创建 view 局限比较大,如果 View 刷新任务确实重,可以考虑使用 SurfaceView 来取代 View。