文档章节

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

 天使爱美
发布于 2016/11/11 22:33
字数 1841
阅读 55
收藏 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是一种极端的情况,这个不仔细分析源码是不知道的。我是最近看了一个面试题,才发现这个。

文章来源:伯乐在线

粉丝 1
博文 36
码字总数 59694
作品 0
朝阳
私信 提问
加载中
请先登录后再评论。
浅入浅出Android(003):使用TextView类构造文本控件

基础: TextView是无法供编辑的。 当我们新建一个项目MyTextView时候,默认的布局(/res/layout/activity_main.xml)中已经有了一个TextView: <TextView 运行效果如下: 修改其文本内容...

樂天
2014/03/22
602
1
Swift百万线程攻破单例(Singleton)模式

一、不安全的单例实现 在上一篇文章我们给出了单例的设计模式,直接给出了线程安全的实现方法。单例的实现有多种方法,如下面: class SwiftSingleton { } 这段代码的实现,在shared中进行条...

一叶博客
2014/06/20
3.3K
16
树莓派(Raspberry Pi):完美的家用服务器

自从树莓派发布后,所有在互联网上的网站为此激动人心的设备提供了很多有趣和具有挑战性的使用方法。虽然这些想法都很棒,但树莓派( RPi )最明显却又是最不吸引人的用处是:创建你的完美家用...

异次元
2013/11/09
5.7K
8
程序猿媛一:Android滑动翻页+区域点击事件

滑动翻页+区域点击事件 ViewPager+GrideView 声明:博文为原创,文章内容为,效果展示,思路阐述,及代码片段。文尾附注源码获取途径。 转载请保留原文出处“http://my.oschina.net/gluoyer...

花佟林雨月
2013/11/09
4.1K
1
iOS 应用版本更新检查--Harpy

实现 app 版本检查功能。用户打开app之后,进行app的版本更新检查。如果检查到 appstore 上有当前qpp的新版本,则弹出对话框提醒用户下载。可以设置让用户选择下载或者强制用户下载。 [Code4...

匿名
2013/02/18
1.7K
0

没有更多内容

加载失败,请刷新页面

加载更多

层次聚类与聚类树

聚类分析 在生态学研究当中,有些环境中的对象是连续(或者离散)的,而有些对象是不连续的,聚类的目的是识别在环境中不连续的对象子集,从而探索隐藏在数据背后的属性特征。聚类分析主要处...

SYSU星空
01/11
0
0
几个无聊但你可能忽略的C知识点

来源:公众号【编程珠玑】 作者:守望先生 ID:shouwangxiansheng C语言main函数的写法 标准中,只有下面两种写法: int main (void) { /**body**/ } 以及 int main (int argc, cha...

学以解忧
01/08
0
0
20个实战项目教你掌握OpenCV和图像处理,PDF开放下载

点击上方“小白学视觉”,选择加"星标"或“置顶” 重磅干货,第一时间送达 近期小白学视觉公众号推出了多篇OpenCV实战项目的文章,深受小伙伴们的喜爱。最近有小伙伴推荐,希望可以讲经典的项...

机器学习炼丹术
28分钟前
9
0
吃瓜!挂一个爬虫工作者

原本不打算撕逼的,可总感觉技术人员这么狗,不曝光他不合适。 此人微信号: YGMXMF 描述如下: 鉴于不可抗力因素,我删除了上篇文章! 此人想178买文章(早就关注了,当时没付费观看,现在要...

adminThorn
46分钟前
9
0
聊一聊一道关于线程池的面试题

网络上有这样一道关于线程池的面试题: 1. 高并发、任务执行时间短的业务怎样使用线程池? 2. 并发不高、任务执行时间长的业务怎样使用线程池? 3. 并发高、业务执行时间长的业务怎样使用线程...

黄小斜
今天
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部