专栏首页逮虾户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 条评论
登录 后参与评论

相关文章

  • AndResGuard编译速度优化

    当前项目内用了腾讯的AndResGuard对资源文件的大小进行了一次深度优化。AndResGuard负责将文件名,arsc文件和R文件也进行了一次混淆,能把整体...

    逮虾户
  • Kotlin拓展函数的真身

    kotlin也写了很长一段时间了,香是真的很香这个东西。但是很多东西也是不求甚解,都是直接开始用,但是为什么我也不关心。举个栗子,就拿拓展函数来说。

    逮虾户
  • 我有个大胆的方案可以提高ARouter和WMRouter的编译速度

    如果使用wmrouter的各位,可以直接用我的插件替换工程内的路由初始化,应该能解决项目编译的问题。基本测试都通过了。

    逮虾户
  • Activity 启动过程的简单分析

    我们都知道,Activity 是有生命周期的,onCreate()、onStart() 、onResume 等等那么这些方法是如何调用的呢?它不会平白无故的自己...

    开发者
  • SpringBoot中实现拦截器, 并实现对404和500等错误的拦截。

    今天给大家介绍一下SpringBoot中拦截器的用法,相比Struts2中的拦截器,SpringBoot的拦截器就显得更加方便简单了。 只需要写几个实现类就可以...

    林老师带你学编程
  • 关于查询转换的一些简单分析(一) (r3笔记第37天)

    在sql解析器中,在生成执行计划的时候,会在多个执行计划中选择最优的计划,在这个过程中,查询转换就是一个很重要的过程。 虽然最终的执行结果没有变化,但是从优化器...

    jeanron100
  • iOS开发中解决报错之the file had a tree conflict

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

    用户1451823
  • java — 重载和覆盖

    Mister24
  • 手把手教你读懂源码,View的加载流程详细剖析

    最近想要理清我们的View是如何加载到界面中的,最好的方式就是分析源代码,这里一同分享给有需要的朋友们。内容较多,需要一定的耐心,请斟酌学习! ...

    分享达人秀
  • 【经典文章】运营优化的秘密武器:重新认识热图的力量!

    主编注:这篇文章获得业内很高的关注。是宋星老师的另一篇讲述如何优化网站页面尤其是着陆页的经典文章。 引言   之前发布的文章:《优化高跳出率着陆...

    iCDO互联网数据官

扫码关注云+社区

领取腾讯云代金券