首先声明一点:子线程里面是可以更新UI的——创建一个空白的Activity,在其xml文件中放一个空白TextView,Java代码如下:

@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_load_gif);tvThread = (TextView) findViewById(R.id.tvThread);new Thread(new Runnable() {@OverridetvThread.setText("子线程加载");}).start();
}

运行结果表示,更新UI时成功的。
现在我们先让子线程休眠100ms再更新UI:

new Thread(new Runnable() {@Overridepublic void run() {try{Thread.sleep(100);}catch (Exception e){}tvThread.setText("子线程加载");
}).start();

结果你会发现,程序崩了。抛出了如下很熟悉的异常:

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)Only the original thread that created a view hierarchy can touch its views:意思是只有创建这个View的线程才能够访问更新这个View。

从异常信息可以知道:
异常是从android.view.ViewRootImpl的checkThread方法抛出的。而ViewRootImpl是接口ViewRoot的实现类。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线程或者MainThread线程),在应用程序启动的时候,就已经被初始化了。

由此我们可以得出结论:

在访问UI的时候,ViewRoot会去检查当前是哪个线程访问的UI,如果不是主线程,就会抛出异常:Only the original thread that created a view hierarchy can touch its views。

再看异常的另一段:

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

再看看它涉及到的requestLayout方法:

@Override
public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {checkThread();       mLayoutRequested=true;    scheduleTraversals();}
}

这里先是调用了**checkThread()方法来检查当前线程;然后调用scheduleTraversals()**方法,scheduleTraversals,字面理解就是线程遍历循环:

void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;TraversalBarrier= Handler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, TraversalRunnable, null);if (!mUnbufferedInputDispatch) {            scheduleConsumeBatchedInput();        }notifyRendererOfFramePending();        pokeDrawLockIfNeeded();       }
}

注意到postCallback方法的的第二个参数:TraversalRunnable,意思是遍历线程,是一个后台任务:

final class TraversalRunnable implements Runnable {@Overridepublic void run() {doTraversal();}
}

里面除了调用了doTraversal();方法,啥也没有干,继续看doTraversal():

void doTraversal() {   if (mTraversalScheduled) {       mTraversalScheduled = false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);if (mProfile) {        Debug.startMethodTracing("ViewAncestor");      }performTraversals();if (mProfile) {   Debug.stopMethodTracing();   }    }
}

可以看到里面调用了performTraversals()方法,View的绘制过程就是从performTraversals方法开始的。它的代码量有点大这里就不多说了,如果继续跟就是学习View的绘制了,偏离了我们的目标。

我们现在知道了,每一次访问UI,Android都会重新绘制View,这个很好理解。

到目前为止,我们可以得到结论:

当访问UI时,ViewRoot会调用checkThread方法检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常。

但是为什么一开始在Activity的onCreate方法中创建子线程更新UI不抛异常呢?
唯一的解释就是执行onCreate方法的那个时候ViewRootImpl对象还没创建,无法去检查当前线程。

哪ViewRootImpl对象是在哪里,在什么时候被创建的呢?
在ActivityThread中,通过分析我们找到了handleResumeActivity方法:

final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume) {unscheduleGcIdler();mSomeActivitiesChanged = true;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 ActivityClientRecord performResumeActivity(IBinder token,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()**这行代码:

final void performResume() {performRestart();mFragments.execPendingActions();mLastNonConfigurationInstances = null;mCalled = false;mInstrumentation.callActivityOnResume(this);......
}

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

public void callActivityOnResume(Activity activity) {activity.mResumed = true;activity.onResume();if (mActivityMonitors != null) {synchronized (mSync) {final int N = mActivityMonitors.size();for (int i=0; i<N; i++) {final ActivityMonitor am = 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();
}

r.activity调用了makeVisible方法,makeVisible方法是干什么的呢?我们跟进去看看:

void makeVisible() {if (!mWindowAdded) {ViewManager wm = getWindowManager();wm.addView(mDecor, getWindow().getAttributes());mWindowAdded = true;}mDecor.setVisibility(View.VISIBLE);
}

他是往WindowManager中添加DecorView,那现在应该关注的就是WindowManageraddView方法了。而WindowManager是一个接口,我们找到WindowManager的实现类WindowManagerImpl。这个和ViewRoot是一样的,就名字多了个impl。在WindowManagerImpladdView方法如下:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);mGlobal.addView(view, params, mDisplay, mParentWindow);
}

里面调用了WindowManagerGlobaladdView方法,那现在就锁定WindowManagerGlobaladdView方法:

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {......ViewRootImpl root;View panelParentView = null;......root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);}try {root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {synchronized (mLock) {final int index = findViewLocked(view, false);if (index >= 0) {removeViewLocked(index, true);}}throw e;}
}

终于看到我们想要看的关键信息:ViewRootImpl是在WindowManagerGlobaladdView方法中创建的。

因此得出一个总结:

ViewRootImpl的创建是在onResume方法回调之后,而我们一开篇是在onCreate方法中创建子线程并访问UI,在那个时刻,ViewRootImpl还没有来得及创建,无法检测当前线程是否是UI线程,所以程序没有崩溃。而之后修改了程序,让线程休眠了100毫秒后再更新UI,程序就崩了。很明显在这100毫秒内ViewRootImpl已经完成了创建,并能执行checkThread方法检查当前访问并更新UI的线程是不是UI线程。

同样的,我们还可以猜测,在onStart方法和onResume方法里面创建子线程并访问更新UI,同样是可以运行成功的。这一点留给读者去验证。

这是从源码的角度分析为什么不能在子线程中更新UI。

哪再问:为什么谷歌要提出:“UI更新一定要在UI线程里实现”这一规则呢?原因如下:

目的在于提高移动端更新UI的效率和和安全性,以此带来流畅的体验。原因是:

Android的UI访问是没有加锁的,多个线程可以同时访问更新操作同一个UI控件。也就是说访问UI的时候,android系统当中的控件都不是线程安全的,这将导致在多线程模式下,当多个线程共同访问更新操作同一个UI控件时容易发生不可控的错误,而这是致命的。所以Android中规定只能在UI线程中访问UI,这相当于从另一个角度给Android的UI访问加上锁,一个伪锁

为什么不能在子线程中更新UI相关推荐

  1. android 不能在子线程中更新ui的讨论和分析

    问题描述 做过android开发基本都遇见过 ViewRootImpl$CalledFromWrongThreadException,上网一查,得到结果基本都是只能在主线程中更改 ui,子线程要修改 ...

  2. C#子线程中更新ui

    本文实例总结了C#子线程更新UI控件的方法,对于桌面应用程序设计的UI界面控制来说非常有实用价值.分享给大家供大家参考之用.具体分析如下: 一般在winform C/S程序中经常会在子线程中更新控件的 ...

  3. Android子线程中更新UI的4种方法

    方法一:用Handler 1.主线程中定义Handler: Handler mHandler = new Handler() { @Override public void handleMessage ...

  4. 子线程中更新UI线程的三个方法

    1.通过handler方式,sendmessage. 多个类间传递比较麻烦,也懒的写... 2.线程中通过runOnUiThread() new Thread() { public void run( ...

  5. android-如何在子线程中更新ui

    参考:https://blog.csdn.net/u013356254/article/details/52287794 实现基本跟链接相同,不同只出在于WindowManager.LayoutPar ...

  6. 【源码】让源码告诉你:为什么在子线程无法更新 UI 操作?

    博主声明: 转载请在开头附加本文链接及作者信息,并标记为转载.本文由博主 威威喵 原创,请多支持与指教. 本文首发于此   博主:威威喵  |  博客主页:https://blog.csdn.net/ ...

  7. android 线程 界面,android开发教程之子线程中更新界面

    每个Handler对象与创建它的线程相关联,并且每个Handler对象只能与一个线程相关联. Handler一般有两种用途:1)执行计划任务,你可以再预定的实现执行某些任务,可以模拟定时器.2)线程间 ...

  8. Android中Handler的使用方法——在子线程中更新界面

    本文主要介绍Android的Handler的使用方法.Handler可以发送Messsage和Runnable对象到与其相关联的线程的消息队列.每个Handler对象与创建它的线程相关联,并且每个Ha ...

  9. Android--Handler的使用方法:在子线程中更新界面

    版权声明:本文为博主原创文章,转载请标明出处. https://blog.csdn.net/chaoyu168/article/details/50914021 本文主要介绍Android的Handl ...

最新文章

  1. 滴滴出行首次进军非洲市场,网络推广外包后的滴滴想去的国家还有很多
  2. c语言不会可以学好java吗_C语言一定要学好吗?
  3. 七、Go 语言面向对象编程
  4. Azure SQL Database (23) Azure SQL Database Dynamic Data Masking动态数据掩码
  5. 编程开发使用的软件大全
  6. 【转】c# [Serializable]的作用
  7. 03_java基础(四)之方法的创建与调用
  8. 10月24号、25号、26号三天PC端云音乐项目总结
  9. 主要国家和地区货币代码表
  10. CenterOS 上 安装 Docker
  11. (附源码)计算机毕业设计SSM基于大数据的高校国有固定资产管理及绩效自动评价系统
  12. Debezium 抽取oracle数据
  13. [c语言+easyx]GUI界面 年会抽奖系统
  14. Vue组件——数字滚动抽奖效果
  15. c语言中%if是什么意思,C语言中if(!a)表示什么意思?
  16. 麦当劳中国推出全新平台“麦麦夜市”;美联航订购5000万加仑可持续航空燃料 | 美通企业日报...
  17. 注册表设置开机自启项
  18. 常州SEO姜东:技术搜索引擎优化
  19. cell-blog 功能介绍与安装
  20. 专升本高数——第六章 向量代数与空间解析几何【学习笔记】

热门文章

  1. FL水果20.9英文版离线汉化中文版教程
  2. 魅族M8第三方应用软件测试
  3. Matplotlib常见问题解决(中文乱码、字体设置、网格设置、坐标轴设置、图片大小和像素设置、坐标轴范围设置)
  4. 【无人机】四轴无人机的轨迹进行可视化和动画处理(Matlab代码实现)
  5. 2022-2027年中国电梯导轨行业发展前景及投资战略咨询报告
  6. Windows Hello指纹识别失败解决方法
  7. 电阻触摸屏的校准算法
  8. UP主排名丨飞瓜数据B站平台充电周榜排行榜2022年1月17日-1月23日
  9. php有一张一毫米的纸,cad单位怎么设置成毫米
  10. 3DMAX和MAYA,到底有什么区别?