onResume中Handler.post(Runnable)为什么获取不到宽高?

原文地址:https://blog.csdn.net/u010019468/article/details/80007675

貌似已无效。

原文如下:

1

概述

 

一般需求中会出现在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");
        }
}

 

日志显示结果:

 

/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 

 

从日志中可以看出几点

 

  1. 在Activity可交互之前的生命周期中UI直接操作是失效的,即使通过handler把Ui操纵任务post到onResume生命周期之后,也依然获失效,日志可以看到此时ui界面都没有绘制。

  2. 发现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调用一样为何效果不一样,下面就从如下几个知识点来分析:

 

  1. Activity生命周期启动流程

  2. Message消息发送和执行原理机制

  3. UI绘制刷新触发原理机制

  4. MessageQueue同步障碍机制

 

 

2

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 pendingResults, List 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) {
        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同步障碍机制

你可能感兴趣的:(基础知识)