Android的子线程可以更新UI吗?
Android的子线程可以更新UI吗?
天使爱美 发表于1年前
Android的子线程可以更新UI吗?
  • 发表于 1年前
  • 阅读 9
  • 收藏 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
×
天使爱美
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: