为啥loop之后就可以子线程更新ui

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为啥loop之后就可以子线程更新ui相关的知识,希望对你有一定的参考价值。

我们常常听到这么一句话:更新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”这件小事
线程
android
ui
女式凉鞋,时尚,优雅,透气,货到付款!
精选推荐
广告

可能是全网最简单透彻的安卓子线程更新 UI 解析
71阅读·0评论·0点赞
2019年4月24日
android 不能在子线程中更新ui的讨论和分析
1.5W阅读·7评论·14点赞
2016年1月26日
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created
140阅读·0评论·0点赞
2022年9月23日
Looper.prepare()和Looper.loop(),在子线程中更新UI
2520阅读·0评论·0点赞
2016年5月2日
Android为什么能在子线程中更新UI
305阅读·0评论·1点赞
2020年4月9日
android多线程中更新ui,Android 在子线程中更新UI
146阅读·0评论·0点赞
2021年6月3日
Android子线程更新UI就会Crash么
1781阅读·0评论·4点赞
2017年4月1日
为什么只能在主线程中操作UI?为什么子线程中setText不报错?
3970阅读·1评论·5点赞
2017年6月27日
Android 子线程更新TextView的text 不抛出异常原因 分析总结
1211阅读·0评论·3点赞
2019年7月10日
非主线程更新UI
210阅读·0评论·0点赞
2018年4月12日
非 UI 线程中更新 UI
215阅读·0评论·0点赞
2021年4月29日
【Android】 Handler——子线程更新UI
722阅读·1评论·4点赞
2019年12月29日
android-如何在子线程中更新ui
4019阅读·4评论·2点赞
2016年8月23日
SurfaceView
251阅读·0评论·0点赞
2019年3月8日
非UI线程中更新UI
373阅读·0评论·0点赞
2018年7月10日
QT非UI线程更新UI(跨线程更新UI)
275阅读·0评论·0点赞
2022年9月21日
Android开发之UI线程和非UI线程
1619阅读·0评论·1点赞
2020年4月5日
非UI线程可不可以更新UI(一)
1265阅读·0评论·2点赞
2016年2月29日
为什么我们可以在非UI线程中更新UI
2.8W阅读·56评论·35点赞
2015年2月3日
去首页
看看更多热门内容
参考技术A 我们常常听到这么一句话:更新UI要在UI线程(或者说主线程)中去更新,不要在子线程中更新UI,而Android官方也建议我们不要在非UI线程直接更新UI。

android 常见错误集锦

1、在非UI线程中创建fragment对象,然后start(fragment),在fragment的操作中用到了handler,这一会报错,handler can‘t create before loop.prepare()的错误。

原因:fragment 在子线程中创建,默认是绑定子线程的loop,而子线程默认是不执行loop.prepare,更不能更新UI,因此要在主线程中new fragment,设置为final,然后再在子线程中使用start (fragment)。

以上是关于为啥loop之后就可以子线程更新ui的主要内容,如果未能解决你的问题,请参考以下文章

子线程Looper.loop之后

Android Toast在子线程中为啥无法正常使用

主线程不能执行耗时的操作,子线程不能更新Ui

如何在android一条单独线程,更新ui ?

android通过Handler使子线程更新UI

Android 子线程更新UI那些事儿