view.post没执行,runOnUiThread,Handler
目录
- 坑点
- 处理
- 原因
- 经历
- 复盘
- 6.0版本
- 10.0版本
- 总结
坑点
子线程执行view.post(Runnable) 部分 手机没有效果。
usernameEditText.post(new Runnable() {@Overridepublic void run() {usernameEditText.setText("text set by runnable!");}});
处理
- 使用handler.post
- 使用runOnUiThread
原因
低版本android基于ThreadLocal实现线程与数据关联,且线程的数据独立不共享。遇到多线程使用时,一个线程存储了数据,另一个线程取不到的数据的原因。
子线程调用view.post时候,会构造一个队列存储到对应的线程数据空间,并将runnable加到此队列。当view要显示时候,ui线程会从ui线程的数据空间取出队列,遍历执行队列中的runnable,但是由于ThreadLocal的缘故,ui线程取到的队列肯定不包含子线程存到队列的runnable,所以这个runnable是不被执行的。因为刚才是子线程存的runnable,子线程可以取到,而UI线程并没有存我们期望的runnable,所以取不到。ThreadLocal特点就是线程之间的数据相互隔离,各自使用各自的数据,多线程使用时保证数据的“安全”。
还没明白的话,这样讲一下:
A、B钱包都没钱了,A从银行取了1000 人民币,装入了自己的钱包,B去商店买1000的商品,此时B从自己钱包里面拿钱时,钱包是空的。所以B是买不了商品的。
7.0之前的系统存在这个问题,7.0之后已经被修复了。用的时候小心一点。
经历
给同事写了个程序,当时是一个子线程处理了数据之后调用view.post更新到界面上,自测是没问题的,结果同事那边告知不显示,当时也查看了源代码,同时也是反复验证没有问题,同事那里始终是有问题的,后来同事也没再提说。一段时间之后见了同事,问及此事,他说 只有他手机有问题,其他的手机没问题,所以就没再说这个事情了。让其掏出手机看了下,果真不显示,当即开始加日志调试,结果runnable代码块没有被执行,隐隐约约感觉到view.post不靠谱,直接在外层再加一个runOnUiThread之后,他的设备正常。 虽然问题是过去了,但一直没时间去弄清楚出现这个问题的原因,最近看到项目中有其他小伙伴也写了同样的代码,心里面有点慌。
同时也查了些资料,总结记录之。
复盘
View.post()方法在android7.0之前,在view没有attachToWindow的时候调用该方法可能失效,尤其异步线程,如在onCreate,onBindViewHolder时调用view.post方法,可能会不生效,在异步线程view.post方法不执行的情况居多。建议使用Handler post方法代替。
longlong2015 这里也对次问题进行说明
于是乎,下载了一份6.0版本的sdk源码,以及9.0的源码进行对比,对比情况和引用文章差不多,也进一步对引用文章进行验证。
6.0版本
- View的post函数
public boolean post(Runnable action) {final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null) {return attachInfo.mHandler.post(action);}// Assume that post will succeed laterViewRootImpl.getRunQueue().post(action);return true;}
如果attachInfo有值,则是用attachInfo中的handler去post这个runnable,如果attachInfo没有值,则是ViewRootImpl.getRunQueue() 去执行post这个runnable。而attachInfo则是分别dispatchAttachedToWindow (首行)赋值的:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {//System.out.println("Attached! " + this);mAttachInfo = info;if (mOverlay != null) {mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);}mWindowAttachCount++;// We will need to evaluate the drawable state at least once.mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;if (mFloatingTreeObserver != null) {info.mTreeObserver.merge(mFloatingTreeObserver);mFloatingTreeObserver = null;}if ((mPrivateFlags&PFLAG_SCROLL_CONTAINER) != 0) {mAttachInfo.mScrollContainers.add(this);mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;}performCollectViewAttributes(mAttachInfo, visibility);onAttachedToWindow();ListenerInfo li = mListenerInfo;final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =li != null ? li.mOnAttachStateChangeListeners : null;if (listeners != null && listeners.size() > 0) {// NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to// perform the dispatching. The iterator is a safe guard against listeners that// could mutate the list by calling the various add/remove methods. This prevents// the array from being modified while we iterate it.for (OnAttachStateChangeListener listener : listeners) {listener.onViewAttachedToWindow(this);}}int vis = info.mWindowVisibility;if (vis != GONE) {onWindowVisibilityChanged(vis);}// Send onVisibilityChanged directly instead of dispatchVisibilityChanged.// As all views in the subtree will already receive dispatchAttachedToWindow// traversing the subtree again here is not desired.onVisibilityChanged(this, visibility);if ((mPrivateFlags&PFLAG_DRAWABLE_STATE_DIRTY) != 0) {// If nobody has evaluated the drawable state yet, then do it now.refreshDrawableState();}needGlobalAttributesUpdate(false);}
dispatchDetachedFromWindow(倒数第三行)中赋空
void dispatchDetachedFromWindow() {AttachInfo info = mAttachInfo;if (info != null) {int vis = info.mWindowVisibility;if (vis != GONE) {onWindowVisibilityChanged(GONE);}}onDetachedFromWindow();onDetachedFromWindowInternal();InputMethodManager imm = InputMethodManager.peekInstance();if (imm != null) {imm.onViewDetachedFromWindow(this);}ListenerInfo li = mListenerInfo;final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =li != null ? li.mOnAttachStateChangeListeners : null;if (listeners != null && listeners.size() > 0) {// NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to// perform the dispatching. The iterator is a safe guard against listeners that// could mutate the list by calling the various add/remove methods. This prevents// the array from being modified while we iterate it.for (OnAttachStateChangeListener listener : listeners) {listener.onViewDetachedFromWindow(this);}}if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER_ADDED) != 0) {mAttachInfo.mScrollContainers.remove(this);mPrivateFlags &= ~PFLAG_SCROLL_CONTAINER_ADDED;}mAttachInfo = null;if (mOverlay != null) {mOverlay.getOverlayView().dispatchDetachedFromWindow();}}
- ViewRootImpl.getRunQueue()
ViewRootImpl 的静态成员 sRunQueues 和静态函数getRunQueue
static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();
static RunQueue getRunQueue() {RunQueue rq = sRunQueues.get();if (rq != null) {return rq;}rq = new RunQueue();sRunQueues.set(rq);return rq;}
ui线程执行“存到队列中的任务"
// Execute enqueued actions on every traversal in case a detached view enqueued an actiongetRunQueue().executeActions(mAttachInfo.mHandler);
根源是sRunQueues.get(),其实也是ThreadLocal的特性。当子线程调用的时候,这里返回的rq 是空的,接着创建一个rt后存入。之后UI线程调用,这里返回的不是子线程创建的rq。
- ViewRootImpl.RunQueue.executeActions
void executeActions(Handler handler) {synchronized (mActions) {final ArrayList<HandlerAction> actions = mActions;final int count = actions.size();for (int i = 0; i < count; i++) {final HandlerAction handlerAction = actions.get(i);handler.postDelayed(handlerAction.action, handlerAction.delay);}actions.clear();}}
- ThreadLocal.get()
再进一步看一下这个ThreadLocal的get实现(get的样子往往容易被忽视)
public T get() {// Optimized for the fast path.Thread currentThread = Thread.currentThread();Values values = values(currentThread);if (values != null) {Object[] table = values.table;int index = hash & values.mask;if (this.reference == table[index]) {return (T) table[index + 1];}} else {values = initializeValues(currentThread);}return (T) values.getAfterMiss(this);}
好的,到这里已经看到取当前的线程做了一系列的事情,因此不同线程返回的自然就不一样。
10.0版本
- View.post
public boolean post(Runnable action) {final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null) {return attachInfo.mHandler.post(action);}// Postpone the runnable until we know on which thread it needs to run.// Assume that the runnable will be successfully placed after attach.getRunQueue().post(action);return true;}
可以看到这里以不是用ViewRootImpl.getRunQueue(),而是view内部的函数getRunQueue().
- View.getRunQueue()
private HandlerActionQueue getRunQueue() {if (mRunQueue == null) {mRunQueue = new HandlerActionQueue();}return mRunQueue;}
好家伙,现在的队列是属于view的了,不再是归属于线程,变成了共享变量。
因此子线程向队列里面添加一个runnable之后,ui线程做来取队列就能取到。执行就是我们期望的结果了。
- View.dispatchAttachedToWindow
void dispatchAttachedToWindow(AttachInfo info, int visibility) {mAttachInfo = info;if (mOverlay != null) {mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);}mWindowAttachCount++;// We will need to evaluate the drawable state at least once.mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;if (mFloatingTreeObserver != null) {info.mTreeObserver.merge(mFloatingTreeObserver);mFloatingTreeObserver = null;}registerPendingFrameMetricsObservers();if ((mPrivateFlags&PFLAG_SCROLL_CONTAINER) != 0) {mAttachInfo.mScrollContainers.add(this);mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;}// Transfer all pending runnables.if (mRunQueue != null) {mRunQueue.executeActions(info.mHandler);mRunQueue = null;}performCollectViewAttributes(mAttachInfo, visibility);onAttachedToWindow();ListenerInfo li = mListenerInfo;final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =li != null ? li.mOnAttachStateChangeListeners : null;if (listeners != null && listeners.size() > 0) {// NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to// perform the dispatching. The iterator is a safe guard against listeners that// could mutate the list by calling the various add/remove methods. This prevents// the array from being modified while we iterate it.for (OnAttachStateChangeListener listener : listeners) {listener.onViewAttachedToWindow(this);}}int vis = info.mWindowVisibility;if (vis != GONE) {onWindowVisibilityChanged(vis);if (isShown()) {// Calling onVisibilityAggregated directly here since the subtree will also// receive dispatchAttachedToWindow and this same callonVisibilityAggregated(vis == VISIBLE);}}// Send onVisibilityChanged directly instead of dispatchVisibilityChanged.// As all views in the subtree will already receive dispatchAttachedToWindow// traversing the subtree again here is not desired.onVisibilityChanged(this, visibility);if ((mPrivateFlags&PFLAG_DRAWABLE_STATE_DIRTY) != 0) {// If nobody has evaluated the drawable state yet, then do it now.refreshDrawableState();}needGlobalAttributesUpdate(false);notifyEnterOrExitForAutoFillIfNeeded(true);notifyAppearedOrDisappearedForContentCaptureIfNeeded(true);}
其中下面这段就是UI线程来执行存入需要处理的任务:
if (mRunQueue != null) {mRunQueue.executeActions(info.mHandler);mRunQueue = null;}
总结
子线程在onAttachedToWindow之后调用view.post,是有效的。其次是与系统版本有一定关系,出现问题的场景就是子线程处理的完成数据之后调用view.post时,onAttachedToWindow还没有回调,一般是activity onCreate函数中初始化完成view之前这段时间可能出现不执行的问题。