多线程学习之--真的不能在子线程里更新UI吗?

在我们学习多线程的路上,都会听到这样一句话:

不能在子线程里更新UI,UI更新必须在UI线程中

why?为什么不能在子线程中更新UI?如果在子线程中更新UI会怎样?
为了模拟在子线程中更新UI的场景,简单地写了几行代码:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (Button) findViewById(R.id.text);
        final Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
            ((TextView)findViewById(R.id.sv_view)).setText("子线程");
            }
        });
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                thread.start();
            }
        });
    }

运行,“理所当然”地崩溃了。打印错误日志如下:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                                                                     at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)
                                                                     at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:874)

崩溃的原因是:“Only the original thread that created a view hierarchy can touch its views.”意思是只有创建这个View布局层次的原始线程才可以改变这个View,看起来好像也并没有解释为什么子线程中不能更新UI。
而我们能看到产生异常崩溃的代码在ViewRootImpl这个类的checkThread方法,所以我们找到这个类:
ViewRootImpl.java#checkThread

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在哪里初始化的呢?接着看。

public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mDisplay = display;
        mBasePackageName = context.getBasePackageName();

        mDisplayAdjustments = display.getDisplayAdjustments();

        mThread = Thread.currentThread();//此处初始化
        ...
        }

在ViewRootImpl的构造方法里可以看到mThread指向当前线程的引用,意思是只要在子线程中创建ViewRootImpl的实例我们就可以避免抛异常了吗?于是楼主尝试在子线程中创建ViewRootImpl的实例可是发现并不能找到ViewRootImpl这个类。换个角度,如果不能在子线程更新UI,那主线程刷新UI是不是也要实例化这个类呢?而我们启动Activity绘制UI的方法在onResume方法里,所以我们找到Activity的线程ActivityThread类。
ActivityThread.java#handleResumeActivity

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
            ...
if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            } else if (!willBeVisible) {
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }
            ...
}

wm.addView(decor, l);是他进行的View的加载,我们去看看他的实现方法,在WindowManager的实现类WindowManagerImpl里:

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

发现他是调用WindowManagerGlobal的方法实现的,最后我们找到了最终实现addView的方法:
WindowManagerGlobal.java#addView

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

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            // Start watching for system property changes.
            ...
            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.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

果然在这里,View的加载最后就是在这里实现的,而ViewRootImpl的实例化也在这里。所以如果我们在子线程中调用WindowManager的addView方法,是不是就可以成功更新UI呢?所以我修改了代码:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (Button) findViewById(R.id.text);
        final Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                TextView tx = new TextView(MainActivity.this);
                tx.setText("子线程");
                tx.setBackgroundColor(Color.WHITE);
                ViewManager viewManager = MainActivity.this.getWindowManager();
                WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                        200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                        WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
                viewManager.addView(view,layoutParams);
            }
        });
        ...
        }

运行,程序崩溃了,来看看错误日志:

 java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

错误原因是没有启动Looper。原来是因为在ViewRootImpl类里新建了ViewRootHandler的实例mHandler,而mHandler要启动Looper才能处理相关信息。所以我们在代码里加入两行:

...
public void run() {
                Looper.prepare();
                TextView tx = new TextView(MainActivity.this);
                tx.setText("子线程");
                ...
                windowManager.addView(tx, params);
                Looper.loop();
            }
            ...

再次运行,成功了!

这里写图片描述

所以其实是可以在子线程中更新UI的,只要实例化ViewRootImpl。而为什么Android设计只能在UI线程中更新UI呢?大概是因为如果子线程更新UI可能导致线程之间抢夺资源和死锁等线程安全问题而不允许在子线程中更新UI。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值