今天讲一个老生常谈的问题,"Android中子线程真的不能更新UI吗?”java
Android中UI访问是没有加锁的,这样在多个线程访问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的实例化也在这里。
因此若是咱们在子线程中调用WindowManager的addView方法,是否是就能够成功更新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 方法,
ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程,以下所示:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
在layout()方法中,首先会判断视图的宽高是否发生过变化,以肯定有没有必要对当前的视图进行重绘,这块显然没有变化,天然也就不会继续往下执行了。
下次若是有人问你 Android 中子线程真的不能更新 UI 吗? 你能够这么回答:
任何线程均可以更新本身建立的 UI。只要保证知足下面几个条件就行了
有同窗可能会问,保证上述条件 1 成立,不就能够避免 checkThread 时候抛出异常了吗?为何还须要开启消息循坏?
条件 1 能够避免检查异常,可是没法保证 UI 能够被绘制出来。
条件 2 可让更新的 UI 效果呈现出来
注:ViewRootImpl 有一个 Choreographer 成员变量,ViewRootImpl 的构造函数中会调用 Choreographer#getInstance(); 方法,获取一个当前线程的 Choreographer 局部实例。
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);(顺便说一下,Handler的handleMessage()方法就是在这一步执行的)
在第二步里面,会发生阻塞,若是消息队列里面没有消息了,会无限制的阻塞下去,主线程休眠,释放CPU资源,直到有消息进入消息队列,唤醒线程。从这里就能够看出来,loop死循环自己大部分时间都处于休眠状态,并不会占用太多的资源,真正会形成线程阻塞的反而是在第三步里的 msg.target.dispatchMessage(msg)方法,所以若是在生命周期或者handler的Handler的handleMessage执行耗时操做的话,才会真正的阻塞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线程的崩溃。可是也并不可以拦截全部的奔溃,若是在Activity的onCreate出现崩溃,致使Activity建立失败,那么就会杀死该app并重启。