美文网首页AndroidDevAndroid技术知识Android知识
Android 真的不能在子线程更新UI吗?

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

作者: acemurder | 来源:发表于2017-02-19 12:17 被阅读1029次

    写过Android 代码的同学应该都听过Android不能在子线程更新UI,只能在主线程即UI线程处理视图。

    public class MainActivity extends AppCompatActivity {
        private TextView textView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            textView = (TextView) findViewById(tv);
            new Thread(){
                @Override
                public void run() {
                    super.run();
                    textView.setText("not from UI thread!!!");
                }
            }.start();
        }
    }
    

    猜一下运行结果呢? 抛出CalledFromWrongThreadException吗? No!No!No!

    不信的话可以试下。
    这是为啥?说好的不能子线程更新UI呢! 这个我们等下再说

    CalledFromWrongThreadException

    首先我们测试一下:

    public class MainActivity extends AppCompatActivity {
        private Button button;
        private TextView textView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            button = (Button) findViewById(R.id.btn);
            textView = (TextView) findViewById(R.id.tv);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new Thread(){
                        @Override
                        public void run() {
                            super.run();
                            textView.setText("not from UI Thread");
                        }
                    }.start();
                }
            });
        }
    }
    

    我们在点击button时,给textView 设置文字,运行App,点击button时我们会看到这个异常

    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    说的很清楚,只有创建视图层次结构的原始线程可以访问它的视图。

    通过查看Android是源码,我们发现这个异常是在 android.view.ViewRootImpl 抛出来的

    void checkThread() {
            if (mThread != Thread.currentThread()) {
                throw new CalledFromWrongThreadException(
                        "Only the original thread that created a view hierarchy can touch its views.");
            }
        }
    

    当Activity对象被创建完毕后,会创建ViewRootImpl对象,mThread 是在ViewRootImpl 的构造方法里这样初始化的
    所以mThread被赋值成主线程。

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

    checkThread() 方法是怎样被调用的呢

    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                          at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
                          at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1083)
                          at android.view.ViewGroup.invalidateChild(ViewGroup.java:5205)
                          at android.view.View.invalidateInternal(View.java:13656)
                          at android.view.View.invalidate(View.java:13620)
                          at android.view.View.invalidate(View.java:13604)
                          at android.widget.TextView.checkForRelayout(TextView.java:7347)
                          at android.widget.TextView.setText(TextView.java:4480)
                          at android.widget.TextView.setText(TextView.java:4337)
                          at android.widget.TextView.setText(TextView.java:4312)
                          at com.acemurder.test.MainActivity$1$1.run(MainActivity.java:27)
    

    这样更直白一点

    com.acemurder.test.MainActivity$1$1.run
    
      -> android.widget.TextView.setText
    
        -> android.widget.TextView.checkForRelayout
    
          -> android.view.View.invalidate
    
            -> android.view.ViewGroup.invalidateChild
    
              -> android.view.ViewRootImpl.invalidateChildInParent
    
                -> android.view.ViewRootImpl.invalidateChild
    
                  -> android.view.ViewRootImpl.checkThread
    

    当运行到``checkThread()时候,Thread.currentThread()我们在OnCreate()`方法里创建的一个子线程,所以抛出来了异常。

    通过创建ViewRootImpl在子线程更新UI

    通过方才的分析,我们发现异常的原因是TextViewViewRootImpl是在我们创建Activity的时候创建的,所以我们能不能通过给一个View单独的一个ViewRootImpl呢?

    button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new Thread(){
                        @Override
                        public void run() {
                            super.run();
                            Looper.prepare();
                            TextView tv = new TextView(MainActivity.this);
                            tv.setText("not from UI Thread");
                            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(tv, params);
                            Looper.loop();
                        }
                    }.start();
                }
            });
    

    看结果


    等等,不是说好的单独创建一个ViewRootImpl吗?别急,我们来理一下
    通过看源码发现,WindowManager是一个抽象类,我们看他的子类WindowManagerImpl

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

    addView方法最终调用了mGlobaladdView方法这里的mGlobalprivate final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();这样得到的,我们看下它的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.
                if (mSystemPropertyUpdater == null) {
                    mSystemPropertyUpdater = new Runnable() {
                        @Override public void run() {
                            synchronized (mLock) {
                                for (int i = mRoots.size() - 1; i >= 0; --i) {
                                    mRoots.get(i).loadSystemProperties();
                                }
                            }
                        }
                    };
                    SystemProperties.addChangeCallback(mSystemPropertyUpdater);
                }
    
                int index = findViewLocked(view, false);
                if (index >= 0) {
                    if (mDyingViews.contains(view)) {
                        // Don't wait for MSG_DIE to make it's way through root's queue.
                        mRoots.get(index).doDie();
                    } else {
                        throw new IllegalStateException("View " + view
                                + " has already been added to the window manager.");
                    }
                    // The previous removeView() had not completed executing. Now it has.
                }
    
                // If this is a panel window, then find the window it is being
                // attached to for future reference.
                if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                        wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                    final int count = mViews.size();
                    for (int i = 0; i < count; i++) {
                        if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                            panelParentView = mViews.get(i);
                        }
                    }
                }
    
                root = new ViewRootImpl(view.getContext(), display);
    
                view.setLayoutParams(wparams);
    
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
            }
    

    看见了吧,在这里创建了一个ViewRootImpl对象,所以在子线程更新UI成功。

    不创建ViewRootImpl在子线程更新UI

    逼逼了这么久,终于说到文章开始那个问题了。
    我们OnCreate()里直接开启一个子线程去更新UI,并没有创建单独的ViewRootImpl对象啊?
    原因就在于ViewRootImpl的建立时间,它是在ActivityThread.Javafinal void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward)里创建的。

    final void handleResumeActivity(IBinder token,
                boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
            ······      
            if (r != null) {
                final Activity a = r.activity;
    
                if (localLOGV) Slog.v(
                    TAG, "Resume " + r + " started activity: " +
                    a.mStartedActivity + ", hideForNow: " + r.hideForNow
                    + ", finished: " + a.mFinished);
    
                final int forwardBit = isForward ?
                        WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
    
               
                boolean willBeVisible = !a.mStartedActivity;
                if (!willBeVisible) {
                    try {
                        willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
                                a.getActivityToken());
                    } catch (RemoteException e) {
                        throw e.rethrowFromSystemServer();
                    }
                }
                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 (r.mPreserveWindow) {
                        a.mWindowAdded = true;
                        r.mPreserveWindow = false;
                        ViewRootImpl impl = decor.getViewRootImpl();
                        if (impl != null) {
                            impl.notifyChildRebuilt();
                        }
                    }
                 }
                    ······
     }
    

    原因就是在Activity的onResume之前ViewRootImpl实例没有建立,所以没有ViewRootImpl.checkThread检查。而textView.setText时设定的文本却保留了下来,所以当ViewRootImpl真正去刷新界面时,就把"not from UI Thread"刷了出来!

    相关文章

      网友评论

      • 8f22e30fc811:那是不是只要在onresume之前更新UI都可以
        cengdong:@轩情余思 应该说是概率性的,因为子线程和主线程是并行执行的
      • 554d675e124f:很佩服喜欢读源码的人

      本文标题:Android 真的不能在子线程更新UI吗?

      本文链接:https://www.haomeiwen.com/subject/lscjwttx.html