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

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

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

因此Android中规定只能在UI线程中访问UI。子线程更新是不被容许的。面试

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

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

<?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() } }

 

展现,并无报错:ide

 

紧接着,咱们试试延时试试:函数

 

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后竟然奔溃了,这是为啥呢?oop

查看错误日志:布局

 

经典问题出现了,查看源码能够看到,是在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."); } //一、获取到消息队列
        final MessageQueue queue = me.mQueue; //.................. //开启死循环
        for (;;) { //二、拿到队列中的消息
            Message msg = queue.next(); // might block
            if (msg == null) { // No message indicates that the message queue is quitting.
                return; } //省略部分不相关的代码 //..................
            try { //三、执行队列中的消息
 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并重启。

 

参考连接:

相关文章
相关标签/搜索