[关闭]
@natsumi 2017-04-27T03:11:51.000000Z 字数 5734 阅读 1408

Android 子线程真的不能更新UI么?

Android


Android中子线程真的不能更新UI吗?
http://blog.csdn.net/xyh269/article/details/52728861

多线程学习之--真的不能在子线程里更新UI吗?
http://blog.csdn.net/u010198148/article/details/51779567

实验代码

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. >
  6. <TextView
  7. android:id="@+id/main_tv"
  8. android:layout_width="wrap_content"
  9. android:layout_height="wrap_content"
  10. android:textSize="18sp"
  11. android:layout_centerInParent="true"
  12. />
  13. </RelativeLayout>
  1. public class MainActivity extends AppCompatActivity {
  2. private TextView main_tv;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_main);
  7. main_tv = (TextView) findViewById(R.id.main_tv);
  8. new Thread(new Runnable() {
  9. //不加sleep不会产生异常
  10. try {
  11. Thread.sleep(200);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. @Override
  16. public void run() {
  17. main_tv.setText("子线程中访问");
  18. }
  19. }).start();
  20. }
  21. }

1. 异常分析

在子线程中更新UI会抛出异常

  1. E/AndroidRuntime: FATAL EXCEPTION: Thread-29719
  2. Process: bupt.tiantian.uithreadtest, PID: 26822
  3. android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
  4. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6759)
  5. at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1077)
  6. //...

每一次修改UI,Android都会调用ViewRootImpl的requestLayout方法,跟踪里面的函数调用,会发现最终调用performTraversals()方法,的确是进行view的绘制。

在更新UI的时候,ViewRootImpl.requestLayout方法会先调用checkThread方法,检查当前是哪个线程访问的UI,如果不是主线程,那就会抛出异常:Only the original thread that created a view hierarchy can touch its views.

2. ViewRootImpl是什么时候创建的呢?

在ActivityThread中,我们找到handleResumeActivity方法,内部调用了performResumeActivity方法,逐层跟进会发现调用了activity.onResume()方法,所以performResumeActivity确实是resume的入口。

  1. //performResumeActivity方法
  2. //...
  3. //先调用performResume,跟进去会调用onResume
  4. r.activity.performResume();
  5. //...
  6. //之后会调用makeVisible
  7. r.activity.makeVisible();

makeVisible()方法往WindowManager中添加DecorView,那现在应该关注的就是WindowManager的addView方法了。

  1. void makeVisible() {
  2. if (!mWindowAdded) {
  3. ViewManager wm = getWindowManager();
  4. wm.addView(mDecor, getWindow().getAttributes());
  5. mWindowAdded = true;
  6. }
  7. mDecor.setVisibility(View.VISIBLE);
  8. }

WindowManager是一个接口来的,我们应该找到WindowManager的实现类才行,而WindowManager的实现类是WindowManagerImpl。

  1. @Override
  2. public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
  3. applyDefaultToken(params);
  4. mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
  5. }

里面调用了WindowManagerGlobal的addView方法,WindowManagerGlobal的addView方法:

  1. public void addView(View view, ViewGroup.LayoutParams params,
  2. Display display, Window parentWindow) {
  3. //...
  4. ViewRootImpl root;
  5. View panelParentView = null;
  6. synchronized (mLock) {
  7. //...
  8. root = new ViewRootImpl(view.getContext(), display);
  9. view.setLayoutParams(wparams);
  10. mViews.add(view);
  11. mRoots.add(root);
  12. mParams.add(wparams);
  13. }
  14. // do this last because it fires off messages to start doing things
  15. try {
  16. root.setView(view, wparams, panelParentView);
  17. } catch (RuntimeException e) {
  18. // BadTokenException or InvalidDisplayException, clean up.
  19. synchronized (mLock) {
  20. final int index = findViewLocked(view, false);
  21. if (index >= 0) {
  22. removeViewLocked(index, true);
  23. }
  24. }
  25. throw e;
  26. }
  27. }

得到了结论:ViewRootImpl是在WindowManagerGlobal的addView方法中创建的。

小结

ViewRootImpl的创建在onResume方法回调之后。
所以在ViewRootImpl的创建之前是可以在子线程修改UI的。另外,ViewRootImpl的创建是在makeVisible中完成的,那么是不是ViewRootImpl的创建之后,不可见的view仍然可以在子线程中修改呢,经过实验发现,ViewRootImpl的创建之后,只有可见属性为GONE的view可以在子线程中修改。

3. 子线程创建ViewRootImpl达成UI修改

checkThread中用ViewRootImpl对象的属性mThread和调用requestLayout方法企图修改UI的线程进行比较,看是不是同一个线程。

找到构造方法,发现mThread在ViewRootImpl对象构造时被赋值为构造它的线程。

  1. public ViewRootImpl(Context context, Display display) {
  2. mContext = context;
  3. mWindowSession = WindowManagerGlobal.getWindowSession();
  4. mDisplay = display;
  5. mBasePackageName = context.getBasePackageName();
  6. mThread = Thread.currentThread();
  7. //...
  8. }

异常的原因是主线程中构造了ViewRootImpl,而子线程去调用requestLayout。

所以构造ViewRootImpl对象的线程和修改UI的线程一致的话,是不是就可以逃过checkThread呢?

修改代码如下:

  1. public class MainActivity extends AppCompatActivity {
  2. private TextView main_tv;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_main);
  7. main_tv = (TextView) findViewById(R.id.main_tv);
  8. main_tv.setText("主线程中访问");
  9. new Thread(new Runnable() {
  10. @Override
  11. public void run() {
  12. try {
  13. Thread.sleep(200);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. // main_tv.setText("子线程中访问");
  18. // System.out.println(main_tv.getText());
  19. TextView tx = new TextView(MainActivity.this);
  20. tx.setText("子线程");
  21. tx.setBackgroundColor(Color.WHITE);
  22. ViewManager viewManager = MainActivity.this.getWindowManager();
  23. WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
  24. 200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
  25. WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
  26. viewManager.addView(tx,layoutParams);
  27. }
  28. }).start();
  29. }
  30. }

还是异常了,原因是没有创建Looper

  1. 04-27 10:47:18.054 4333-4420/bupt.tiantian.uithreadtest E/AndroidRuntime: FATAL EXCEPTION: Thread-30665
  2. Process: bupt.tiantian.uithreadtest, PID: 4333
  3. java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

在子线程中加上Looper.prepare()和Looper.loop(),然后成功了~

  1. new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. try {
  5. Thread.sleep(200);
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. // main_tv.setText("子线程中访问");
  10. // System.out.println(main_tv.getText());
  11. Looper.prepare();
  12. TextView tx = new TextView(MainActivity.this);
  13. tx.setText("子线程");
  14. tx.setBackgroundColor(Color.WHITE);
  15. ViewManager viewManager = MainActivity.this.getWindowManager();
  16. WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
  17. 200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
  18. WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
  19. viewManager.addView(tx,layoutParams);
  20. Looper.loop();
  21. }
  22. }).start();

这就是Android为我们设计的单线程模型,每个根视图ViewRootImpl只能由创建它的线程更新其中的UI。子线程可以更新UI,但是需要创建子线程的根视图,并添加到WindowManager,还要创建子线程的Looper。以上条件都满足时,它可以修改它自己创建的根视图中的UI。

View类的属性mAttachInfo.mRootView记录了The top view of the hierarchy

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注