Android能否在子线程中更新UI呢?
2017-01-03 03:30
507 查看
如题,Android能否在子线程中更新UI呢?这是一道面试题。那么这道题应该怎么去回答呢?在此我给出个人答案:“Android是不允许在子线程中更新UI的,但是在某种特殊情况下子线程是可以更新UI的”。为什么这么说呢?下面我们来看一个例子:
MainActivity
activity_main.xml
例子很简单就是一个Activity和一个布局,在onCreate中写了一个Thread并且在子线程中更新了UI,但是程序不会报错,不信大家可以去试一下。但是,将代码做如下修改:
MainActivity
再次运行程序就会崩溃,logcat报错如下:
那么这到底是为什么呢?这里我们提出问题,本文下面所有的内容都是要去解答这个问题的,接下来就正式开始我们今天的探索。
**
从logcat显示的信息中我们发现了很有用的几行:
这几行信息直接告诉了我们错误是从ViewRootImpl的checkThread方法中报出来的,那么我们就去证实一下,打开checkThread方法:
是的错误就是这个方法报出的,mThread指的是UI线程而Thread.currentThread()则是当前线程。由此,我们知道了Android中更新UI的时候检查线程的操作是在ViewRootImpl中进行的。到这里我们还是没有办法去解答我们提出的问题,我们接着分析。
在上面代码中,有两个需要我们注意的地方。第一个就是 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代码:
看到在WindowManagerImpl 的addView方法中,又调用了WindowManagerGlobal的addView方法,接着打开WindowManagerGlobal类,代码如下:
以上是WindowManagerGlobal类,只给出了addView方法其他代码省略了。我们仔细的来看下addView方法,再把无用的代码省去,结果如下:
好了代码省去的比较多,不过我们终于找到了我们想要的代码了。
root = new ViewRootImpl(view.getContext(), display);
到这ViewRootImpl就创建完了!也就是说ViewRootImpl是在WindowManagerGlobal中的addView方法中创建的!
那么现在我们来尝试回答一下我们提出的问题。合理的说法是让线程睡眠200ms以后再次醒来此时onResume方法已经被调用ViewRootImpl已经创建完成,此时可以检查线程了。
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"> 4000 <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; import org.w3c.dom.Text; 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开发之——子线程中使用Toast或者更新UI
- Android常用的3种定时刷新UI的方法和子线程能否刷新UI那些事
- Android的子线程可以更新UI吗?
- Android 子线程创建消息队列更新UI
- Android子线程真的不可以更新UI吗???
- android 子线程真的不能更新ui吗
- Android—子线程更新UI问题( java.lang.RuntimeException: Can't create handler inside thread that has not cal)
- android中的UI视图更新不能放在子线程中操作
- 为什么我的子线程更新了 UI 没报错?借此,纠正一些Android 程序员的一个知识误区
- Android 中确定子线程不能更新UI吗?
- Android子线程居然可以更新UI?
- android 使用Handler UI线程和子线程通讯 更新UI
- Android线程模型解析(包括UI的更新)
- android异步更新UI
- Android中实现view的更新UI有两组方法
- Android的线程使用来更新UI----Thread、Handler、Looper、TimerTask等
- Android 完美解决自定义preference与ActivityGroup UI更新的问题
- Android线程模型解析(包括UI的更新)
- Android UI更新问题
- 如何利用Handler更新android的UI