美文网首页我眼中的AndroidAndroid知识Android开发
Android能否在子线程中更新UI呢?

Android能否在子线程中更新UI呢?

作者: grr1314 | 来源:发表于2017-01-08 16:19 被阅读222次

    如题,Android能否在子线程中更新UI呢?这是一道面试题。那么这道题应该怎么去回答呢?在此我给出个人答案:“Android是不允许在子线程中更新UI的,但是在某种特殊情况下子线程是可以更新UI的”。为什么这么说呢?下面我们来看一个例子:

    MainActivity

    package example.lc.com.uicheckdemo;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.widget.TextView;
    
    public class MainActivity extends AppCompatActivity {
    
        private TextView text;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            text= (TextView) findViewById(R.id.text);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    text.setText("在子线程中更新的UI");
                }
            }).start();
    
        }
    }
    

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="example.lc.com.uicheckdemo.MainActivity">
    
        <TextView
            android:layout_centerInParent="true"
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!" />
    </RelativeLayout>
    

    例子很简单就是一个Activity和一个布局,在onCreate中写了一个Thread并且在子线程中更新了UI,但是程序不会报错,不信大家可以去试一下。但是,将代码做如下修改:
    MainActivity

    package example.lc.com.uicheckdemo;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.widget.TextView;
    
    public class MainActivity extends AppCompatActivity {
    
        private TextView text;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            text= (TextView) findViewById(R.id.text);
            new Thread(new Runnable() {
                @Override
                public void run() {
                   try { 
                       Thread.sleep(200); 
                  } catch (InterruptedException e) 
                  { 
                   e.printStackTrace(); 
                      }
                    text.setText("在子线程中更新的UI");
                }
            }).start();
    
        }
    }
    

    再次运行程序就会崩溃,logcat报错如下:

    01-07 18:21:11.207 1476-2957/example.lc.com.uicheckdemo E/AndroidRuntime: FATAL EXCEPTION: Thread-96 Process: example.lc.com.uicheckdemo, PID: 1476 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6118) 
    at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:848) 
    at android.view.View.requestLayout(View.java:16431) 
    at android.view.View.requestLayout(View.java:16431) 
    at android.view.View.requestLayout(View.java:16431) 
    at android.view.View.requestLayout(View.java:16431)
    at android.view.View.requestLayout(View.java:16431)
    at android.view.View.requestLayout(View.java:16431) 
    at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:352)
    at android.view.View.requestLayout(View.java:16431) 
    at android.widget.TextView.checkForRelayout(TextView.java:6600) 
    at android.widget.TextView.setText(TextView.java:3813) 
    at android.widget.TextView.setText(TextView.java:3671) 
    at android.widget.TextView.setText(TextView.java:3646)
    at example.lc.com.uicheckdemo.MainActivity$1$override.run(MainActivity.java:25) 
    at example.lc.com.uicheckdemo.MainActivity$1$override.access$dispatch(MainActivity.java) 
    at example.lc.com.uicheckdemo.MainActivity$1.run(MainActivity.java:0) 
    at java.lang.Thread.run(Thread.java:841)
    

    那么这到底是为什么呢?这里我们提出问题,本文下面所有的内容都是要去解答这个问题的,接下来就正式开始我们今天的探索。

    **
    从错误信息入手寻找问题的根源
    **
    从logcat显示的信息中我们发现了很有用的几行:

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

    这几行信息直接告诉了我们错误是从ViewRootImpl的checkThread方法中报出来的,那么我们就去证实一下,打开checkThread方法:

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

    是的错误就是这个方法报出的,mThread指的是UI线程而Thread.currentThread()则是当前线程。由此,我们知道了Android中更新UI的时候检查线程的操作是在ViewRootImpl中进行的。到这里我们还是没有办法去解答我们提出的问题,我们接着分析。

    ViewRootImpl是何时创建的?

    想要知道ViewRootImpl是在哪里创建的,我们要先要找到handleResumeActivity()方法,这个方法在ActivityThread中,代码如下:

     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);
                    }
               //代码省略
        }
    

    在上面代码中,有两个需要我们注意的地方。第一个就是 View decor = r.window.getDecorView();,这个decor 就是我们熟知的DecorView,但是这不是我们今天研究的重点。第二个是 ViewManager wm = a.getWindowManager();,这个才是今天真正的重点。ViewManager是一个接口其定义了addView、updateViewLayout、removeView三个方法,a.getWindowManager是一个WindowManager,WindowManager也是一个接口并且继承了ViewManager,也就是说wm 是一个WindowManager。WindowManager是一个借口它的实现类是WindowManagerImpl, 也就是说wm.addView(decor, l);这句话调用的是WindowManagerImpl中的addView方法,我们看下WindowManagerImpl代码:

    package android.view;
    
    import android.annotation.NonNull;
    import android.os.IBinder;
    
    public final class WindowManagerImpl implements WindowManager {
        private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
        private final Display mDisplay;
        private final Window mParentWindow;
    
        private IBinder mDefaultToken;
    
        public WindowManagerImpl(Display display) {
            this(display, null);
        }
     //代码省略
        @Override
        public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            applyDefaultToken(params);
            mGlobal.addView(view, params, mDisplay, mParentWindow);
        }
     //代码省略
    }
    

    看到在WindowManagerImpl 的addView方法中,又调用了WindowManagerGlobal的addView方法,接着打开WindowManagerGlobal类,代码如下:

    package android.view;
     //代码省略
    public final class WindowManagerGlobal {
        private static final String TAG = "WindowManager";
    
     //代码省略
    
        private Runnable mSystemPropertyUpdater;
    
        private WindowManagerGlobal() {
        }
    
        public static void initialize() {
            getWindowManagerService();
        }
    
        public static WindowManagerGlobal getInstance() {
            synchronized (WindowManagerGlobal.class) {
                if (sDefaultWindowManager == null) {
                    sDefaultWindowManager = new WindowManagerGlobal();
                }
                return sDefaultWindowManager;
            }
        }
    
    
     //代码省略
    
        public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
            if (view == null) {
                throw new IllegalArgumentException("view must not be null");
            }
            if (display == null) {
                throw new IllegalArgumentException("display must not be null");
            }
            if (!(params instanceof WindowManager.LayoutParams)) {
                throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
            }
    
            final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
            if (parentWindow != null) {
                parentWindow.adjustLayoutParamsForSubWindow(wparams);
            } else {
                // If there's no parent, then hardware acceleration for this view is
                // set from the application's hardware acceleration setting.
                final Context context = view.getContext();
                if (context != null
                        && (context.getApplicationInfo().flags
                                & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
                    wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
                }
            }
    
            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);
            }
    
            // 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;
            }
        }
    
        //代码省略
    
    
    }
    

    以上是WindowManagerGlobal类,只给出了addView方法其他代码省略了。我们仔细的来看下addView方法,再把无用的代码省去,结果如下:

        public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
            //代码省略
            ViewRootImpl root;
            //代码省略
    
                int index = findViewLocked(view, false);
                root = new ViewRootImpl(view.getContext(), display);
    
                view.setLayoutParams(wparams);
    
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
           //代码省略
        }
    

    好了代码省去的比较多,不过我们终于找到了我们想要的代码了。 root = new ViewRootImpl(view.getContext(), display); 到这ViewRootImpl就创建完了!也就是说ViewRootImpl是在WindowManagerGlobal中的addView方法中创建的!
    那么现在我们来尝试回答一下我们提出的问题。合理的说法是让线程睡眠200ms以后再次醒来此时onResume方法已经被调用ViewRootImpl已经创建完成,此时可以检查线程了。

    相关文章

      网友评论

      本文标题: Android能否在子线程中更新UI呢?

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