更新ui时一般(后面会讲不调用的状况)会调用以下方法检查更新ui的线程,一般子线程更新ui报错就是viewrootimpl中这句代码致使的java
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
mThread是建立viewrootimpl时的线程android
public ViewRootImpl(Context context, Display display) { mContext = context; ... mThread = Thread.currentThread(); ... }
一般状况下这个mThread就是主线程,因此当更新ui的线程也是主线程时mThread != Thread.currentThread()
就不成立,因此不会报错。web
viewrootimpl是何时建立的?
当咱们经过windowmanager添加view时就会建立viewrootimpl。例如:api
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对象app
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。例如:ide
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(); } }
效果图:
svg
还有其余状况下能在子线程更新UI吗?有的oop
更新ui时一般会调用requestLayout方法,最终调用到ViewRootImpl的requestLayout方法。布局
public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
因此只要mHandlingLayoutInLayoutRequest==true
就不会检查线程。根据命名就可知道是用于标记是否正在布局。viewrootimpl中只有两个地方对其赋值,都是在performLayout中post
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(); } }
布局代码 <?xml version="1.0" encoding="utf-8"?> <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,其余版本相似