再谈子线程-居然可以在非UI线程中更新UI
来源:互联网 发布:盘古网络唐山icp备 编辑:程序博客网 时间:2024/05/01 20:56
我们常常听到这么一句话:更新UI要在UI线程(或者说主线程)中去更新,不要在子线程中更新UI,而Android官方也建议我们不要在非UI线程直接更新UI。
事实是不是如此呢,做一个实验:
更新之前:
代码:
package com.bourne.android_common.ServiceDemo;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;import com.bourne.android_common.R;public class ThreadActivity extends AppCompatActivity { private Thread thread; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread); textView = (TextView) findViewById(R.id.textView); thread = new Thread(new Runnable() { @Override public void run() { textView.setText("text text text"); } }); thread.start(); } @Override protected void onDestroy() { super.onDestroy(); }}
这里在Activity里面新建了一个子线程去更新UI,按理说会报错啊,可是执行结果是并没有报错,如图所示:
接下来让线程休眠一下:
package com.bourne.android_common.ServiceDemo;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;import com.bourne.android_common.R;public class ThreadActivity extends AppCompatActivity { private Thread thread; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread); textView = (TextView) findViewById(R.id.textView); thread = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } textView.setText("text text text"); } }); thread.start(); }}
应用报错,抛出异常:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views
只有创建View层次结构的线程才能修改View,我们在非UI主线程里面更新了View,所以会报错
因为在OnCreate里面睡眠了一下才报错,这是为什么呢?
Android通过检查我们当前的线程是否为UI线程从而抛出一个自定义的AndroidRuntimeException来提醒我们“Only the original thread that created a view hierarchy can touch its views”并强制终止程序运行,具体的实现在ViewRootImpl类的checkThread方法中:
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { // 省去海量代码………………………… void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } } // 省去巨量代码…………………… }
这就是Android在4.0后对我们做出的一个限制。
其实造成这个现象的根本原因是:
还没有到执行checkThread方法去检查我们的当前线程那一步。”Android对UI事件的处理需要依赖于Message Queue,当一个Msg被压入MQ到处理这个过程并非立即的,它需要一段事件,我们在线程中通过Thread.sleep(200)在等,在等什么呢?在等ViewRootImpl的实例对象被创建。”
ViewRootImpl的实例对象是在OnResume中创建的啊!
看onResume方法的调度,其在ActivityThread中通过handleResumeActivity调度:
public final class ActivityThread { // 省去海量代码………………………… final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { unscheduleGcIdler(); ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; // 省去无关代码………… final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { } } 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); } } else if (!willBeVisible) { // 省去无关代码………… r.hideForNow = true; } cleanUpPendingRemoveWindows(r); if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { // 省去无关代码………… performConfigurationChanged(r.activity, r.newConfig); freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.newConfig)); r.newConfig = null; } // 省去无关代码………… WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) { r.activity.makeVisible(); } } if (!r.onlyLocalRequest) { r.nextIdle = mNewActivities; mNewActivities = r; // 省去无关代码………… Looper.myQueue().addIdleHandler(new Idler()); } r.onlyLocalRequest = false; // 省去与ActivityManager的通信处理 } else { // 省略异常发生时对Activity的处理逻辑 } } // 省去巨量代码…………………… }
handleResumeActivity方法逻辑相对要复杂一些,除了对当前显示Window的逻辑判断以及没创建的初始化等等工作外其在最终会调用Activity的makeVisible方法
public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2 { // 省去海量代码………………………… void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); } // 省去巨量代码…………………… }
在makeVisible方法中逻辑相当简单,获取一个窗口管理器对象并将根视图DecorView添加到其中,addView的具体实现在WindowManagerGlobal中:
public final class WindowManagerGlobal { public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { // 省去很多代码 ViewRootImpl root; // 省去一行代码 synchronized (mLock) { // 省去无关代码 root = new ViewRootImpl(view.getContext(), display); // 省去一行代码 // 省去一行代码 mRoots.add(root); // 省去一行代码 } // 省去部分代码 } }
在addView生成了一个ViewRootImpl对象并将其保存在了mRoots数组中,每当我们addView一次,就会生成一个ViewRootImpl对象,其实看到这里我们还可以扩展一下问题一个APP是否可以拥有多个根视图呢?答案是肯定的,因为只要我调用了addView方法,我们传入的View参数就可以被认为是一个根视图,但是!在framework的默认实现中有且仅有一个根视图,那就是我们上面makeVisible方法中addView进去的DecorView,所以为什么我们可以说一个APP虽然可以有多个Activity,但是每个Activity只会有一个Window一个DecorView一个ViewRootImpl,看到这里很多童鞋依然会问,也就是说在onResume方法被执行后我们的ViewRootImpl才会被生成对吧,但是为什么下面的代码依然可以正确运行呢:
package com.bourne.android_common.ServiceDemo;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;import com.bourne.android_common.R;public class ThreadActivity extends AppCompatActivity { private Thread thread; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread); textView = (TextView) findViewById(R.id.textView); thread = new Thread(new Runnable() { @Override public void run() { textView.setText("text text text"); } }); thread.start(); } @Override protected void onResume() { super.onResume(); }}
Activity.onResume前,ViewRootImpl实例没有建立,所以没有checkThread检查。但是使用了Thread.sleep(200)的时候,ViewRootImpl已经被创建完毕了,自然checkThread就起作用了,抛出异常顺理成章。
第一种做法中,虽然是在子线程中setText,但是这时候View还没画出来呢,所以并不会调用之后的invalidate,而相当于只是设置TextView的一个属性,不会invalidate,就没有后面的那些方法调用了,归根结底,就不会调用ViewRootImpl的checkThread,也就不会报错。而第二种方法,调用setText之后,就会引发后面的一系列的方法调用,VIew要刷新界面,ViewGroup要更新布局,计算子View的大小位置,到最后,ViewRootImpl就会checkThread,就崩了。
所以,严格上来说,第一种方法虽然在子线程了设置View属性,但是不能够归结到”更新View”的范畴,因为还没画出来呢,就没有所谓的更新。
当我们执行Thread.sleep时候,这时候onStart、onResume都执行了,子线程再调用setText的时候,就会崩溃。
那么说,在onStart()或者onResume()里面执行线程操作UI也是可以的:
@Override protected void onStart() { super.onStart(); thread = new Thread(new Runnable() { @Override public void run() { textView.setText("text text text"); } }); thread.start(); }
@Override protected void onResume() { super.onResume(); thread = new Thread(new Runnable() { @Override public void run() { textView.setText("text text text"); } }); thread.start(); }
注意的是:当你在来回切换界面的时候,onStart()和onResume()是会再执行一遍的,这时候程序就崩溃了!
1、能不能在非UI线程中更新UI呢?
答案:能、当然可以
2、View的运行和Activity的生命周期有什么必然联系吗?
答案:没有、或者隐晦地说没有必然联系
3、除了Handler外是否还有更简便的方式在非UI线程更新UI呢?
答案:有、而且还不少,Activity.runOnUiThread(Runnable)、View.Post(Runnable)、View.PostDelayed(Runnable,long)、AsyncTask、其内部实现原理都是向此View的线程的内部消息队列发送一个Message消息,并传送数据和处理方式,省去了自己再写一个专门的Handler去处理。
4、在子线程里面用Toast也会报错,加上Looper.prepare和Looper.loop就可以了,这里可以这样做吗?
答案当然是不可以。Toast和View本质上是不一样的,Toast在子线程报错,是因为Toast的显示需要添加到一个MessageQueue中,然后Looper取出来,发给Handler调用显示,子线程因为没有Looper,所以需要加上Looper.prepare和Looper.loop创建一个Looper,但是实质上,这还是在子线程调用,所以还是会报错的!
5、为什么Android要求只能在UI主线程中更改View呢
这就要说到Android的单线程模型了,因为如果支持多线程修改View的话,由此产生的线程同步和线程安全问题将是非常繁琐的,所以Android直接就定死了,View的操作必须在UI线程,从而简化了系统设计。
参考文章
为什么我们可以在非UI线程中更新UI
【Android开发经验】来来来,同学,咱们讨论一下“只能在UI主线程更新View”这件小事
- 再谈子线程-居然可以在非UI线程中更新UI
- 为什么我们可以在非UI线程中更新UI
- 为什么我们可以在非UI线程中更新UI
- 为什么我们可以在非UI线程中更新UI
- 为什么我们可以在非UI线程中更新UI
- 为什么我们可以在非UI线程中更新UI
- 为什么我们可以在非UI线程中更新UI
- ProgressBar 为什么可以在非UI线程中更新进度。
- Android子线程居然可以更新UI?
- Android在非UI线程中更新UI的方法
- Android开之在非UI线程中更新UI
- 基础篇-在非UI线程中更新UI元素
- android在非UI线程中更新UI
- Android 非UI线程中更新UI
- Android 非UI线程中更新UI
- 非UI线程更新UI!?
- 为啥可以在非ui线程也可以更新ui组件??
- Android 非UI线程是否可以更新UI
- 为什么要使用泛型
- jdbc连接数据库步骤(mysql、oracle、sqlserver2008)
- 84.MYSQL数据库安装与配置详解
- Ubuntu16.04安装flash_player插件
- java:‘类’与'对象'的学习
- 再谈子线程-居然可以在非UI线程中更新UI
- leetcode解题之237# Delete Node in a Linked List Java版 (删除链表中指定的结点)
- java中的字节流总结
- 蓝桥杯 集合运算(set)
- “玲珑杯”ACM比赛 Round #12【dp】
- Spark + Kafka 集成 指南
- Ubuntu vsftp配置
- 快速排序
- Date.parse()与Date.getTime()方法详解