美文网首页
为何onResume中getWidth为0,view.post获

为何onResume中getWidth为0,view.post获

作者: 瑜小贤 | 来源:发表于2021-10-02 01:01 被阅读0次

    开局一张图,先带大家看一下Activity、Window、View建立关系的流程

    然后进入正题,下面代码中的两个个方法,一个能获取到,一个获取不到,从log的表现来看,前一个方法的log还出现的更晚,具体原因,咱们细细道来。

        @Override
        protected void onResume() {
            super.onResume();
            view.post(new Runnable() {
                @Override
                public void run() {
                    Log.e("Test", "onResume() mBtn post button width=" + mBtn.getWidth());
                }
            });
    
            new Handler().post(new Runnable() {
                @Override
                public void run() {
                    Log.e("Test", "onResume() Handler button width=" + view.getWidth());
                }
            });
        }
    

    1、为什么在 onResume中, handler.post 获取控件的宽高是0

    Activity启动流程做参考
    在上图的最下方的 scheduleLaunchActivity() 接口方法之后的流程:handleLaunchActivity() -> handleResumeActivity() -> performResumeActivity() -> Activity#performResume() -> Instrumentation#callActivityOnResume() -> Activity#onResume()

    由上面一段做引子,我们先来看ActivityThread.java类中handleResumeActivity函数

        @Override
        public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                String reason) {
            ...
    
            // TODO Push resumeArgs into the activity for consideration
            //执行 onResume
            final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason); 
            if (r == null) {
                // We didn't actually resume the activity, so skipping any follow-up actions.
                return;
            }
    
            ...       
            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 (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l); //建立联系
                    } else {
                        // The activity will get a callback for this {@link LayoutParams} change
                        // earlier. However, at that time the decor will not be set (this is set
                        // in this method), so no action will be taken. This call ensures the
                        // callback occurs with the decor set.
                        a.onWindowAttributesChanged(l);
                    }
                }
    
                // If the window has already been added, but during resume
                // we started another activity, then don't yet make the
                // window visible.
            } else if (!willBeVisible) {
                if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }
    
         ...
        }
    

    在handleResumeActivity方法中可见,performResumeActivity方法的执行先于wm.addView(decor, l)方法。
    performResumeActivity方法代表着Activity.onResume的执行。
    wm.addView(decor, l)方法代表着window和view建立关系,然后一系列动作就开始了:

    1. 此时会新建ViewRootImpl,并调用setView方法
    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
        //...
        
        ViewRootImpl root;
        View panelParentView = null;
    
        synchronized (mLock) {
            //...
    
            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 {
            //触发开发绘制
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            //...
            throw e;
        }
    }
    
    1. setView方法会调用requestLayout方法,触发界面刷新,requestLayout方法算是老熟人了。
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
    
                //...
    
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                //触发界面刷新
                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) {
                    //...
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
    
                //...
    
                //这里的 view 是 DecorView
                view.assignParent(this);
                //...
            }
        }
    }
    
    1. requestLayout 会调用scheduleTraversals方法去发送一个消息,然后执行doTraversal方法进而调用performTraversals方法
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            //准备刷新,参考 3
            scheduleTraversals();
        }
    }
    
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //设置同步障碍 Message
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //屏幕刷新信号 VSYNC 监听回调把 mTraversalRunnable(执行doTraversal())push 到主线程了,异步 Message 会优先得到执行 ,具体看下 Choreographer 的实现
            //mTraversalRunnable,参考 TraversalRunnable
            mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
    
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            //参考 doTraversal
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //移除同步障碍 Message
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    
            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
    
            //参考 ,在上面移除同步障碍后,开始对控件树进行测量、布局、绘制
            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;
    
            //...
    
            // We used to use the following condition to choose 32 bits drawing caches:
            // PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888
            // However, windows are now always 32 bits by default, so choose 32 bits
            mAttachInfo.mUse32BitDrawingCache = true;
            mAttachInfo.mHasWindowFocus = false;
            mAttachInfo.mWindowVisibility = viewVisibility;
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mLastConfiguration.setTo(host.getResources().getConfiguration());
            mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
            // Set the layout direction if it has not been set before (inherit is the default)
            if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
                host.setLayoutDirection(mLastConfiguration.getLayoutDirection());
            }
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
            dispatchApplyInsets(host);
            //Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn);
    
        } else {
            desiredWindowWidth = frame.width();
            desiredWindowHeight = frame.height();
            if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
                if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
                mFullRedrawNeeded = true;
                mLayoutRequested = true;
                windowSizeMayChange = true;
            }
        }
    
        //...
    
        // Execute enqueued actions on every traversal in case a detached view enqueued an action
        getRunQueue().executeActions(mAttachInfo.mHandler);
        
        //...
        
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        performLayout(lp, mWidth, mHeight);
        performDraw();
        
        //...
    }
    

    所以 handler.post 消息回先执行导致获取 view 宽高失败,而new Handler.postDelay(runnable, 1000)再等待合适的时间后可能成功。

    2.view.post 中为什么能获取控件宽高

    1.我们来看View.java中的post方法

    public boolean post(Runnable action) {
        //mAttachInfo 是在 ViewRootImpl 的构造函数中初始化的
        //而 ViewRootmpl 的初始化是在 addView() 中调用
        //所以此处的 mAttachInfo 为空,所以不会执行该 if 语句
        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.
        //保存消息到 RunQueue 中,等到在 performTraversals() 方法中被执行
        getRunQueue().post(action);
        return true;
    }
    

    2.getRunQueue返回HandlerActionQueue

    private HandlerActionQueue getRunQueue() {
            if (mRunQueue == null) {
                mRunQueue = new HandlerActionQueue();
            }
            return mRunQueue;
        }
    

    3.把View#post()的消息放到了HandlerAction数组中

    public class HandlerActionQueue {
        private HandlerAction[] mActions;
        private int mCount;
    
        public void post(Runnable action) {
            postDelayed(action, 0);
        }
    
        public void postDelayed(Runnable action, long delayMillis) {
            final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
    
            synchronized (this) {
                if (mActions == null) {
                    mActions = new HandlerAction[4];
                }
                mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
                mCount++;
            }
        }
        ...
    }
    

    4.在View#dispatchAttachedToWindow()方法中,才给mAttachInfo赋了值,也是在这个方法里执行了executeActions方法,相当于是mAttachInfo不为空了,开始把HandlerAction保存下来的消息发出去吧

    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;
            }
            ...
    }
    

    5.HandlerActionQueue#executeActions()把消息从HandlerAction数组中转到handler中,正儿八经开始发送消息

    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;
            }
        }
    

    按理说这样已经能够基本证明为什么view.post能够获取高度,大白话说就是

    因为attachInfo为空,View把post的runnable保存了下来,等待attachInfo不为空的时候发消息去执行runnable

    但是为了严谨,还要继续做一下验证,例如
    6.步骤4中的info.handlerViewRootImpl的handler

    public ViewRootImpl(Context context, Display display) {
            mContext = context;
            mWindowSession = WindowManagerGlobal.getWindowSession();
            mDisplay = display;
            mBasePackageName = context.getBasePackageName();
            mThread = Thread.currentThread();
            mLocation = new WindowLeaked(null);
            mLocation.fillInStackTrace();
            mWidth = -1;
            mHeight = -1;
            mDirty = new Rect();
            mTempRect = new Rect();
            mVisRect = new Rect();
            mWinFrame = new Rect();
            mWindow = new W(this);
            mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
            mViewVisibility = View.GONE;
            mTransparentRegion = new Region();
            mPreviousTransparentRegion = new Region();
            mFirst = true; // true for the first time the view is added
            mAdded = false;
            mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                    context);
            ...
    }
    ...
    
    final ViewRootHandler mHandler = new ViewRootHandler();
    ...
    

    7.布局测量的handler是ViewRootImpl的,和步骤4中View#post()的handler是同一个

    void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                //指的是这个handler
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    

    8.ViewRootImpl#performTraversals()调用View#dispatchAttachedToWindow(),说明 界面绘制 已经开始了
    步骤4中,View#post()的消息从HandlerAction数组中转到ViewRootImpl#mHandler中,说明在ViewRootImpl#mHandler中,View#post()的消息是在 界面绘制 消息之后的,所以View#post()这个消息是能获得宽高的。

    相关文章

      网友评论

          本文标题:为何onResume中getWidth为0,view.post获

          本文链接:https://www.haomeiwen.com/subject/lqspnltx.html