onResume中Handler.post(Runnable)为什么获取不到宽高? 一般需求中会出现在Activity启动中需要获取Ui控件相关大小或者在界面绘制完成之后刷新数据,我们都知道在UI绘制完成之后,时机最好,不会阻塞主线程导致卡顿或者UI控件参数获取失败。
也许大家使用过或 知道Handler(MainLooper).Post(Runnable)和View.Post(Runnable)都是把Runnable封装成Message再 push到主线程中looper中MessageQueue中,会发现在Activity的生命周期中执行这两种方式效果不同,前者不满足我们的需求,而后者却能做到,但这是为啥,有没有深入分析,本文就从Activity启动流程以及UI刷新和绘制流程原理以及消息循环机制、同步障碍机制来剖析。
先看demo运行效果,以获取Ui控件大小为例子,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 class MyActivity extends Activity {..... @Override protected void onCreate (Bundle savedInstanceState) super .onCreate (savedInstanceState) ; setContentView(R.layout.activity_main); myCustomView = findViewById(R.id.custom); Log.i("chuan" , "onCreate init myCustomView width=" + myCustomView.getWidth()); } @Override protected void onResume () { super .onResume(); Log.i("chuan" , "Main onResume" ); new Handler().post(new Runnable() { @Override public void run () { Log.i("chuan" , "onResume Handler post runnable button width=" + myCustomView.getWidth()); } }); myCustomView.post(new Runnable() { @Override public void run () { Log.i("chuan" , "onResume myCustomView post runnable button width=" + myCustomView.getWidth()); } }); } public class MyView extends View { @Override public void layout (int l, int t, int r, int b) { super .layout(l, t, r, b); Log.i("chuan" , "myView layout" ); } @Override protected void onAttachedToWindow () { Log.i("chuan" , "myView onAttachedToWindow with" +getWidth()); try { Object mAttachInfo = ReflectUtils.getDeclaredField(this , View.class, "mAttachInfo" ); Log.i("chuan" , "myView onAttachedToWindow mAttachInfo=null?" + (mAttachInfo == null )); Object mRunQueue = ReflectUtils.getDeclaredField(this , View.class, "mRunQueue" ); Log.i("chuan" , "myView onAttachedToWindow mRunQueue=null?" + (mRunQueue == null )); } catch (Exception e) { } super .onAttachedToWindow(); } @Override public boolean post (Runnable action) { try { Object mAttachInfo = ReflectUtils.getDeclaredField(this , View.class, "mAttachInfo" ); Log.i("chuan" , "myView post mAttachInfo=null?" + (mAttachInfo == null )); } catch (Exception e) { } return super .post(action); } @Override protected void onDraw (Canvas canvas) { super .onDraw(canvas); Log.i("chuan" , "myView onDraw" ); } }
日志显示结果:
/chuan: myView init /chuan: Main onCreate /chuan: onCreate init myCustomView width=0 /chuan: Main onResume /chuan: myView post mAttachInfo=null?true /chuan: onResume Handler post runnable button width=0 /chuan: myView onAttachedToWindow width=0 myView onAttachedToWindow mAttachInfo=null?false /chuan: myView layout /chuan: myView onDraw /chuan: onResume myCustomView post runnable button width=854
从日志中可以看出几点
在Activity可交互之前的生命周期中UI直接操作是失效的,即使通过handler把Ui操纵任务post到onResume生命周期之后,也依然获取失效,日志可以看到此时ui界面都没有绘制。
发现View.post(Runnable)会让runnable在该View完成了measure、layout、draw之后再执行,这个时候当然就可以获取到Ui相关参数了。
先看下两者的源码实现:
1、handler.post(Runnable)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Handler.class public final boolean post (Runnable r) { return sendMessageDelayed(getPostMessage(r), 0 ); } public boolean sendMessageAtTime (Message msg, long uptimeMillis) { MessageQueue queue = mQueue; if (queue == null ) { RuntimeException e = new RuntimeException( this + " sendMessageAtTime() called with no mQueue" ); Log.w("Looper" , e.getMessage(), e); return false ; } return enqueueMessage(queue, msg, uptimeMillis); } private boolean enqueueMessage (MessageQueue queue, Message msg, long uptimeMillis) { msg.target = this ; if (mAsynchronous) { msg.setAsynchronous(true ); } return queue.enqueueMessage(msg, uptimeMillis); }
代码简单可以看到就是把runnable封装成Message然后加入当前Looper的MessageQueue队列中。
2、再看下View.post(Runnable)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public boolean post (Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null ) { return attachInfo.mHandler.post(action); } getRunQueue().post(action); return true ; } void dispatchAttachedToWindow (AttachInfo info, int visibility) { mAttachInfo = info; if (mOverlay != null ) { mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility); } ..........省略.... if (mRunQueue != null ) { mRunQueue.executeActions(info.mHandler); mRunQueue = null ; } performCollectViewAttributes(mAttachInfo, visibility); onAttachedToWindow(); .......省略...... } private HandlerActionQueue getRunQueue () { if (mRunQueue == null ) { mRunQueue = new HandlerActionQueue(); } return mRunQueue; } public void executeActions (Handler handler) { synchronized (this ) { final HandlerAction[] actions = mActions; for (int i = 0 , count = mCount; i < count; i++) { final HandlerAction handlerAction = actions[i]; handler.postDelayed(handlerAction.action, handlerAction.delay); } mActions = null ; mCount = 0 ; } }
通过源码调用发现最终都是通过handler.post()方式来加入到主线程队列中,api调用一样为何效果不一样,下面就从如下几个知识点来分析:
Activity生命周期启动流程
Message消息发送和执行原理机制
UI绘制刷新触发原理机制
MessageQueue同步障碍机制
Activity启动流程 :ApplicationThread收到AMS的scheduleLaunchActivity的Binder消息之后,原因是binder线程,会通过ActivityThread中的mH(Handler)来sendMessage;
mH(Handler)会把这个异步消息加入到MainLooper中MessageQueue,等到执行时候回调handleLaunchActivity,handleLaunchActivity方法会执行很多方法,这个是入口,简单来说会创建Activity对象,调用其启动生命周期,attach、onCreate、onStart、onResume,以及添加到WindowManager中,重点看下本文中onResume生命周期是如何回调的。
在Activity可见之后,紧接着就是要触发绘制界面了,会走到handleResumeActivity方法,会performResumeActivity调用activity的onResume方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public final ActivityClientRecord performResumeActivity (IBinder token, boolean clearHide) { ActivityClientRecord r = mActivities.get(token); if (r != null && !r.activity.mFinished) { ..................... try { r.activity.onStateNotSaved(); r.activity.mFragments.noteStateNotSaved(); r.activity.performResume(); ............................... } catch (Exception e) { ........ } } return r; } final void handleResumeActivity (IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { ActivityClientRecord r = performResumeActivity(token, clearHide); ...... if (r != null ) { final Activity a = r.activity; final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0 ; ....................... if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true ; wm.addView(decor, l); } .............. r.activity.mVisibleFromServer = true ; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) { r.activity.makeVisible(); } } }
由此可见:从handleResumeActivity执行流程来看onResume调用时候,Activity中的UI界面并没有经过measure、layout、draw等流程,所以直接在onResume或者之前的onCreate中执行ui操纵都是无用的,因为这个时候Ui界面不可见,没有绘制。那为何通过hander.post(Runnable)让执行发生在handleLaunchActivity这个Message之后,这个时候流程已经走完了,也是在Ui界面触发绘制之后,怎么还是不行呢。
Message消息发送和执行原理机制这里就不阐述了,hander.post(Runnable)让执行发生在handleLaunchActivity这个Message之后就是因为这个Message循环机制原理,可以让任务通常让加入的先后顺序依次执行,所以handleLaunchActivity这个Message执行之后,就是onResume中的push的Message。
但是为何onResume中hander.post(Runnable)还不能ui操作呢,就猜测handleLaunchActivity之后还没有同步完成UI绘制,那UI绘制刷新触发原理机制是怎么样的了,直接分析触发条件,上文中的wm.addVIew开始:windowManager会通过子类WindowManagerImpl来实现,其内部又通过WindowManagerGlobal的单实例来实现addVIew,源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 WindowManagerGlobal.class 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) { } } addView动作又转给ViewRootImpl.setView来实现,具体源码如下: ViewRootImpl.class public void setView (View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this ) { if (mView == null ) { mView = view; .......... requestLayout(); if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0 ) { mInputChannel = new InputChannel(); } .......... try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true ; collectViewAttributes(); res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } catch (RemoteException e) { } finally { if (restore) { attrs.restore(); } } ............... view.assignParent(this ); } } }
setView完成了上述几个重要步骤,其中requestLayout的实现是如何触发刷新绘制的:
从上述代码可以发现在addView之后同步执行到requestLayout,再到scheduleTraversals中设置了同步障碍消息,这个简单阐述,看下源码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 MessageQueue.class private int postSyncBarrier (long when) { synchronized (this ) { final int token = mNextBarrierToken++; final Message msg = Message.obtain(); msg.markInUse(); msg.when = when; msg.arg1 = token; Message prev = null ; Message p = mMessages; if (when != 0 ) { while (p != null && p.when <= when) { prev = p; p = p.next; } } if (prev != null ) { msg.next = p; prev.next = msg; } else { msg.next = p; mMessages = msg; } return token; } } public void removeSyncBarrier (int token) { synchronized (this ) { Message prev = null ; Message p = mMessages; while (p != null && (p.target != null || p.arg1 != token)) { prev = p; p = p.next; } if (p == null ) { throw new IllegalStateException("The specified message queue synchronization " + " barrier token has not been posted or has already been removed." ); } final boolean needWake; if (prev != null ) { prev.next = p.next; needWake = false ; } else { mMessages = p.next; needWake = mMessages == null || mMessages.target != null ; } p.recycleUnchecked(); if (needWake && !mQuitting) { nativeWake(mPtr); } } } ```Java MessageQueue同步障碍机制: 可以发现就是把一条Message,注意这个Message是没有设置target的,整个消息循环唯一一处不设置回调的target(hander),因为这个即使标志了同步障碍消息,也是不需要handler来pushMessage到队列中,直接手动循环移动链表插入到合适time的Message之后的即可。 然后是如何识别这个障碍消息的呢,在Looper的loop循环获取MessageQueue.next()函数获取下一个的message,是如何实现的, ```Java MessageQueue.class Message next () { final long ptr = mPtr; if (ptr == 0 ) { return null ; } int pendingIdleHandlerCount = -1 ; int nextPollTimeoutMillis = 0 ; for (;;) { if (nextPollTimeoutMillis != 0 ) { Binder.flushPendingCommands(); } nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this ) { final long now = SystemClock.uptimeMillis(); Message prevMsg = null ; Message msg = mMessages; if (msg != null && msg.target == null ) { do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } if (msg != null ) { if (now < msg.when) { nextPollTimeoutMillis = (int ) Math.min(msg.when - now, Integer.MAX_VALUE); } else { mBlocked = false ; if (prevMsg != null ) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null ; if (DEBUG) Log.v(TAG, "Returning message: " + msg); msg.markInUse(); return msg; } } else { nextPollTimeoutMillis = -1 ; } ........省略................. nextPollTimeoutMillis = 0 ; } }
可以看到scheduleTraversals中设置了同步障碍消息,就是相当于在MessageQueue中插入了一个Message,并且是在onResume之后插入的,所以在onResume中handler.post(Runnable)之后,这个消息会在同步障碍Message之前,会先被执行,这个时候依然没有刷新绘制界面,待查询到同步障碍Message时候,会等待下个异步Message(刷新Message)出现。
所以在onResume中handler.post(Runnable)是Ui操作失效的。
那么为何View.post(Runnable)就可以了,再回过头来看下其源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 View.class public boolean post (Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null ) { return attachInfo.mHandler.post(action); } getRunQueue().post(action); return true ; }
由于在onResume中执行,这个时候ViewRootImpl还没有初始化(addView时),而mAttachInfo是在ViewRootImpl构造函数中初始化的,过此时mAttachInfo=null,从上文知道 getRunQueue()维护了一个mRunQueue 队列,然后在dispatchAttachedToWindow通过mRunQueue.executeActions(info.mHandler);那这个方法dispatchAttachedToWindow什么会被调用,回顾上文中ViewRootImpl第一次收到Vsync同步刷新信号之后会执行performTraversals,这个函数内部做了个判断当时第一次mFirst时候会调用host.dispatchAttachedToWindow(mAttachInfo, 0);把全局mAttachInfo下发给所有子View,其源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 View.class void dispatchAttachedToWindow (AttachInfo info, int visibility) { mAttachInfo = info; if (mOverlay != null ) { mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility); } mWindowAttachCount++; ......... if (mRunQueue != null ) { mRunQueue.executeActions(info.mHandler); mRunQueue = null ; } performCollectViewAttributes(mAttachInfo, visibility); onAttachedToWindow(); ........ }
由此可以知道在performTraversals(Message)中push Message到主线中,肯定会这个performTraversals(Message)之后再执行,并且在doTraversals中移除了同步障碍消息(Message),故会依次执行。所以onResume中View.post的Message就会在performTraversals之后执行,而performTraversals就是完成了View整个测量、布局和绘制。当View的mAttachInfo !=null时也说明肯定完成过UI绘制。
感谢看完,看似简单的东西,其实内部原理没有分析清楚容易糊涂,同时研究源码会学到很多相关的知识点,例如要看懂本文就需要了解上文中提到的4个知识点。
Activity生命周期启动流程
Message消息发送和执行原理机制
UI绘制刷新触发原理机制
MessageQueue同步障碍机制