专栏名称: Android_Gaomh
目录
51好读  ›  专栏  ›  Android_Gaomh

Android 子线程能否更新UI

Android_Gaomh  · CSDN  ·  · 2019-10-17 21:53

正文

Android 子线程能否更新UI

前言

作为一只安卓开发人员,我们应该在开始学习安卓的时候就被告知,UI修改只能在主线程中进行(UI线程),为啥?
不用知道为啥这么记好了。
过了一段时间的学习,你可能会产生疑问,主线程,子线程不都是线程吗,主线程不就是Activity创建的时候
的ActivityThread吗,不就是生命周期都是在他这里实现的吗,不就是一个特殊的线程吗,归根结底还不就是
谷歌的大哥们做了限制,不让你在子线程中进行修改ui,那么问题就来了,我能不能逃避这种检测机制呢?
肯定可以啊,要不我没得写了。。。
先说说为什么谷歌的大佬们会这么设计把。你设想一下,如果说每个线程都能去操作你的UI,前后顺序再来个随机,
界面会出现什么乱七八糟的东西,谁也说不好。

首先明确一点,其实子线程中是可以进行UI的操作的。

假如说,你在子线程中进行UI的操作,会导致你的程序直接挂掉,异常信息如下:

Process: com.letv.myapplication, PID: 19088
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7286)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1155)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.widget.TextView.checkForRelayout(TextView.java:8526)
        at android.widget.TextView.setText(TextView.java:5392)
        at android.widget.TextView.setText(TextView.java:5248)
        at android.widget.TextView.setText(TextView.java:5205)

对于异常的理解是什么,谷歌的大神们认为你的这个操作已经不符合规则了,不能再让你玩这个程序了,强行停止掉。在源码中搜索 Only the original thread that created a view hierarchy can touch its views.在ViewRootImpl.java中存在下面的代码。
该代码出自 framework/base/core/java/android/view/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是在什么时候赋值的呢?

 public ViewRootImpl(Context context, Display display) {
        ......
        mThread = Thread.currentThread();
        ......
    }

本着删繁就简大家一眼看到的原则,除了有用的代码其余全省略号。
我们能够知道他是在构造方法里面被赋值。
继续看下去,WindowManagerGlobal.java中
/frameworks/base/core/java/android/view/WindowManagerGlobal.java

  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);
        ......
    }

WindowManagerImpl.java中
/frameworks/base/core/java/android/view/WindowManagerImpl.java

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

好,我们现在找到了问题在什么地方,接下来呢,先讲点别的哈。
Android activity的启动,其实是通过AMS创建activityThread,然后各大生命周期在主线程中一步一步执行。AMS首先会调用ActivityThread的performLaunchActivity。
上面WindowManagerImpl.java 中addView()的调用

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
        ......
        // TODO Push resumeArgs into the activity for consideration
        ActivityClientRecord r = performResumeActivity(token, clearHide);

       ......
            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);
                }
        ......
    }

handleResumeActivity,这个方法其实就是activityThread中在我们看到的生命周期后面默默奉献的方法。
本着多余代码一点不留的原则,大家也看到了出来addview(),我还留下了一行代码,我们不妨点进去一探究竟。

    public final ActivityClientRecord performResumeActivity(IBinder token,
            boolean clearHide) {
        ......
        activity.performResume();
        ......
              
       
    }

    final void performResume() {
        ......
        mInstrumentation.callActivityOnResume(this);
        ......
    }

我明白了,这个就是生命周期onResume的回调啊。还有啊,还有啊,看清楚了,咱们的addView和检测都是在onResume的回调执行结束后啊,那时候才调用的啊。

那么在那个之前我在onCreate,onStart,onResume 中 我开一个子线程,去更新UI可不可以,额,要是不行的话我好像写了半天就全都是废话了。

在onCreate中启动一个子线程

new Thread() {
            @Override
            public void run() {
                super.run();
                Log.v("gaomh3","new Thread()");
                textView.setText("子线程");
                textView.setTextSize(50);
            }
        }.start();

发现UI可以进行修改并且程序运行正常。

2019-10-17 17:36:27.117 27750-27750/? V/gaomh3: onCreate
2019-10-17 17:36:27.155 27750-27771/com.letv.myapplication V/gaomh3: new Thread()
2019-10-17 17:36:27.156 27750-27750/com.letv.myapplication V/gaomh3: onStart
2019-10-17 17:36:27.165 27750-27750/com.letv.myapplication V/gaomh3: onResume
2019-10-17 17:36:29.193 27750-27750/com.letv.myapplication V/gaomh3: onMeasure
2019-10-17 17:36:29.237 27750-27750/com.letv.myapplication V/gaomh3: onMeasure
2019-10-17 17:36:29.238 27750-27750/com.letv.myapplication V/gaomh3: onLayout
2019-10-17 17:36:29.269 27750-27750/com.letv.myapplication V/gaomh3: onDraw

查看log打印发现,即使你在oncreate中开启子线程修改Ui真正的UI操作也是在onResume之后,addview完成以后,此时将你的赋值直接刷新到ui上面。检测也才刚刚开启。调用invalidateChildInParent(); 所以当你在子线程进行ui修改的时候,并我没有执行invalidateChildInParent();也同样不会触发相关的检测。这样就被我们蒙混过关了。

对比一下

        new Thread() {
            @Override
            public void run() {
                super.run();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.v("gaomh3","new Thread()");
                textView.setText("子线程");
                textView.setTextSize(50);
            }
        }.start();
2019-10-17 17:37:44.896 27917-27917/? V/gaomh3: onCreate
2019-10-17 17:37:44.942 27917-27917/? V/gaomh3: onStart
2019-10-17 17:37:44.944 27917-27917/? V/gaomh3: onResume
2019-10-17 17:37:46.986 27917-27917/com.letv.myapplication V/gaomh3: onMeasure
2019-10-17 17:37:47.039 27917-27917/com.letv.myapplication V/gaomh3: onMeasure
2019-10-17 17:37:47.041 27917-27917/com.letv.myapplication V/gaomh3: onLayout
2019-10-17 17:37:47.057 27917-27917/com.letv.myapplication V/gaomh3: onDraw
2019-10-17 17:37:47.945 27917-27939/com.letv.myapplication V/gaomh3: new Thread()
  Process: com.letv.myapplication, PID: 27917
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7286)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1155)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.view.View.requestLayout(View.java:21926)
        at android.widget.TextView.checkForRelayout(TextView.java:8526)
        at android.widget.TextView.setText(TextView.java:5392)
        at android.widget.TextView.setText(TextView.java:5248)
        at android.widget.TextView.setText(TextView.java:5205)

崩溃了。。。

 @Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        ......

        return null;
    }

好,给个结论,在onCreate中我们开启子线程进行ui操作.在我看来,其实相当于改变初始属性,没有执行到刷新操作,没有执行invalidateChildInParent(),不会触发监测机制,onResume结束后,一切准备就绪,你再想改ui,在子线程中,对不起,谷歌不会允许。(其实也不是完全不允许啊,我再子线程重新创建一个ViewRoot,这样mThread不就又和调用线程一样了吗,我又能通过检测。)

   new Thread() {
            @Override
            public void run() {
                super.run();
                Looper.prepare();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.v("gaomh3","new Thread()");
                TextView textView =new TextView(MainActivity.this);
                textView.setText("子线程");
                textView.setTextSize(50);
                WindowManager windowManager = MainActivity.this.getWindowManager();
                WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                        200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                        WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
                windowManager.addView(textView, params);
                Looper.loop();

            }
        }.start();

理论可行啊,跑起来也不崩溃,但是问题是你这种写法不是相当于你想把子线程做成一个“主线程”吗?哪个大哥写代码这么写,我服。

结论

子线程中不可以进行UI操作,这句话我们应该理解成当onResume结束后,一切准备就绪之后,子线程无法修改在UI。即使你在之前开启子线程修改ui,也不会立刻刷新,显示出来。

参考文献

Android子线程真的不能更新UI么 .
Android中Activity启动过程探究 .