Android线程与 UI

808 阅读8分钟

1、单线程模型

Android 的用户界面(UI),是基于一个主线程(UI 线程)的单线程控制执行的,这意味着所有的触摸事件处理,布局计算,视图绘制都在这个线程中执行,为了保证 UI 的流畅性,系统要求这些操作必须快速完成,避免阻塞主线程出现 ANR 的情况。

1.1 ANR(Application not Responding)

当在主线程上执行操作耗时过长或被阻塞,以至于无法及时响应用户的输入或者系统事件,系统就会判定应用程序没有响应。

不同类型的 ANR 阈值:

  • 按键分发超时:对于用户的输入(触摸或者按键),如果应用在 5 秒内未作出响应,就会触发 ANR
  • 广播队列超时:对于 BroadCaseReceiver,如果在 10 秒内未完成onReceive方法,会触发 ANR
  • 服务超时: Service Timeout 如果一个服务在 20 秒内没有处理完它的启动操作或者绑定请求,也会触发 ANR。服务应避免在主线程执行耗时操作,以免影响到 UI 响应。

1.如何在其他线程访问 UI 线程

1.Activity.runOnUiThread(Runable)
2.View.post(Runable)
3.View.postDelay(Runable,long)
4.Handle(Looper.getMainLooper()).post(Runable)
5.Handle(Looper.getMainLooper()).postDelay(Runable,long)
7.协程 withContext(Dispathers.Main

2.为什么 UI 线程是不安全的

有以下几点原因:

  1. 单线程模型,如 1.单线程模型中所说
  2. 线程安全 API,Android的 UI 框架并未设计为多线程环境的,它们内部没有内置的同步机制来保证并发访问的安全性如 findviewbyid()
  3. 并发问题,因为 UI框架并不适合多线程环境,所以如果出现同时又多个线程尝试修改 UI 元素,就会出现线程不安全状态,这样会导致状态混乱
  4. 抛出异常,如果尝试再非 UI 线程直接修改 UI,系统会抛出异常:CallFromWrongThreadException

Android 实现 UI 更新有两组方法,Invalidate 和 PostInvalidate。

Invalidate 是只能在UI线程使用的,如果在子线程调用 Invalidate方法的同事在 UI 线程调用 Invalidate方法,


    /**
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     */
    public void invalidate() {
        invalidate(true);
    }

PostInvalidate 是在子线程中使用的

 /**
     * <p>Cause an invalidate to happen on a subsequent cycle through the event loop.
     * Use this to invalidate the View from a non-UI thread.</p>
     *
     * <p>This method can be invoked from outside of the UI thread
     * only when this View is attached to a window.</p>
     *
     * @see #invalidate()
     * @see #postInvalidateDelayed(long)
     */
    public void postInvalidate() {
        postInvalidateDelayed(0);
    }

   public void postInvalidateDelayed(long delayMilliseconds) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        //铜锅 handler 将刷新 UI的消息发送到主线程
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

实际上虽然可以在子线程中调用 PostInvalidate方法刷新 UI,但实际上 方法内部还是通过 Handler 将事件发送到子线程进行 UI更新。

3.子线程到底能不能更新 UI

通常情况下,如果在子线程尝试更新 UI,会出现报错

Only the original thread that created a view hierarchy can touch its views.

这个报错源自于android/view/ViewRootImpl.java 核心就是 checkThread 方法这里判断了线程,

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

3.1子线程更新 UI 的思路

3.1.1 在子线程创建 RootViewImpl

原理:

这里并没有判断线程是不是 UI 线程,而是判断了是不是mThread.那么当 nThread 就是当前线程(子线程)时,是不是就能在子线程修改 UI 了。

我们找到 mThread 的初始化位置,

 public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
            boolean useSfChoreographer) {
        mContext = context;
        mWindowSession = session;
        mDisplay = display;
        mBasePackageName = context.getBasePackageName();
     //!!!!mThread 在 ViewRootImpl 的构造函数中进行了初始化
        mThread = Thread.currentThread();
      //...省略其他代码
    }

那么证明,如果在子线程创建了 ViewRootImpl ,那么 mThread就会被初始化成子线程,即可在子线程进行 UI 操作。那么看下ViewRootImpl 在哪可以创建实例。

直接调用构造函数显然是不行的

调查发现,在调用windowmanager.addView 方法时会初始化 viewrootimpl,具体实现是在这个类中的addView 方法,代码如下:

android/view/WindowManagerGlobal.java 

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
        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, userId);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

也就是说,如果直接在子线程调用 windowmanager的 addview方法,viewrootimpl 中的 mthread就会被初始化成子线程,这样在 checkThread()方法时,就不会被阻拦了。

还有一个重要的点是:

ViewRootImpl 会创建Handler,而子线程是默认没有 Looper的,所以要在子线程创建自己的 Looper

代码demo

       thread {
           //创建 Looper,ViewRootImpl 中会使用 handler
            Looper.prepare()
            val textView = TextView(this)
            textView.text = "在子线程创建 ViewRootImpl"
            textView.setTextColor(ContextCompat.getColor(this,R.color.white))
            textView.setBackgroundColor(ContextCompat.getColor(this,R.color.primary_red))
            textView.setOnClickListener {
                textView.text = "修改了内容"
            }
           //此处创建了子线程的 ViewRootImpl
            windowManager.addView(textView, WindowManager.LayoutParams().apply {
                this.width = WindowManager.LayoutParams.WRAP_CONTENT
                this.height = WindowManager.LayoutParams.WRAP_CONTENT
            })
            Looper.loop()
        }

3.1.2 在 Resume回调函数之前修改 UI

如果 checkThread 方法不被执行,那么就不会报这个错误了。

如果在子线程修改 UI 时,ViewRootImpl 还没有被创建,就不会触发 checkThread 方法。因此,我们只要找到 acitivity本身创建viewrootimpl 也就是 activity本身调用 windowmanager 的 addView方法的调用时机,并在它之前修改 UI 即可

既然 AndroidUI是单线程模型,那么这块的逻辑就是在 ActivityThread 这个类中,

/**
 * This manages the execution of the main thread in an
 * application process, scheduling and executing activities,
 * broadcasts, and other operations on it as the activity
 * manager requests.
 *
   这个类管理了应用程序进程中的主线程的执行,组织和执行 activity,broadcast,和其他 activity 管理请求
 * {@hide}
 */
public final class ActivityThread extends ClientTransactionHandler
        implements ActivityThreadInternal {

不出意外,在handleResumeActivity方法内部找到了调用的地方

   @Override
    public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
            boolean isForward, String reason) {
        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        //此处调用 onResume 方法
        if (!performResumeActivity(r, finalStateRequest, reason)) {
            return;
        }
    

        final int forwardBit = isForward
                ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

        // If the window hasn't yet been added to the window manager,
        // and this guy didn't finish itself or start another activity,
        // then go ahead and add the window.
        boolean willBeVisible = !a.mStartedActivity;
        if (!willBeVisible) {
            willBeVisible = ActivityClient.getInstance().willActivityBeVisible(
                    a.getActivityToken());
        }
        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;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //调用 windowmanager的 addview 方法,创建 ViewRootImpl 对象
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }

    }

onResume方法的调用路径

activitythread.handleResumeActivity -》activitythread.performResumeActivity-》activity.performResume ->Instrumentation.callActivityOnResume -> activity.onResume

3.1.3 在子线程修改 UI 前手动调用 requestlayout()

对 view的修改通常会调用 requestlayout 方法,每一层级的又会调用父层级的 requestlayout 知道最顶层的 ViewRootImpl中,ViewRootImpl的 requestlayout 方法中调用了 checkthread.

但 requestlayout 有一个类似锁的机制,避免重复的布局请求,在请求requestlayout 时会设置一个标志位

    /**
     * <p>Indicates whether or not this view's layout will be requested during
     * the next hierarchy layout pass.</p>
     *
     * @return true if the layout will be forced during next layout pass
     */
    public boolean isLayoutRequested() {
        return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    }

在本次layout 结束前,其他的 requestlayout 方法不会被执行,因此不会触发 checkthread 方法

demo:

    //2.先执行requestLayout()再更新UI
        tv.setOnClickListener {
            it.requestLayout()
            thread {
                tv.text = "先执行requestLayout()再更新UI"
            }
        }

3.1.4 开启硬件加速,并让目标 view保持固定大小

当TargetSDK > 14时,默认是开启硬件加速的,即 android:hardwareAccelerated=“true”

在 textview 中,修改 text会触发 textview 的 checkforrelayout

  @UnsupportedAppUsage
    private void checkForRelayout() {
        //如果宽度不是自适应
        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
            // Static width, so try making a new text layout.

            int oldht = mLayout.getHeight();
            int want = mLayout.getWidth();
            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

            /*
             * No need to bring the text into view, since the size is not
             * changing (unless we do the requestLayout(), in which case it
             * will happen at measure).
             */
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                          false);

            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // 如果高度不是 wrapcontent或者 matchparent
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

                //如果豪赌没有变化
                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }
            }

            // We lose: the height has changed and we have a dynamic height.
            // Request a new view layout using our new text layout.
            requestLayout();
            invalidate();
        } else {
            // Dynamic width, so we have no choice but to request a new
            // view layout with a new text layout.
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }

从代码中可以看出,如果高度没有变化,不会调用 requestlayout 方法,而是直接去刷新。

invalidate 方法内部层层调用走到了 viewgroup 的 invalidateChild方法

onDescendantInvalidated 方法会递归调用到 ViewRootImpl的onDescendantInvalidated方法

然后内部调用ViewRootImpl的invalidate方法

3.2 其他方式

使用 Surface,Surface 的绘制流程不走 checkThread()

  val sf = findViewById<SurfaceView>(R.id.tv_splash)
        sf.holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceCreated(holder: SurfaceHolder) {
                thread {
                    while (true) {
                        HdyjLog.d(Thread.currentThread().name)
                        val canvas = holder.lockCanvas()
                        val random = java.util.Random()
                        val r = random.nextInt(255)
                        val g = random.nextInt(255)
                        val b = random.nextInt(255)
                        canvas.drawColor(Color.rgb(r, g, b))
                        holder.unlockCanvasAndPost(canvas)
                        SystemClock.sleep(500)
                    }
                }
            }

            override fun surfaceChanged(
                holder: SurfaceHolder,
                format: Int,
                width: Int,
                height: Int
            ) {
            }

            override fun surfaceDestroyed(holder: SurfaceHolder) {
            }

        })

参考文档:

Android子线程可以更新UI

Android 如何让子线程更新UI (仅分析,不建议使用)

Android子线程真的不能更新UI么