文档章节

Android的子线程可以更新UI吗?

 天使爱美
发布于 2016/11/11 22:33
字数 1841
阅读 10
收藏 0
点赞 0
评论 0

Android的UI访问是没有加锁的,这样在多个线程访问UI是不安全的。所以Android开发中规定只能在UI线程中访问UI。

  但是有没有极端的情况?使得我们在子线程中访问UI也可以使程序跑起来呢?接下来我们用一个例子去证实一下。

  新建一个工程,activity_main.xml布局如下所示:

  <RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"

  android:layout_width="match_parent"

  android:layout_height="match_parent"

  >

  <TextView

  android:id="@+id/main_tv"

  android:layout_width="wrap_content"

  android:layout_height="wrap_content"

  android:textSize="18sp"

  android:layout_centerInParent="true"

  />

  RelativeLayout>

  很简单,只是添加了一个居中的TextView

  MainActivity代码如下所示:

 

public class MainActivity extends AppCompatActivity {

  private TextViewmain_tv;

  @Override

  protected void onCreate(BundlesavedInstanceState) {

  super.onCreate(savedInstanceState);

  setContentView(R.layout.activity_main);

  main_tv = (TextView) findViewById(R.id.main_tv);

  new Thread(new Runnable() {

  @Override

  public void run() {

  main_tv.setText("子线程中访问");

  }

  }).start();

  }

  }

  也是很简单的几行,在onCreate方法中创建了一个子线程,并进行UI访问操作。

  点击运行。你会发现即使在子线程中访问UI,程序一样能跑起来。结果如下所示:

 

咦,那为嘛以前在子线程中更新UI会报错呢?难道真的可以在子线程中访问UI?

  先不急,这是一个极端的情况,修改MainActivity如下:

  public class MainActivity extends AppCompatActivity {

  private TextViewmain_tv;

  @Override

  protected void onCreate(BundlesavedInstanceState) {

  super.onCreate(savedInstanceState);

  setContentView(R.layout.activity_main);

  main_tv = (TextView) findViewById(R.id.main_tv);

  new Thread(new Runnable() {

  @Override

  public void run() {

  try {

  Thread.sleep(200);

  } catch (InterruptedException e) {

  e.printStackTrace();

  }

  main_tv.setText("子线程中访问");

  }

  }).start();

  }

  }

  让子线程睡眠200毫秒,醒来后再进行UI访问。

  结果你会发现,程序崩了。这才是正常的现象嘛。抛出了如下很熟悉的异常:

  android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

  at android.view.ViewRootImpl.checkThread(ViewRootImpl.Java:6581)

  at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

  ……

  作为一名开发者,我们应该认真阅读一下这些异常信息,是可以根据这些异常信息来找到为什么一开始的那种情况可以访问UI的。那我们分析一下异常信息:

  首先,从以下异常信息可以知道

  atandroid.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581)

  这个异常是从android.view.ViewRootImpl的checkThread方法抛出的。

  那现在跟进ViewRootImpl的checkThread方法瞧瞧,源码如下:

  void checkThread() {

  if (mThread != Thread.currentThread()) {

  throw new CalledFromWrongThreadException(

  "Only the original thread that created a view hierarchy can touch its views.");

  }

  }

  只有那么几行代码而已的,而mThread是主线程,在应用程序启动的时候,就已经被初始化了。

  由此我们可以得出结论:

  在访问UI的时候,ViewRootImpl会去检查当前是哪个线程访问的UI,如果不是主线程,那就会抛出如下异常:

  Onlytheoriginalthreadthatcreated a viewhierarchycantouchitsviews

  这好像并不能解释什么?继续看到异常信息

  atandroid.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

  那现在就看看requestLayout方法,

  @Overridepublic void requestLayout() {

  if (!mHandlingLayoutInLayoutRequest) {

  checkThread();

  mLayoutRequested = true;

  scheduleTraversals();

  }

  }

  这里也是调用了checkThread()方法来检查当前线程,咦?除了检查线程好像没有什么信息。那再点进scheduleTraversals()方法看看

  void scheduleTraversals() {

  if (!mTraversalScheduled) {

  mTraversalScheduled = true;

  mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

  mChoreographer.postCallback(

  Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

  if (!mUnbufferedInputDispatch) {

  scheduleConsumeBatchedInput();

  }

  notifyRendererOfFramePending();

  pokeDrawLockIfNeeded();

  }

  }

  注意到postCallback方法的的第二个参数传入了很像是一个后台任务。那再点进去

  final class TraversalRunnable implements Runnable {

  @Override

  public void run() {

  doTraversal();

  }

  }

  找到了,那么继续跟进doTraversal()方法。

  void doTraversal() {

  if (mTraversalScheduled) {

  mTraversalScheduled = false;

  mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

  if (mProfile) {

  Debug.startMethodTracing("ViewAncestor");

  }

  performTraversals();

  if (mProfile) {

  Debug.stopMethodTracing();

  mProfile = false;

  }

  }

  }

  可以看到里面调用了一个performTraversals()方法,View的绘制过程就是从这个performTraversals方法开始的。PerformTraversals方法的代码有点长就不贴出来了,如果继续跟进去就是学习View的绘制了。而我们现在知道了,每一次访问了UI,Android都会重新绘制View。这个是很好理解的。

  分析到了这里,其实异常信息对我们帮助也不大了,它只告诉了我们子线程中访问UI在哪里抛出异常。

  而我们会思考:当访问UI时,ViewRootImpl会调用checkThread方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常,这是没问题的。但是为什么一开始在MainActivity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢??

  唯一的解释就是执行onCreate方法的那个时候ViewRootImpl还没创建,无法去检查当前线程。

  那么就可以这样深入进去。寻找ViewRootImpl是在哪里,是什么时候创建的。好,继续前进

  在ActivityThread中,我们找到handleResumeActivity方法,如下:

  final void handleResumeActivity(IBindertoken,

  boolean clearHide, boolean isForward, boolean reallyResume) {

  // If we are getting ready to gc after going to the background, well

  // we are back active so skip it.

  unscheduleGcIdler();

  mSomeActivitiesChanged = true;

  // TODO Push resumeArgs into the activity for consideration

  ActivityClientRecord r = performResumeActivity(token, clearHide);

  if (r != null) {

  final Activity a = r.activity;

  //代码省略

  r.activity.mVisibleFromServer = true;

  mNumVisibleActivities++;

  if (r.activity.mVisibleFromClient) {

  r.activity.makeVisible();

  }

  }

  //代码省略

  }

  可以看到内部调用了performResumeActivity方法,这个方法看名字肯定是回调onResume方法的入口的,那么我们还是跟进去瞧瞧。

  public final ActivityClientRecordperformResumeActivity(IBindertoken,

  boolean clearHide) {

  ActivityClientRecord r = mActivities.get(token);

  if (localLOGV) Slog.v(TAG, "Performing resume of " + r

  + " finished=" + r.activity.mFinished);

  if (r != null && !r.activity.mFinished) {

  //代码省略

  r.activity.performResume();

  //代码省略

  return r;

  }

  可以看到r.activity.performResume()这行代码,跟进 performResume方法,如下:

  final void performResume() {

  performRestart();

  mFragments.execPendingActions();

  mLastNonConfigurationInstances = null;

  mCalled = false;

  // mResumed is set by the instrumentation

  mInstrumentation.callActivityOnResume(this);

  //代码省略

  }

  Instrumentation调用了callActivityOnResume方法,callActivityOnResume源码如下:

  public void callActivityOnResume(Activityactivity) {

  activity.mResumed = true;

  activity.onResume();

  if (mActivityMonitors != null) {

  synchronized (mSync) {

  final int N = mActivityMonitors.size();

  for (int i=0; i<N; i++) {

  final ActivityMonitoram = mActivityMonitors.get(i);

  am.match(activity, activity, activity.getIntent());

  }

  }

  }

  }

  找到了,activity.onResume()。这也证实了,performResumeActivity方法确实是回调onResume方法的入口。

  那么现在我们看回来handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后,

  会来到这一块代码:

  r.activity.mVisibleFromServer = true;

  mNumVisibleActivities++;if (r.activity.mVisibleFromClient) {

  r.activity.makeVisible();

  }

  activity调用了makeVisible方法,这应该是让什么显示的吧,跟进去探探。

  void makeVisible() {

  if (!mWindowAdded) {

  ViewManagerwm = getWindowManager();

  wm.addView(mDecor, getWindow().getAttributes());

  mWindowAdded = true;

  }

  mDecor.setVisibility(View.VISIBLE);

  }

  往WindowManager中添加DecorView,那现在应该关注的就是WindowManager的addView方法了。而WindowManager是一个接口来的,我们应该找到WindowManager的实现类才行,而WindowManager的实现类是WindowManagerImpl。

  找到了WindowManagerImpl的addView方法,如下:

  @Override

  public void addView(@NonNull Viewview, @NonNull ViewGroup.LayoutParamsparams) {

  applyDefaultToken(params);

  mGlobal.addView(view, params, mDisplay, mParentWindow);

  }

  里面调用了WindowManagerGlobal的addView方法,那现在就锁定

  WindowManagerGlobal的addView方法:

  public void addView(Viewview, ViewGroup.LayoutParamsparams,

  Displaydisplay, WindowparentWindow) {

  //代码省略

  ViewRootImplroot;

  ViewpanelParentView = null;

  //代码省略

  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;

  }

  }

  终于击破,ViewRootImpl是在WindowManagerGlobal的addView方法中创建的。

  回顾前面的分析,总结一下:

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

这篇博客的分析如题目一样,Android中子线程真的不能更新UI吗?在onCreate方法中创建的子线程访问UI是一种极端的情况,这个不仔细分析源码是不知道的。我是最近看了一个面试题,才发现这个。

文章来源:伯乐在线

© 著作权归作者所有

共有 人打赏支持
粉丝 0
博文 28
码字总数 53872
作品 0
朝阳
说说在 Android 中如何实现多线程编程

当我们执行一些耗时操作,比如发起一条网络请求时,考虑到网速等其他因素,服务器未必会立刻响应我们的请求,那么就必须将这类操作放在子线程中运行,这就需要实现多线程编程。 1 启动线程 ...

deniro ⋅ 06/18 ⋅ 0

说说在 Android 中如何发送 HTTP 请求

客户端会向服务器发出一条 HTTP 请求,服务器收到请求后会返回一些数据给客户端,然后客户端再对这些数据进行解析与处理。 1 HttpURLConnection 可以使用 HttpURLConnection(官方推荐) 来发...

deniro ⋅ 06/09 ⋅ 0

关于Activity销毁,而绘制UI的子线程未销毁出现的问题

项目总结 --------------------------------------------------------------------------------------------------------- 有一个功能模块,需要播放音频,画一个简单的界面 一个例子: 我们...

听着music睡 ⋅ 2015/11/12 ⋅ 0

Android的进程与线程(3)线程安全问题

当一个程序启动的时候,系统会为程序创建一个名为main的线程。这个线程重要性在于它负责把事件分发给适合的用户组件,这些事件包括绘制事件。并且这个线程也是你的程序与Android UI工具包中的...

一路漫漫 ⋅ 2012/04/01 ⋅ 0

对Android Handler Message Looper常见用法,知识点的一些总结

Android 非UI线程中是不能更新UI的,Handler是Android 提供的一套更新UI的机制,也是用来发送消息和处理消息的一套机制。 以前刚接触的Handler的时候,感觉总是很困惑,对Handler原理也是一知...

猴亮屏 ⋅ 06/11 ⋅ 0

Handler消息处理机制分析

Handler经常用,然后自己总结一下下 一. What、Handler 是什么 Handler 与 Message、MessageQueue、Looper 一起构成了 Android 的消息机制,Android 系统通过大量的消息来与用户进行交互,V...

大二架构师 ⋅ 05/07 ⋅ 0

Handler和AsyncTask

在Android中实现异步任务机制有两种方式,Handler和AsyncTask。 Handler模式需要为每一个任务创建一个新的线程,任务完成后通过Handler实例向UI线程发送消息,完成界面的更新,这种方式对于整...

hisense20112784 ⋅ 2017/06/03 ⋅ 0

AIDL跨进程Service推送消息到Activity

AIDL的概念不说了,一般都是Activity调用service的方法去获取一些东西,但是如何做到service主动回调activity的方法去推送一些东西的,这种需求一般也是会有的(比如后台有个定位,每次位置更新或...

倔强码农 ⋅ 04/11 ⋅ 0

Handler的基本使用

一、Handler的定义: 主要接受子线程发送的数据, 并用此数据配合主线程更新UI。 解释:当应用程序启动时,Android首先会开启一个主线程 (也就是UI线程) , 主线程为管理界面中的UI控件, 进...

初来小修 ⋅ 2016/02/04 ⋅ 0

Handler消息处理机制

Android在设计时引入了Handler消息机制,每一个消息发送到主线路的消息队列中,消息队列遵循先进先出原则,发送消息不会阻塞线程,而接收线程会阻塞线程。Handler允许发送并处理Message消息,...

LionSword ⋅ 2014/06/22 ⋅ 1

没有更多内容

加载失败,请刷新页面

加载更多

下一页

来自一个优秀Java工程师的简历

写在前面: 鉴于前几天的一份前端简历,虽然带着很多不看好的声音,但却帮助了很多正在求职路上的人,不管评论怎么说,我还是决定要贴出一份后端的简历。 XXX ID:357912485 目前正在找工作 ...

颖伙虫 ⋅ 12分钟前 ⋅ 0

Confluence 6 恢复一个站点有关使用站点导出为备份的说明

推荐使用生产备份策略。我们推荐你针对你的生产环境中使用的 Confluence 参考 Production Backup Strategy 页面中的内容进行备份和恢复(这个需要你备份你的数据库和 home 目录)。XML 导出备...

honeymose ⋅ 今天 ⋅ 0

JavaScript零基础入门——(九)JavaScript的函数

JavaScript零基础入门——(九)JavaScript的函数 欢迎回到我们的JavaScript零基础入门,上一节课我们了解了有关JS中数组的相关知识点,不知道大家有没有自己去敲一敲,消化一下?这一节课,...

JandenMa ⋅ 今天 ⋅ 0

火狐浏览器各版本下载及插件httprequest

各版本下载地址:http://ftp.mozilla.org/pub/mozilla.org//firefox/releases/ httprequest插件截至57版本可用

xiaoge2016 ⋅ 今天 ⋅ 0

Docker系列教程28-实战:使用Docker Compose运行ELK

原文:http://www.itmuch.com/docker/28-docker-compose-in-action-elk/,转载请说明出处。 ElasticSearch【存储】 Logtash【日志聚合器】 Kibana【界面】 答案: version: '2'services: ...

周立_ITMuch ⋅ 今天 ⋅ 0

使用快嘉sdkg极速搭建接口模拟系统

在具体项目研发过程中,一旦前后端双方约定好接口,前端和app同事就会希望后台同事可以尽快提供可供对接的接口方便调试,而对后台同事来说定好接口还仅是个开始、设计流程,实现业务逻辑,编...

fastjrun ⋅ 今天 ⋅ 0

PXE/KickStart 无人值守安装

导言 作为中小公司的运维,经常会遇到一些机械式的重复工作,例如:有时公司同时上线几十甚至上百台服务器,而且需要我们在短时间内完成系统安装。 常规的办法有什么? 光盘安装系统 ===> 一...

kangvcar ⋅ 昨天 ⋅ 0

使用Puppeteer撸一个爬虫

Puppeteer是什么 puppeteer是谷歌chrome团队官方开发的一个无界面(Headless)chrome工具。Chrome Headless将成为web应用自动化测试的行业标杆。所以我们很有必要来了解一下它。所谓的无头浏...

小草先森 ⋅ 昨天 ⋅ 0

Java Done Right

* 表示难度较大或理论性较强。 ** 表示难度更大或理论性更强。 【Java语言本身】 基础语法,面向对象,顺序编程,并发编程,网络编程,泛型,注解,lambda(Java8),module(Java9),var(...

风华神使 ⋅ 昨天 ⋅ 0

Linux系统日志

linux 系统日志 /var/log/messages /etc/logrotate.conf 日志切割配置文件 https://my.oschina.net/u/2000675/blog/908189 logrotate 使用详解 dmesg 命令 /var/log/dmesg 日志 last命令,调......

Linux学习笔记 ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部