[TOC]
概述 在我们学习多线程的路上,都会听到这样一句话:
不能在子线程里更新UI,UI更新必须在UI线程中:
why?为什么不能在子线程中更新UI?如果在子线程中更新UI会怎样?
为了模拟在子线程中更新UI的场景,简单地写了几行代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @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(); } }); }
运行,“理所当然”地崩溃了。打印错误日志如下:
1 2 3 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
1 2 3 4 5 6 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在哪里初始化的呢?接着看。
1 2 3 4 5 6 7 8 9 10 11 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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); } } else if (!willBeVisible) { if (localLOGV) Slog.v( TAG, "Launch " + r + " mStartedActivity set" ); r.hideForNow = true ; } ... }
wm.addView(decor, l);是他进行的View的加载,我们去看看他的实现方法,在WindowManager的实现类WindowManagerImpl里:
1 2 3 4 5 @Override public void addView (@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); mGlobal.addView(view, params, mDisplay, mParentWindow); }
发现他是调用WindowManagerGlobal的方法实现的,最后我们找到了最终实现addView的方法: WindowManagerGlobal.java#addView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public void addView (View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ... ViewRootImpl root; View panelParentView = null ; synchronized (mLock) { ... root = new ViewRootImpl (view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); } try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { synchronized (mLock) { final int index = findViewLocked(view, false ); if (index >= 0 ) { removeViewLocked(index, true ); } } throw e; } }
果然在这里,View的加载最后就是在这里实现的,而ViewRootImpl的实例化也在这里。所以如果我们在子线程中调用WindowManager的addView方法,是不是就可以成功更新UI呢?所以我修改了代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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); } }); ... }
运行,程序崩溃了,来看看错误日志:
1 java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
错误原因是没有启动Looper。原来是因为在ViewRootImpl类里新建了ViewRootHandler的实例mHandler,而mHandler要启动Looper才能处理相关信息。所以我们在代码里加入两行:
1 2 3 4 5 6 7 8 9 10 ... 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。