Android-高工面试真题:说说你对FPS的理解?如何检测应用的FPS?

本文首发于掘金:面试官:如何监测应用的 FPS ?
作者:秉心说TM

目录

什么是 FPS ?

即使你不知道 FPS,但你一定听说过这么一句话,在 Android 中,每一帧的绘制时间不要超过 16.67ms。那么,这个 16.67ms 是怎么来的呢?就是由 FPS 决定的。

FPS,Frame Per Second,每秒显示的帧数,也叫 帧率。Android 设备的 FPS 一般是 60,也即每秒要刷新 60 帧,所以留给每一帧的绘制时间最多只有 1000/60 = 16.67ms 。一旦某一帧的绘制时间超过了限制,就会发生 掉帧,用户在连续两帧会看到同样的画面。

监测 FPS 在一定程度上可以反应应用的卡顿情况,原理也很简单,但前提是你对屏幕刷新机制和绘制流程很熟悉。所以我不会直接进入主题,让我们先从 View.invalidate() 说起。

从 View.invalidate() 说起

要探究屏幕刷新机制和 View 绘制流程,View.invalidate() 无疑是个好选择,它会发起一次绘制流程。

> View.java

public void invalidate() {
    invalidate(true);
}

public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
    boolean fullInvalidate) {
    ......
    final AttachInfo ai = mAttachInfo;
    final ViewParent p = mParent;
    if (p != null && ai != null && l < r && t < b) {
        final Rect damage = ai.mTmpInvalRect;
        damage.set(l, t, r, b);
	// 调用 ViewGroup.invalidateChild()
        p.invalidateChild(this, damage);
    }
    ......
}

这里调用到 ViewGroup.invalidateChild()

> ViewGroup.java

public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    ......
    ViewParent parent = this;
    if (attachInfo != null) {
        ......
        do {
            View view = null;
            if (parent instanceof View) {
                view = (View) parent;
            }
            ......
            parent = parent.invalidateChildInParent(location, dirty);
            ......
        } while (parent != null);
    }
}

这里有一个递归,不停的调用父 View 的 invalidateChildInParent() 方法,直到最顶层父 View 为止。这很好理解,仅靠 View 本身是无法绘制自己的,必须依赖最顶层的父 View 才可以测量,布局,绘制整个 View 树。但是最顶层的父 View 是谁呢?是 setContentView() 传入的布局文件吗?不是,它解析之后被塞进了 DecorView 中。是 DecorView 吗?也不是,它也是有父亲的。

DecorView 的 parent 是谁呢?这就得来到 ActivityThread.handleResume() 方法中。

> ActivityThread.java

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
    ......
    // 1\. 回调 onResume()
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    ......
    View decor = r.window.getDecorView();
    decor.setVisibility(View.INVISIBLE);
    ViewManager wm = a.getWindowManager();
    // 2\. 添加 decorView 到 WindowManager
    wm.addView(decor, l);
    ......
}

第二步中实际调用的是 WindowManagerImpl.addView() 方法,WindowManagerImpl 中又调用了 WindowManagerGlobal.addView() 方法。

> WindowManagerGlobal.java

// 参数 view 就是 DecorView
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    ......
    ViewRootImpl root;
    // 1\. 初始化 ViewRootImpl
    root = new ViewRootImpl(view.getContext(), display);

    mViews.add(view);
    mRoots.add(root);
    // 2\. 重点在这
    root.setView(view, wparams, panelParentView);
    ......
}

跟进 ViewRootImpl.setView() 方法。

> ViewRootImpl.java

// 参数 view 就是 DecorView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;

            // 1\. 发起首次绘制
            requestLayout();

            // 2\. Binder 调用 Session.addToDisplay(),将 window 添加到屏幕
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);

            // 3\. 重点在这,注意 view 是 DecorView,this 是 ViewRootImpl 本身
            view.assignParent(this);
        }
    }
}

跟进 View.assignParent() 方法。

> View.java

// 参数 parent 是 ViewRootImpl
void assignParent(ViewParent parent) {
    if (mParent == null) {
        mParent = parent;
    } else if (parent == null) {
        mParent = null;
    } else {
        throw new RuntimeException("view " + this + " being added, but"
                + " it already has a parent");
    }
}

还记得我们跟了这么久在干嘛吗?为了探究 View 的刷新流程,我们跟着 View.invalidate() 方法一路追到 ViewGroup.invalidateChild() ,其中递归调用 parent 的 invalidateChildInParent() 方法。所以我们在 给 DecorView 找爸爸 。现在很清晰了,DecorView 的爸爸就是 ViewRootImpl ,所以最终调用的就是 ViewRootImpl.i

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值