QA
- CalledFromWrongThreadException 触发的判断逻辑是什么?
- 子线程可以更新 ui 线程创建的 view 吗?注意这里说的 ui 线程创建的 view。
- 如何在子线程创建 view 并能执行 view 的各种操作?
可以先看下之前的文章, android View 绘制过程及与Window的关联 对理解这些问题有很大帮助。
为什么不建议在子线程访问UI?
为了效率, UI 控件的实现是单线程的。正常情况下非UI线程访问会抛出 CalledFromWrongThreadException 异常。ViewRootImpl 在执行 view 的刷新操作时会进行线程的判断:
scss
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吗?
kotlin
override fun onResume() {
super.onResume()
val textView = findViewById(R.id.text_view)
thread { textView.setTextColor(Color.RED) }
}
这里更新 ui 线程创建的 textView 并没有报 CalledFromWrongThreadException,什么原因哪?
view 的刷新操作(本质就是调用 invalidate/requestLayout)最终都会调用到 ViewRootImpl 的 peformTraversals 。但 onResume 时,其实 ViewRootImpl 还没有创建,自然就不会进行实质上的 ui 更新。ActivityThread 在执行 handleResumeActivity 时,才会创建 ViewRootImpl ,参见 handleResumeActivity 章节。
ini
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
}
刷新调用过程中具体是在哪部分被中止的哪?
java
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 哪。
scss
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:
ini
//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刷新各种响应条件:
scala
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。