更新ui时通常(后面会讲不调用的情况)会调用如下方法检查更新ui的线程,通常子线程更新ui报错就是viewrootimpl中这句代码导致的
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
mThread是创建viewrootimpl时的线程
public ViewRootImpl(Context context, Display display) {
mContext = context;
...
mThread = Thread.currentThread();
...
}
通常情况下这个mThread就是主线程,所以当更新ui的线程也是主线程时mThread != Thread.currentThread()
就不成立,所以不会报错。
viewrootimpl是什么时候创建的?
当我们通过windowmanager添加view时就会创建viewrootimpl。例如:
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
final TextView textView = (TextView) LayoutInflater.from(getApplicationContext()).inflate(R.layout.item, null);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
windowManager.addView(textView, layoutParams);
windowManager是WindowManagerImpl,WindowManagerImpl的addView调用了WindowManagerGlobal单例的addView方法,该方法内部new了viewRootImpl对象
WindowManagerImpl#addView
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
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) {
...
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;
}
}
所以,如果我们直接在子线程往windowmanager中添加view后,在这个子线程更新ui就不满足mThread != Thread.currentThread()
,所以就可以在子线程中更新UI。例如:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.update).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
update();
}
});
}
private void update() {
new Thread() {
@Override
public void run() {
super.run();
Looper.prepare();
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
final TextView textView = (TextView) LayoutInflater.from(getApplicationContext()).inflate(R.layout.item, null);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
windowManager.addView(textView, layoutParams);
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
textView.setText("主线程?" + (Thread.currentThread() == Looper.getMainLooper().getThread()) + " 当前线程:" + Thread.currentThread() + " " + System.currentTimeMillis());
handler.postDelayed(this, 1000);
}
}, 1000);
Looper.loop();
}
}.start();
}
}
还有其他情况下能在子线程更新UI吗?有的
更新ui时通常会调用requestLayout方法,最终调用到ViewRootImpl的requestLayout方法。
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
所以只要mHandlingLayoutInLayoutRequest==true
就不会检查线程。根据命名就可知道是用于标记是否正在布局。viewrootimpl中只有两个地方对其赋值,都是在performLayout中
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mHandlingLayoutInLayoutRequest = true;
//进行onMeasure
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
//进行onLayout
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mHandlingLayoutInLayoutRequest = false;
}
所以我们如果找准时机,在测量和布局时在子线程更新UI也不会报错。例如如下代码很多时候都不会报错
public class MainActivity extends Activity {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.update);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
update();
}
});
}
private void update() {
button.setText("hello");
new Thread() {
@Override
public void run() {
super.run();
button.setText("主线程?" + (Thread.currentThread() == Looper.getMainLooper().getThread()) + " 当前线程:" + Thread.currentThread() + " " + System.currentTimeMillis());
}
}.start();
}
}
布局代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="子线程更新view" />
LinearLayout>
update
中第一次设置setText
时会导致调用requestLayout
,这就导致会执行performLayout
,然后mHandlingLayoutInLayoutRequest
就被置为了true。这时立马开启子线程再次调用setText
时又会导致调用requestLayout
,但这时很多情况下还在布局中,所以mHandlingLayoutInLayoutRequest
还是true,这样就导致了不检查线程。不过这种情况下只有大多数时候能更新成功,有时也会报错。如果把update中第一个setText去掉,那么百分百会报错。
以上代码基于android api26,其他版本类似