专栏首页逮虾户Android 统计页面渲染时长

Android 统计页面渲染时长

文章开头还是先抛出几个小小的问题,大家在开发的时候有没有考虑过一个问题,onCreate方法执行完了是不是页面已经完全打开了呢?为什么呢?

什么是页面渲染时长?

我们先聊聊页面渲染时长的定义。简单的说,我们把一个页面从创建到渲染完成出现第一帧作为一个页面的渲染时间,当然这个也不能作为完全的参考,毕竟复杂的商业应用都涉及到接口相关的,所以第一帧并不代表完全可用。

接下来我们要先从文章开始的问题开始分析。ActivityonCreate方法执行完能不能作为页面完全打开?答案吗自然肯定是不行了。原因我们就先从Activity的生命周期聊起。对了,本文不讨论任何关于ams(ActivityManagerService)相关的东西哦(作者实在是太菜了,吃不透啊)。

当一个Activity被打开之后,会触发ActivityonCreate,onStart,onResume方法。因为AMS我不熟源码我也吃的不是特别透所以我不展开,但是AMS最后会触发ActivityThread内部的ApplicationThread去启动一个页面,所以我们可以认为所有的Activity生命周期方法都是被执行在主线程上的(如果我写的有问题,可以留言打脸我)。有兴趣的可以参考下这个文章

由于29的源代码发生了变化,所以我暂时还没找到源代码的位置所在,感觉可能是变成事务机制了。

public final class ActivityThread extends ClientTransactionHandler {
    class H extends Handler {
        public void handleMessage(Message msg) {
        
        ## 由于源代码升级了,所以都被更换到这个方法内了
             case EXECUTE_TRANSACTION:
                    final ClientTransaction transaction = (ClientTransaction) msg.obj;
                    mTransactionExecutor.execute(transaction);
                    if (isSystem()) {
                        // Client transactions inside system process are recycled on the client side
                        // instead of ClientLifecycleManager to avoid being cleared before this
                        // message is handled.
                        transaction.recycle();
                    }
                    // TODO(lifecycler): Recycle locally scheduled transactions.
                    break;
            case RELAUNCH_ACTIVITY:
                    handleRelaunchActivityLocally((IBinder) msg.obj);
                    break;
        }
    } 
    
     private class ApplicationThread extends IApplicationThread.Stub {
     }
 }
复制代码

因为所有生命周期相关的方法全部执行在主线程的handler上,所以我们可以理解为顺序执行一个个msg任务。那么是不是onResume方法执行完了之后就是页面已经完全创建完成了呢?是不是onResume方法之后救能看到View了呢?

onCreate下获取View的宽高

答案肯定是不能。举个栗子,有时候做些页面宽高动态计算的时候你就会发现,在生命周期内的方法内去获取view的宽高,拿到的值都是0。那么我们一般会怎么解决这个问题呢?

  1. ViewTreeObserver 通过添加view的layout监听等来获取页面绘制完毕的节点。
  2. View.post() 通过给view添加一个runnable来获取view的宽高。
  3. 覆盖View的Layout方法或者measure方法等。
  4. Activity.onWindowFocusChanged方法内获取view的宽高。

其实上述四个方法都是同一个原理,我们先看一下view绘制相关流程的一张图。

当进入一个Activity时,Activity会在attach方法中向WMS(WindowManagerService)获取WindowManagersetContentView之后只是把View放置到DecorView上,之后调用WindowsetContentView,而并没有马上进行任何绘制操作。

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        attachBaseContext(context);
        mWindow = new PhoneWindow(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
       //省略部分代码
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        //mWindowManager初始化
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;
    }
复制代码

DecorView的绘制相关则是依赖于ViewRootImp进行和Window进行上述相关的绘制流程的。补充一句,基本所有View绘制相关的都是和ViewRootImp相关。之后通过上图的流转方式,最后进入到ViewRootImpperformTraversals环节中。

@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    final View.AttachInfo mAttachInfo;
    
      public ViewRootImpl(Context context, Display display) {
        ...
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mWindow = new W(this);
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                context);
        mChoreographer = Choreographer.getInstance();
        mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);

        loadSystemProperties();
    }

 static class W extends IWindow.Stub {
    
    }
}
复制代码

View.post其实就是调用View内部的Handler去执行一个Runnable。而View内部的Handler也是由ViewRootImp中的View.AttachInfo传递下去的,所以当我们调用View.post的时候其实View的第一次渲染已经完成了,毕竟传递下去的时候View已经完成了第一次的绘制了。

 AttachInfo(IWindowSession session, IWindow window, Display display,
                ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
                Context context) {
            mSession = session;
            mWindow = window;
            mWindowToken = window.asBinder();
            mDisplay = display;
            mViewRootImpl = viewRootImpl;
            mHandler = handler;
            mRootCallbacks = effectPlayer;
            mTreeObserver = new ViewTreeObserver(context);
        }

ViewTreeObserver则是同理,因为其内部也是和AttachInfo相关的。可以看到AttachInfo的构造函数内就有一个ViewTreeObserver。而View的measure方法也是由ViewRootImp下发的,所以我们也可以理解为同源方法。最后我们的onWindowFocusChanged的方法其实也是和ViewTreeObserver内的onWindowFocusChanged基本都是一样的。

那么其实我们也可以理解当View能获取到宽高的时候就是我们第一帧绘制完的时间了,那么我们完全可以给在ActivityonWindowFocusChanged方法被触发的时候就是代表了页面渲染完成了。

为什么要统计整个渲染时长。

由于生命周期和绘制等都是执行在安卓主线程上的。如果我们在onCreate或者onResume中执行了一些耗时操作,就会导致页面的整体渲染时间就会被延迟。通俗一点说,就容易出现用户吐槽,卧槽为什么打开一个页面这么慢。

当然这个也就是为什么我们需要响应式(稍微广一点,所有的async都算响应式)的原因。多线程并行的情况下,才能保证主线程做最少的事情。举几个例子来说明下我之前碰到过的一些问题吧。

  1. 从SP读取一些缓存操作,当一个sp文件存储的内容逐渐变大的情况下,sp的读取操作其实会变得原来越慢。
  2. 从xml转化成一个view,这个时间相对来说是耗时的,这个也就是为什么谷歌要出compose和AsyncLayoutInflate这些工具的原因。
  3. 一些异常的代码,比如之前检测到在bindview的时候通过new一个webview去获取ua之类的。
  4. 数据埋点,万一我们的基础仓库封装的不够好,那么这就是一个主线程的卡顿操作了。
  5. 获取Abtest数据,正常也会是一个数据库io操作,但是由于Abtest的特殊性,所以需要同步写法。

如果我们将这样的一些耗时的逻辑写在了ActivityonCreate或者onResume中的话,就自然会导致后续的流程被延迟,然后也就会出现当你按下按钮,但是跳转的反馈会延迟很久才出现的问题。同时主线程耗时任务导致屏幕无法按照标准的刷新频率刷新,界面停止绘制和掉帧,在手机操作界面上表现出的操作卡和绘制顿。常见的出现在有滑动或者动画的页面。

那么如何去改进渲染过慢的问题呢?

其实我们完全可以基于BlockCanaryEx去定位卡顿问题。说起来这个库的原理还是很巧妙的。他利用Looper.loop()方法中,消息分发前后的Printer接口,获取单个message消费的耗时,当任务耗时超出预期阀值获取内存堆栈CPU等信息的方式,把主线程卡顿进行了量化提示,最后会把耗时方法像LeakCanary一样提示出来。

public void start() {
    if (!mMonitorStarted) {
        mMonitorStarted = true;
        Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
    }
}
复制代码

主线程卡顿的监控,大概就是上述的核心代码,看着自然是简单的,但是从0到1寻找这个方法作者也是个大佬了啊。当然monitor内肯定还有些复杂的逻辑,各位有时间可以去看下这个仓库的原理,这个仓库已经很久没有更新了,所以有兴趣的老哥可以自己做下升级。

如何智能的给页面添加呢?

其实我的个人看法,还是整个APM相关的都还是最好基于gradle transfrom去做是最简单的,我们可以在onCreate方法调用前插入一个时间戳的记录,然后调用Activity的onWindowFocusChanged方法,在方法尾部插入埋点的代码就可以了。

这部分因为我暂时也没有写完Demo,我可能后续会补充在我的自动化埋点的demo工程上,我就先写点简单的介绍好了。

    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
        ##根据当前类信息判断,这个class是不是activity,如果是则进行后续的修改操作
    }


        @Override
        void visitEnd() {
            super.visitEnd()
            if (isActivity(superName)) {
                ##调用onWindowFoucusChange方法,插入我们埋点代码
                ##为什么是这个方法,因为并不是所有的activity都会实现该方法的,没有实现该方法的我们需要主动访问一次。
            }
        }
        
            @Override
    MethodVisitor visitMethod(int access, String name,
                              String desc, String signature, String[] exceptions) {
        ## 判断方法是不是onWindowFocusChanged 如果是则修改代码
        return super. visitMethod(access, name, desc, signature, exceptions)
    }

        
复制代码

通过这样一个ClassVisitor,我们就可以将所有Activity插上我们所需要的页面渲染市场的时间计算了,而且相对于别的来说也不需要让业务开发接入,降低了接入成本以及有效性。同样的我们也可以在Application内部也插入对应的耗时计算的代码,这样我们就可以监控整个启动流程的耗时了,但是这就是另外一个分支了,本文不展开。

当然也还可以通过ActivityLifeCycle来实现,同时Fragment也可以去做同样的耗时计算操作,而且Fragment内部也是有LifeCycle的,但是我个人的想法是把一些数据采集相关的全部都不让业务开发所感知到,所以我只是这么设想的。

总结

其实本文看似一个小小的渲染时长的点,但是要吃透就要把安卓内的一大部分源码相关的吃透。就比如View.post方法,其实就涉及到ViewRootImp绘制相关。而Activity的生命周期内又涉及到Handler下发,而当你要去监控主线程卡顿,你又可能要知道Handler的setMessageLogging,有时候说的面试造火箭,问源代码可能也是有他们必须的原因的。

最后

如果你有兴趣来B站的话,那么请直接联系我呀,还能帮你略微辅导辅导。为爱发电吧, bilibili干杯=( ゜- ゜)つロ。加我扣扣。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • APP-hybrid页面性能测试的一些知识记录

    做hybrid页面,需要测试其性能。我们不能认为用浏览器打开该网页得到的数据就算它线上的性能,因为 webview 的环境,其性能和浏览器还是有所差距的。最近一...

    腾讯IVWEB团队
  • 关于移动互联网的跨平台技术演进

    随着移动互联网的普及和快速发展,手机成了互联网行业最大的流量分发入口。以及随着5G的快速发展,未来越来越多的“端”也会如雨后春笋般快速兴起。而“快”作为互联网的...

    Android技术干货分享
  • 跨平台技术演进

    随着移动互联网的普及和快速发展,手机成了互联网行业最大的流量分发入口。以及随着5G的快速发展,未来越来越多的“端”也会如雨后春笋般快速兴起。而“快”作为互联网的...

    用户2356368
  • Android性能测试——发现和定位内存泄露和卡顿

      因此,对开发的Android应用,必须对其进行性能测试,不然将会直接影响用户体验。

    小老鼠
  • 自绘引擎时代,为什么Flutter能突出重围?

    如上图所示,与2019年1月相比,全球使用互联网的人数已增加到45.4亿,增长了7%(2.98亿新用户)。

    腾小云
  • 10分钟了解Flutter跨平台运行原理!

    导语 | 本文将从选型、简介和运行原理三大部分为你介绍Flutter的相关概念,希望能站在框架设计和实现原理的高度,带领大家去理解Flutter区别其他跨平台解...

    腾小云
  • 再谈移动端跨平台框架 Flutter 与 React Native

    这几年在大前端的开发领域,选择跨端方案的公司和部门越来越多,一方面是跨平台的前端框架越来越成熟,另一方面也是因原生开发者正逐年减少。所以,在当下掌握一门跨平台的...

    做个快乐的码农
  • 测试开发进阶(四十八)

    https://github.com/google/battery-historian

    zx钟
  • 移动端跨平台开发的深度解析

     跨平台一直是老生常谈的话题,cordova、ionic、react-native、weex、kotlin-native、flutter等跨平台框架的百花齐放,...

    GSYTech
  • 懒加载 React 长页面 - 动态渲染组件

    长页面在前端开发中是非常常见的。例如下图中的电商首页,楼层数据来自运营人员在后台的配置,楼层数量是不固定的,同时每个楼层可能会依赖更多翻页数据。在这种情况下,如...

    用户3806669
  • Flutter区别于其他技术的关键是什么?

    上一篇文章中我们了解到,跨端方案经历了三个阶段,第一阶段是混合开发的Web容器时代,第二阶段是以RN和Weex为代表的泛Web容器时代,第三阶段就是以Flutt...

    拉维
  • 当 Flutter 遇见 Web,会有怎样的秘密?

    作者:haigecao,腾讯 CSIG Web 开发工程师 在线教育团队(简称:OED)已经将 Flutter 这样技术在业务中落地了,做为 IMWeb 前端团...

    腾讯技术工程官方号
  • 手机管家(Android)UI过度渲染自动化测试方案

    通常我们可以从各种渠道听到用户反馈app卡顿,究竟是什么用户觉得卡顿呢?

    腾讯移动品质中心TMQ
  • Flutter上的数据监控深入理解

    最近看公司Flutter项目的时候,发现想要分析数据非常的困难,不是数据缺失就是数据异常,作为一个成熟的企业来说这是非常危险的,缺少了数据就像船只在海上航行的时...

    砸漏
  • 移动端跨平台开发的深度解析

    跨平台一直是老生常谈的话题,cordova、ionic、react-native、weex、kotlin-native、flutter等跨平台框架的百花齐放,...

    GSYTech
  • 开发者选项详解

    然后不可免得去想,这个东西是什么,有什么用.这篇文章就是来解决这些个问题得.

    云深无际
  • 【基本功】Litho的使用及原理剖析

    美美导读:【基本功】专栏又上新了,本期介绍一套高效构建Android UI的声明式框架——Litho。作者将带领大家深入剖析它的原理和用法。

    美团技术团队
  • Android硬件加速原理与实现简介

    在手机客户端尤其是Android应用的开发过程中,我们经常会接触到“硬件加速”这个词。由于操作系统对底层软硬件封装非常完善,上层软件开发者往往对硬件加速的底层原...

    美团技术团队
  • 【Unity 实用工具】✨| Unity 十款 浏览器相关插件 整理(web view / browser)

    有的是内嵌形式的,就是在Unity中显示浏览器的相关内容,有的则是会调用电脑本身的浏览器

    呆呆敲代码的小Y

扫码关注云+社区

领取腾讯云代金券