美文网首页
【Android面试题】高级UI面试题——在onResume中是

【Android面试题】高级UI面试题——在onResume中是

作者: 小城哇哇 | 来源:发表于2023-08-29 16:50 被阅读0次

在onResume中是否可以测量宽高?

这道题想考察什么?

这道题想考察同学对 View的绘制流程 的理解。

考生应该如何回答

这个问题答案是:不一定能够正确的获取view的宽高,然后我们要分析原因,然后我们还要讲解解决办法。

如果是activity 启动后第一次进入onResume 生命周期,那么获取到的View的宽高是错误的;如果是从其他activity回到当前activity而执行的onResume方法,那么就能够获取到View的宽高。究其原因如下:

首先我们要找到系统调用 onResume 的地方,大家可以看到 ActivityThread 类,在这个类中有一个函数handleResumeActivity,这个函数在下面我们只保留了核心代码。

handleResumeActivity() {
    //...
    r = performResumeActivity(token, clearHide, reason);//调用 onResume() 方法
    //...
    wm.addView(decor, l);// WindowManager添加Decor(decor是DecorView)
    //...
}

可以看出 onResume() 方法在 addView() 方法前调用。
重点关注:onResume() 方法所处的位置,前后都发生了什么?
从上面总结的流程看出,onResume() 方法是由 handleResumeActivity 触发的,而界面绘制被触发是因为 handleResumeActivity() 中调用了wm.addView(decor, l),大家看下面的分析;

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 {
        //触发开发绘制,参考 1
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        //...
        throw e;
    }
}

参考 1:setView()

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.
            //触发界面刷新,参考 2
            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);
            //...
        }
    }
}

参考 2:requestLayout()

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        //准备刷新,参考 3
        scheduleTraversals();
    }
}

参考 3:scheduleTraversals(),这个函数的主要作用是告诉安卓系统app端需要界面的刷新,并且设置一个回调,用于接收系统发送的同步app到系统的信号。

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //设置同步障碍 Message
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //屏幕刷新信号 VSYNC 监听回调把 mTraversalRunnable(执行doTraversal())push 到主线程了,异步 Message 会优先得到执行 ,具体看下 Choreographer 的实现
        //mTraversalRunnable,参考 4
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

参考 4:mTraversalRunnable,当系统审核app端刷新界面的申请后,在合适的时机会调用之前设定的TraversalRunnable回调。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        //参考 5
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

参考 5:doTraversal()

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步障碍 Message
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        //参考 6,在上面移除同步障碍后,开始对控件树进行测量、布局、绘制
        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

参考 6:performTraversals()

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);// code 7
    
    //...
    
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//触发viewTree onMeasure
    ...
    performLayout(lp, mWidth, mHeight);//触发viewTree onLayout
    ...
    performDraw();//触发ViewTree onDraw
    
    //...
}

小结:

在上面的代码中,performTraversals 函数是整个viewTree 的onMeasure 、onLayout、onDraw的发起点。所以,基于上面的代码流程分析大家不难发现activity 的onResume 函数是在 viewTree 的onMeasure 函数执行之前先执行,也就是在执行onResume函数的时候,viewTree并未完成度量。那么,当我们去onResume函数里面获取view 的宽高自然是获取不到的。

那么如何在onResume中获取view的宽高呢?

在 onResume() 中 handler.post(Runnable) 是无法正确的获取不到 View 的真实宽高

原因:能够获取到view的宽高的前提条件是view要完成onMeasure,那么我们的handler.post 的方式是不能够保证 runnable一定是在view进行了onMeasure后执行的,因此,这个方式不行。同样的道理,运用handler.postDelay也不是最佳的方案,因为,不同型号的手机代码的执行时间不同,导致postDelay到底需要delay多久对于用户来说仍旧是未知数。那么正确的做法是怎样的呢?答案就是调用View.post(Runnable)。

View.post(Runnable) 为什么可以获取到 View 的宽高?
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;
}

由参考 6 中的code 7 可知,通过 View.post(Runnable) 的 Message 会在 performMeaure() 之前被调用,那为什么还可以正确的获取到 View 的宽高呢?其实我们的 Message 并没有立即被执行,因为此时主线程的 Handler 正在执行的 Message 是 TraversalRunnable,而 performMeaure() 方法也是在该 Message 中被执行,所以排队等到主线程的 Handler 执行到我们 post 的 Message 时,View 的宽高已经测量完毕,因此我们也就很自然的能够获取到 View 的宽高。

总结

在 onResume() 中直接获取或者调用创建的 Handler 的 post 方法都是获取不到 View 宽高的,需要通过 View.post 才能获取。

最后

有需要以上面试题的朋友可以关注一下哇哇,以上都可以分享!!!

相关文章

  • Android面试题

    Android面试必备: (一)、Java面试题 (二)、Android面试题 (三)、Android高级面试题

  • l 主要分为以下几部分: (1)java 面试题 (2)Android 面试题 (3)高级开发技术面试题 (...

  • 源码阅读分析-view的绘制流程

    高级面试题:首先看案例 上面我们可以看到oncreate和onResume中获得高度为0,即没有获得高度,而开个线...

  • Android面试题与解析

    主要分为以下几部分: (1)java面试题 (2)Android面试题 (3)高级开发技术面试题 (4)跨平台Hy...

  • 2019-10-17

    主要分为以下几部分:(1)java面试题 (2)Android面试题 (3)高级开发技术面试题 (4)跨平台Hyb...

  • 面试题

    关注的面试题文集 Android面试题整理 Android工程师面试题大全 Android 面试题总结之Andro...

  • ios面试题

    初级面试题 中级面试题 高级面试题 swift篇

  • Android面试总结二

    参考文章 40个Android面试题Java面试题集Android名企面试题及知识点整理Android面试题收集较...

  • Android超实用最全面试大纲(三)

    文章目录: ANR面试题 OOM面试题 Bitmap面试题 UI卡顿面试题 内存泄漏面试题 内存管理面试题 一、A...

  • Android最全面试大纲(三)

    文章目录: ANR面试题 OOM面试题 Bitmap面试题 UI卡顿面试题 内存泄漏面试题 内存管理面试题 一、A...

网友评论

      本文标题:【Android面试题】高级UI面试题——在onResume中是

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