Android非UI线程更新UI的探索
inf
7年前
<p>众所周知,在Android中如果在非UI线程更新UI的话,会抛出异常:</p> <p>Only the original thread that created a view hierarchy can touch its views.</p> <p>因此我们很自然地认为只能在UI线程更新UI了。但是在实际开发中,有时可能有在非UI线程更新UI的需求,如:想通过非UI线程来预加载View。因此本文将探索在非UI线程更新UI的方式。</p> <h2>checkThread突破口</h2> <p>首先来找下突破口。从上面提到的异常开始切入,抛出该异常的代码如下: android.view.ViewRootImpl#checkThread</p> <pre> <code class="language-java">void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }</code></pre> <p>这个方法在View更新的一些关键操作中都会调用,如layout,invalid,focus等。而这里的判断条件是如果 ViewRootImpl 中的 mThread 的值和当前调用的线程不一样,就抛出异常。而 mThread 赋值是在 ViewRootImpl 构造时:</p> <pre> <code class="language-java">public ViewRootImpl(Context context, Display display) { // ... mThread = Thread.currentThread(); // ... }</code></pre> <p>这里是将 mThread 直接赋值为构造调用的当前线程。再看看 ViewRootImpl 的构造调用的地方是:</p> <p>android.view.WindowManagerGlobal#addView</p> <pre> <code class="language-java">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); } // ... }</code></pre> <p>这个 WindowManagerGlobal 其实就是 WindowManager 的具体实现。也就是android.view.WindowManager#addView,最终都会调用到这里。</p> <p>在平时 View 操作最多的 Activity 中,当 Activity resume 时系统会将 DecorView 添加到 Window 中,代码如下:</p> <p>android.app.ActivityThread#handleResumeActivity</p> <pre> <code class="language-java">final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { // ... ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; // ... 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); } } // ... } // ... }</code></pre> <p>这段逻辑概括起来就是:</p> <p>ActivityThread.handleResumeActivity -> WindowManagerGlobal.addView -> new ViewRootImpl -> ViewRootImpl.mThread = Thread.currentThread()</p> <p>这里都是UI线程调用的,而 ViewRootImpl.mThread 也就赋值为UI线程,因此在 Activity 中的 View,我们都是只能在UI线程更新的,如果在非UI线程更新的话,就无法通过 checkThread 检查。</p> <p>回到开头提到的问题,如果想在非UI线程更新UI,拆分下,大致分为两步:</p> <ol> <li>在非UI线程创建 View;</li> <li>在非UI线程更新 View。</li> </ol> <p>而这两步的关键都在怎么通过 checkThread 这个检查。</p> <h2>能否在非UI线程创建View?</h2> <p>首先来看第一个问题,能否在非UI线程创建View。</p> <p>从上面对 checkThread 的分析可以知道,checkThread 只存在于 ViewRootImpl 中,而ViewRootImpl 是当我们通过 WindowManager 向 Window 中添加 View 的时候才构造的一个 rootView。只要我们不向 Window 中添加 View,那么也就不会触发 checkThread。</p> <p>因此在非UI线程创建 View 理论上是可行的。无论是通过直接 new View,还是通过 LayoutInflater 。</p> <h2>能否在非UI线程更新View?</h2> <p>上面只是通过非UI线程来创建 View,那么在非UI更新 View 是否可行呢?这里就涉及到在更新UI时怎么通过 checkThread 的检查。</p> <p>从上面的分析可以得知,如果 ViewRootImpl.mThread 的值和当前更新UI调用的线程是一样的,那么就不会抛出异常。</p> <p>那么试想,如果 ViewRootImpl.mThread 的值是非UI线程,而且更新UI也是在同一个非UI线程中,那我们是不是就可以通过 checkThread 检查了呢?</p> <p>同时还有个问题,怎么将 ViewRootImpl.mThread 赋值为一个非UI线程?</p> <p>做过悬浮窗开发或者对 WMS 源码熟悉的应该知道,通过 Context 可以获得一个 WindowManager 对象,顾名思义,它就是用来操作 Window 的,Activity 也正是通过它显示在 Window 上的。</p> <p>结合上面的分析,只要我们将 WindowManager.addView 这一步放到非UI线程去做,那么 ViewRootImpl.mThread 必然指向的是当前调用的非UI线程,后续自然就可以在这个非UI线程去更新这个View了。</p> <h2>示例</h2> <p>下面通过一个示例来验证下上述的想法:</p> <p>一个简单的布局文件:</p> <pre> <code class="language-java"><?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </FrameLayout></code></pre> <p>非UI线程创建View和更新View的示例:</p> <pre> <code class="language-java">public static void showViewInNonUiThread(final Activity context) { final HandlerThread handlerThread = new HandlerThread("view_test"); handlerThread.start(); final Handler handler = new Handler(handlerThread.getLooper()); handler.post(new Runnable() { @Override public void run() { WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); lp.width = WindowManager.LayoutParams.MATCH_PARENT; lp.height = WindowManager.LayoutParams.WRAP_CONTENT; lp.gravity = Gravity.LEFT | Gravity.TOP; lp.format = PixelFormat.RGBA_8888; lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; lp.token = context.getWindow().getDecorView().getWindowToken(); lp.packageName = context.getPackageName(); WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); View contentView = LayoutInflater.from(context).inflate(R.layout.layout_test, null); final Button button = (Button) contentView.findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(v.getContext(), "test click", Toast.LENGTH_SHORT).show(); button.setText("test update ui3."); } }); button.setText("test update ui."); handler.post(new Runnable() { @Override public void run() { button.setText("test update ui2."); } }); windowManager.addView(contentView, lp); } }); }</code></pre> <p>将上述代码在 Activity 中运行下,可以正常的显示,而且点击事件,View 的更新都能正常执行,同时不影响UI线程的正常运行。因此该方案理论上是可行的。</p> <h2>稳定性</h2> <p>该方案本人在项目的测试环境上已经做过一些场景的应用,暂未发现任何问题,但是不排除有未知的风险,毕竟这不是常规的方案。</p> <p>因为 Android 系统默认所有的 View 都在UI线程更新,因此不存在线程间的同步问题。但是如果需要使用多线程来创建更新 View 的话,多线程的问题不得不考虑,比如静态变量的同步问题。</p> <p>如:Android 中使用最广泛的 TextView,其内部使用 android.text.TextLine 来表示一行文本,同是负责 TextView 的绘制,而这个类内部就有个静态的 cache :TextLine#sCached,不过好在其内部对 sCached 的所有操作都已经加锁。</p> <p>但是不排除系统中还有其他控件中有未知的坑。</p> <h2>应用</h2> <ol> <li>性能优化:对 View 的预加载,可以使用非UI线程来实例化 View ,然后放到UI线程去更新,节省 View 创建的开销。</li> <li>浮层:如果有些浮层本身存在大量复杂的绘制操作,而为了避免和UI绘制抢占资源,可以将其放到非UI线程来做,如:视频小窗。</li> </ol> <p> </p> <p>来自:https://techblog.toutiao.com/2017/08/16/untitled-5/</p> <p> </p>