Tears_fg

导航

Android中子线程真的不能更新UI吗?

今天讲一个老生常谈的问题,"Android中子线程真的不能更新UI吗?

AndroidUI访问是没有加锁的,这样在多个线程访问UI是不安全的。

所以Android中规定只能在UI线程中访问UI。子线程更新是不被允许的。

那么子线程访问UI会报错吗?

 首先,我们在布局文件随意定义一个textview:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:paddingTop="10dp"
        android:paddingBottom="10dp"
        android:textColor="@color/yellow"
        android:background="#000000"
        android:text="test"
        android:textSize="16sp"
        android:gravity="center"/>
</LinearLayout>

接着我们在activity中开了个子线程修改UI

 

class TestActivity :AppCompatActivity(R.layout.activity_test) {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Thread(object :Runnable{
            override fun run() {
                tv_test.text = "这是修改后的text"
            }

        }).start()
    }
}

 

展示,并没有报错:

 

紧接着,我们试试延时试试:

 

class TestActivity :AppCompatActivity(R.layout.activity_test) {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Thread(object :Runnable{
            override fun run() {
                Thread.sleep(3000)
                tv_test.text = "这是修改后的text"
            }

        }).start()
    }
}

 

运行试了下,3s后居然奔溃了,这是为啥呢?

查看错误日志:

 

经典问题出现了,查看源码可以看到,是在ViewRootImpl.checkThread中抛出的这个异常.

 

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

 

当访问 UI 时,ViewRootImpl 会调用 checkThread方法去检查当前访问 UI 的线程是否为创建 UI 的那个线程,如果不是。则会抛出异常。

那为啥要一定需要checkThread?根据handler的相关知识:

 

因为UI控件不是线程安全的。那为啥不加锁呢?

一是加锁会让UI访问变得复杂;

二是加锁会降低UI访问效率,会阻塞一些线程访问UI

所以干脆使用单线程模型处理UI操作,使用时用Handler切换即可。

 

 

疑问点:为什么一开始在 MainActivity onCreate 方法中创建一个子线程访问 UI,程序还是正常能跑起来呢???

 

我们可以看看tv_text.setText触发的调用流程:

 

TextView#setText
    -->View#requestLayout()
    满足条件:
       --> ViewParent # requestLayout
       --> ViewRootImpl # requestLayout
       
    -->View#invalidate
    -->  View#invalidate(boolean)
    -->  View#invalidateInternal //如果 if mAttachInfo 以及 mParent 都不为空
        -->ViewParent#invalidateChild
        -->ViewRootImpl#invalidateChild
        -->ViewRootImpl#invalidateChildInParent
              
---------------------
View#invalidateInternal
// Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);
        }

ViewRootImpl#invalidateChildInParent
        @Override
        public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
            checkThread();
            if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);

            if (dirty == null) {
                invalidate();
                return null;
            } else if (dirty.isEmpty() && !mIsAnimating) {
                return null;
            }

 

我们仔细看一下mThread,他这个错误信息并不是:

Only the UI Thread ... 而是 Only the original thread

这个mThread是什么?

ViewRootImpl的成员变量,我们重点应该关注它什么时候赋值的:

public ViewRootImpl(Context context, Display display) {
    mContext = context;
    mThread = Thread.currentThread();

}

ViewRootImpl构造的时候赋值的,赋值的就是当前的Thread对象。

也就是说,ViewRootImpl在哪个线程创建的,你后续的UI更新就需要在哪个线程执行,跟是不是UI线程毫无关系。

那么,VIewRootImpl是什么时候创建的呢?

我们启动Activity绘制UI的方法在onResume方法里,所以我们找到Activity的线程ActivityThread类。在ActivityThread中,我们找到handleResumeActivity方法,内部调用了performResumeActivity方法,逐层跟进会发现调用了activity.onResume()方法,所以performResumeActivity确实是resume的入口。

 

ActivityThread.java#handleResumeActivity

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);
                }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            } else if (!willBeVisible) {
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }
            ...
}

wm.addView(decor, l);是他进行的View的加载,我们去看看他的实现方法,在WindowManager的实现类WindowManagerImpl里:

 

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

 

发现他是调用WindowManagerGlobal的方法实现的,最后我们找到了最终实现addView的方法:

WindowManagerGlobal.java#addView

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            // Start watching for system property changes.
            ...
            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;
        }
    }

果然在这里,View的加载最后就是在这里实现的,而ViewRootImpl的实例化也在这里。

所以如果我们在子线程中调用WindowManageraddView方法,是不是就可以成功更新UI呢?

 

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Thread(object :Runnable{
        override fun run() {
                Thread.sleep(3000)
                // tv_test.text = "这是修改后的text"
                val tx = TextView(this@TestActivity)
                tx.text = "这是修改后的text"
                tx.setBackgroundColor(Color.WHITE)
                val layoutParams = WindowManager.LayoutParams(
                    200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                    WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE
                )
                windowManager.addView(tx, layoutParams)
        }

    }).start()
}

 

 错误原因是没有启动Looper。原来是因为在ViewRootImpl类里新建了ViewRootHandler的实例mHandler在子线程中加上Looper.prepare()Looper.loop(),然后成功了~

 

Thread(object :Runnable{
            override fun run() {
//                tv_test.text = "这是修改后的text"
                Thread.sleep(3000)
                Looper.prepare()
                val tx = TextView(this@TestActivity)
                tx.text = "这是修改后的text"
                tx.setBackgroundColor(Color.BLACK)
                tx.setTextColor(Color.YELLOW)
                val layoutParams = WindowManager.LayoutParams(
                    WindowManager.LayoutParams.WRAP_CONTENT, 100, 0, 0, WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION,
                    WindowManager.LayoutParams.TYPE_STATUS_BAR, PixelFormat.OPAQUE
                )
                windowManager.addView(tx, layoutParams)
                Looper.loop()
            }

        }).start()

 

运行成功!

 

扩展点:为什么子线程需要加Looper.loop(),主线程不用?

 

总结:

ViewRootImpl 的创建在 onResume 方法回调之后,而我们一开篇是在 onCreate 方法中创建了子线程并访问 UI,在那个时刻,ViewRootImpl 还没有创建,我们在因此 ViewRootImpl#checkThread 没有被调用到,也就是说,检测当前线程是否是创建的 UI 那个线程 的逻辑没有执行到,所以程序没有崩溃一样能跑起来。而之后修改了程序,让线程休眠了 3000 毫秒后,程序就崩了。很明显 3000 毫秒后 ViewRootImpl 已经创建了,可以执行 checkThread 方法检查当前线程。

等等,还没完?

在测试的时候,偶然发现这么写不会报错??怀疑人生!!?

 

Thread(object :Runnable{
    override fun run() {
        Thread.sleep(3000)
        tv_test.text = tv_test.text.toString()+"abc"
    }

}).start()

 

 

 

 仔细想想,想明白了,这块就涉及到View的绘制流程了,设置值时,执行到View.requestLayout->ViewRootImpl#requestLayout 方法,

ViewRootperformTraversals()方法会在measure结束后继续执行,并调用Viewlayout()方法来执行此过程,如下所示:

 

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

 

在layout()方法中,首先会判断视图的宽高是否发生过变化,以确定有没有必要对当前的视图进行重绘,这块显然没有变化,自然也就不会继续往下执行了。

下次如果有人问你 Android 中子线程真的不能更新 UI 吗? 你可以这么回答:

任何线程都可以更新自己创建的 UI。只要保证满足下面几个条件就好了

  1.  在 ViewRootImpl 还没创建出来之前。
    1. UI 修改的操作没有线程限制。因为 checkThread 方法不会被执行到。
  2. 在 ViewRootImpl 创建完成之后
    •  保证「创建 ViewRootImpl 的操作」和「执行修改 UI 的操作」在同一个线程即可。也就是说,要在同一个线程调用 ViewManager#addView 和 ViewManager#updateViewLayout 的方法。
      •  注:ViewManager 是一个接口,WindowManger 接口继承了这个接口,我们通常都是通过 WindowManger(具体实现为 WindowMangerImpl) 进行 view 的 add remove update 操作的。
    • 对应的线程需要创建 Looper 并且调用 Looper#loop 方法,开启消息循环。

有同学可能会问,保证上述条件 1 成立,不就可以避免 checkThread 时候抛出异常了吗?为什么还需要开启消息循坏?

 条件 1 可以避免检查异常,但是无法保证 UI 可以被绘制出来。

 条件 2 可以让更新的 UI 效果呈现出来

  •  WindowManger#addView 最终会调用 WindowManageGlobal#addView 方法,进而触发ViewRootImpl#setView 方法,该方法内部会调用ViewRootImpl#requestLayout 方法。
  •  根据 UI 绘制原理,下一步就是 scheduleTraversals 了,该方法会往消息队列中插入一条消息屏障,然后调用 Choreographer#postCallback 方法,往 looper 中插入一条异步的 MSG_DO_SCHEDULE_CALLBACK 消息。等待垂直同步信号回来之后执行。

 注:ViewRootImpl 有一个 Choreographer 成员变量,ViewRootImpl 的构造函数中会调用 Choreographer#getInstance(); 方法,获取一个当前线程的 Choreographer 局部实例。

 

使用子线程更新 UI 有实际应用场景吗?

Android 中的 SurfaceView 通常会通过一个子线程来进行页面的刷新。如果我们的自定义 View 需要频繁刷新,或者刷新时数据处理量比较大,那么可以考虑使用 SurfaceView 来取代 View

 

扩展知识-使用Looper实现日志捕获:

讲这个之前,先说下Looper的流程吧~

 

前面说到,需要在子线程中调用Looper.loop开启循环。那么我们的主线程为什么没有调用呢?

这里又涉及到handler的事件分发机制了,查看源码得知,在ActivityThread中,系统已经帮我们调用了Looper.prepareMainLooper()

 

public static void main(String[] args) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

    // Install selective syscall interception
    AndroidOs.install();

    // CloseGuard defaults to true and can be quite spammy.  We
    // disable it here, but selectively enable it later (via
    // StrictMode) on debug builds, but using DropBox, not logs.
    CloseGuard.setEnabled(false);

    Environment.initForCurrentUser();

    // Make sure TrustedCertificateStore looks in the right place for CA certificates
    final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
    TrustedCertificateStore.setDefaultUserDirectory(configDir);

    Process.setArgV0("<pre-initialized>");

    Looper.prepareMainLooper();

    // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
    // It will be in the format "seq=114"
    long startSeq = 0;
    if (args != null) {
        for (int i = args.length - 1; i >= 0; --i) {
            if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                startSeq = Long.parseLong(
                        args[i].substring(PROC_START_SEQ_IDENT.length()));
            }
        }
    }
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

 

然后执行Looper.loop(),不断从队列中抽取message

 

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        //1、获取到消息队列
        final MessageQueue queue = me.mQueue;

       //..................

        //开启死循环
        for (;;) {
            //2、拿到队列中的消息
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

        //省略部分不相关的代码
        //..................
            try {
                //3、执行队列中的消息
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
        //..................
            msg.recycleUnchecked();
        }

 

loop()方法中,代码非常简单,分三步走

 1、获取到looper中的 MessageQueue

 2、开启一个死循环,从MessageQueue 中不断的取出消息

 3、执行取出来的消息  msg.target.dispatchMessage(msg);(顺便说一下,HandlerhandleMessage()方法就是在这一步执行的)

在第二步里面,会发生阻塞,如果消息队列里面没有消息了,会无限制的阻塞下去,主线程休眠,释放CPU资源,直到有消息进入消息队列,唤醒线程。从这里就可以看出来,loop死循环本身大部分时间都处于休眠状态,并不会占用太多的资源,真正会造成线程阻塞的反而是在第三步里的  msg.target.dispatchMessage(msg)方法,因此如果在生命周期或者handlerHandlerhandleMessage执行耗时操作的话,才会真正的阻塞UI线程。

 由此我们可以自定义一个handler+Looper截全局崩溃(主线程),避免 APP 退出。

相关代码如下:

 

class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()

        var handler = Handler(Looper.getMainLooper())
        handler.post {
            while (true){
                try {
                 Looper.loop()
                }catch (e:Throwable){
                    e.printStackTrace()
                    if (e.message != null && e.message!!.startsWith("Unable to start activity")) {
                        System.gc();
                        _restart();
                        android.os.Process.killProcess(android.os.Process.myPid());
                        break
                    }
                }
            }
        }
        Thread.setDefaultUncaughtExceptionHandler { t, e ->
            e.printStackTrace()
        }
    }

    private fun _restart() {
        val intent = getPackageManager().getLaunchIntentForPackage(getPackageName())
        intent?.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
        startActivity(intent)
    }


}

 

testDemo:

 

var data:ArrayList<String> ?= null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    tv_test.setOnClickListener {
        data!!.add("1")
    }
}

不添加上述代码时,点击文本:

页面闪退,报错:

 

添加后:

异常捕获,页面不受影响

 

 

 

通过上面的代码就可以就可以实现拦截UI线程的崩溃。但是也并不能够拦截所有的奔溃,如果在ActivityonCreate出现崩溃,导致Activity创建失败,那么就会杀死该app并重启。

 

参考链接:

 

posted on 2020-05-19 15:29  Tears_fg  阅读(374)  评论(0编辑  收藏