前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android 统计页面渲染时长

Android 统计页面渲染时长

作者头像
逮虾户
发布2020-10-15 10:24:54
4K0
发布2020-10-15 10:24:54
举报
文章被收录于专栏:逮虾户逮虾户

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

什么是页面渲染时长?

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

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

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

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

代码语言:javascript
复制
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,而并没有马上进行任何绘制操作。

代码语言:javascript
复制
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环节中。

代码语言:javascript
复制
@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已经完成了第一次的绘制了。

代码语言:javascript
复制
 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一样提示出来。

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

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

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

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

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

代码语言:javascript
复制
    @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干杯=( ゜- ゜)つロ。加我扣扣。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是页面渲染时长?
    • onCreate下获取View的宽高
    • 为什么要统计整个渲染时长。
    • 那么如何去改进渲染过慢的问题呢?
    • 如何智能的给页面添加呢?
    • 总结
    • 最后
    相关产品与服务
    文件存储
    文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档