一般需求中会出现在Activity启动中需要获取Ui控件相关大小或者在界面绘制完成之后刷新数据,我们都知道在UI绘制完成之后,时机最好,不会阻塞主线程导致卡顿或者UI控件参数获取失败。 也许大家使用过或 知道Handler(MainLooper).Post(Runnable)和View.Post(Runnable)都是把Runnable封装成Message再 push到主线成中looper中MessageQueue中,会发现在Activity的生命周期中执行这两种方式效果不同,前者不满足我们的需求,而后者却能做到,但这是为啥,有没有深入分析,本文就从Activity启动流程以及UI刷新和绘制流程原理以及消息循环机制、同步障碍机制来剖析。
先看demo运行效果,以获取Ui控件大小为例子,如下:
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");
}
}
日志显示结果:
4-19 17:24:05.543 27745-27745/com.happyelements.AndroidAnimal I/chuan: myView init
04-19 17:24:05.545 27745-27745/com.happyelements.AndroidAnimal I/chuan: Main onCreate
04-19 17:24:05.570 27745-27745/com.happyelements.AndroidAnimal I/chuan: onCreate init myCustomView width=0
04-19 17:24:05.578 27745-27745/com.happyelements.AndroidAnimal I/chuan: Main onResume
04-19 17:24:05.577 27745-27745/com.happyelements.AndroidAnimal I/chuan:
myView post mAttachInfo=null?true
04-19 17:24:05.584 27745-27745/com.happyelements.AndroidAnimal I/chuan: onResume Handler post runnable button width=0
04-19 17:24:05.594 27745-27745/com.happyelements.AndroidAnimal I/chuan: myView onAttachedToWindow width=0
myView onAttachedToWindow mAttachInfo=null?false
04-19 17:24:05.630 27745-27745/com.happyelements.AndroidAnimal I/chuan: myView layout
04-19 17:24:05.630 27745-27745/com.happyelements.AndroidAnimal I/chuan: myView onDraw
04-19 17:24:05.631 27745-27745/com.happyelements.AndroidAnimal I/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)
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)
View.class
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
//先是通过attachInfo.mHandler.post来实现,实际上就是用Handler.post和上述一样
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
//若attachInfo =null时,维护一个mRunQueue 队列,然后在dispatchAttachedToWindow通过mRunQueue.executeActions(info.mHandler);跟上述方法一样
getRunQueue().post(action);
return true;
}
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mOverlay != null) {
mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
}
..........省略....
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
onAttachedToWindow();
.......省略......
}
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
HandlerActionQueue.class
//实际也是通过handler来post到主线程
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
private class ApplicationThread extends ApplicationThreadNative {
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
....省略....
sendMessage(H.LAUNCH_ACTIVITY, r);
}
private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
if (DEBUG_MESSAGES) Slog.v(
TAG, "SCHEDULE " + what + " " + mH.codeToString(what)
+ ": " + arg1 + " / " + obj);
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
msg.arg1 = arg1;
msg.arg2 = arg2;
if (async) {
msg.setAsynchronous(true);
}
mH.sendMessage(msg);
}
}
mH(Handler)会把这个异步消息加入到MainLooper中MessageQueue,等到执行时候回调handleLaunchActivity
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY:
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
handleLaunchActivity方法会执行很多方法,这个是入口,简单来说会创建Activity对象,调用其启动生命周期,attach、onCreate、onStart、onResume,以及添加到WindowManager中,重点看下本文中onResume生命周期是如何回调的。在Activity可见之后,紧接着就是要触发绘制界面了,会走到handleResumeActivity方法,会performResumeActivity调用activity的onResume方法
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();
//调用activity的onResume方法
r.activity.performResume();
...............................
} catch (Exception e) {
........
}
}
return r;
}
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
//1、调用activity的onResume方法
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();
//2、decorView先暂时隐藏
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;
//3、关键函数 添加到window触发ui测量、布局、绘制
wm.addView(decor, l);
}
..............
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
//4、添加decorView之后,设置可见,从而显示了activity的界面
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,源码如下
WindowManagerGlobal.class
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...............
ViewRootImpl root;
View panelParentView = null;
......
//ViewRootImpl整个UI操作实际控制着
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 {
//绑定decorView,并触发开发绘制
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();
//通过binder call添加到Display
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
} finally {
if (restore) {
attrs.restore();
}
}
...............
//decorView添加父类ViewParent 就是ViewRootImpl
view.assignParent(this);
}
}
}
setView完成了上述几个重要步骤,其中requestLayout的实现是如何触发刷新绘制的:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
//安排刷新请求
scheduleTraversals();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void scheduleTraversals() {
//一个刷新周期只执行一次即可,屏蔽其他的刷新请求
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//设置同步障碍Message
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//屏幕刷新信号VSYNC 监听回调把mTraversalRunnable(执行doTraversal()) push到主线程了且是个异步Message会优先得到执行 ,具体看下Choreographer的实现
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步障碍Message
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//真正执行decorView的绘制
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
.............
Rect frame = mWinFrame;
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
.......
mAttachInfo.mUse32BitDrawingCache = true;
mAttachInfo.mHasWindowFocus = false;
mAttachInfo.mWindowVisibility = viewVisibility;
mAttachInfo.mRecomputeGlobalAttributes = false;
//performTraversals 第一次调用时候decorView dispatch mAttachInfo变量
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
if (DEBUG_ORIENTATION) Log.v(TAG,
"View " + host + " resized to: " + frame);
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
...................
//会根据状态判断是否执行,对mVIew(decorView)执行view的测量、布局、绘制
perforMeasure()
perforLayout()
perforDraw()
mFirst=false;
}
从上述代码可以发现在addView之后同步执行到requestLayout,再到scheduleTraversals中设置了同步障碍消息,这个简单阐述,看下源码实现:
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) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
//根据token移动这个Message
public void removeSyncBarrier(int token) {
// Remove a sync barrier token from the queue.
// If the queue is no longer stalled by a barrier then wake it.
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 the loop is quitting then it is already awake.
// We can assume mPtr != 0 when mQuitting is false.
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
MessageQueue同步障碍机制: 可以发现就是把一条Message,注意这个Message是没有设置target的,整个消息循环唯一一处不设置回调的target(hander),因为这个即使标志了同步障碍消息,也是不需要handler来pushMessage到队列中,直接手动循环移动链表插入到合适time的Message之后的即可。然后是如何识别这个障碍消息的呢,在Looper的loop循环获取MessageQueue.next()函数获取下一个的message,是如何实现的,
MessageQueue.class
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
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;
//关键地方,首先识别 msg.target == null情况就是同步障碍消息,如果该消息是同步障碍消息的话,就会循环查询下一个消息是否是isAsynchronous状态,异步Message,专门给刷新UI消息使用的
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//如果查到异步消息或者没有设置同步障碍的消息,直接返回执行
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
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 {
// No more messages.
nextPollTimeoutMillis = -1;
}
........省略.................
nextPollTimeoutMillis = 0;
}
}
可以看到scheduleTraversals中设置了同步障碍消息,就是相当于在MessageQueue中插入了一个Message,并且是在onResume之后插入的,所以在onResume中handler.post(Runnable)之后,这个消息会在同步障碍Message之前,会先被执行,这个时候依然没有刷新绘制界面,待查询到同步障碍Message时候,会等待下个异步Message(刷新Message)出现。所以在onResume中handler.post(Runnable)是Ui操作失效的。
那么为何View.post(Runnable)就可以了,再回过头来看下其源码:
View.class
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;
}
由于在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,其源码如下:
View.class
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mOverlay != null) {
//向下分发info,其实现在ViewGroup中
mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
}
mWindowAttachCount++;
// We will need to evaluate the drawable state at least once
.........
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
onAttachedToWindow();
........
}
可以看到这个函数同时执行了 mRunQueue.executeActions(info.mHandler);
从上文可知就是通过hander把mRunQueue中任务全部push到主线程中。由此可以知道在performTraversals(Message)中push Message到主线中,肯定会这个performTraversals(Message)之后再执行,并且在doTraversals中移除了同步障碍消息(Message),故会依次执行。所以onResume中View.post的Message就会在performTraversals之后执行,而performTraversals就是完成了View整个测量、布局和绘制。当View的mAttachInfo !=null时也说明肯定完成过UI绘制。
感谢看完,看似简单的东西,其实内部原理没有分析清楚容易糊涂,同时研究源码会学到很多相关的知识点,例如要看懂本文就需要了解上午中提到的4个知识点。
1. Activity生命周期启动流程
2. Message消息发送和执行原理机制
3. UI绘制刷新触发原理机制
4. MessageQueue同步障碍机制**