写点什么

Android 自动化页面测速在美团的实践

  • 2020 年 2 月 20 日
  • 本文字数:11166 字

    阅读完需:约 37 分钟

Android自动化页面测速在美团的实践

背景

随着移动互联网的快速发展,移动应用越来越注重用户体验。美团技术团队在开发过程中也非常注重提升移动应用的整体质量,其中很重要的一项内容就是页面的加载速度。如果发生冷启动时间过长、页面渲染时间过长、网络请求过慢等现象,就会直接影响到用户的体验,所以,如何监控整个项目的加载速度就成为我们部门面临的重要挑战。


对于测速这个问题,很多同学首先会想到在页面中的不同节点加入计算时间的代码,以此算出某段时间长度。然而,随着美团业务的快速迭代,会有越来越多的新页面、越来越多的业务逻辑、越来越多的代码改动,这些不确定性会使我们测速部分的代码耦合进业务逻辑,并且需要手动维护,进而增加了成本和风险。于是通过借鉴公司先前的方案 Hertz( 移动端性能监控方案Hertz ),分析其存在的问题并结合自身特性,我们实现了一套无需业务代码侵入的自动化页面测速插件,本文将对其原理做一些解读和分析。


现有解决方案 Hertz( 移动端性能监控方案 Hertz](https://tech.meituan.com/hertz.html) ) * 手动在 Application.onCreate() 中进行SDK 的初始化调用,同时计算冷启动时间。



  • 手动在 Activity 生命周期方法中添加代码,计算页面不同阶段的时间。

  • 手动为 Activity.setContentView() 设置的 View 上,添加一层自定义父 View,用于计算绘制完成的时间。

  • 手动在每个网络请求开始前和结束后添加代码,计算网络请求的时间。



  • 本地声明 JSON 配置文件来确定需要测速的页面以及该页面需要统计的初始网络请求 API,getClass().getSimpleName() 作为页面的 key,来标识哪些页面需要测速,指定一组 API 来标识哪些请求是需要被测速的。



现有方案问题


  • 冷启动时间不准:冷启动起始时间从 Application.onCreate() 中开始算起,会使得计算出来的冷启动时间偏小,因为在该方法执行前可能会有 MultiDex.install() 等耗时方法的执行。

  • 特殊情况未考虑:忽略了ViewPager+Fragment 延时加载这些常见而复杂的情况,这些情况会造成实际测速时间非常不准。

  • 手动注入代码:所有的代码都需要手动写入,耦合进业务逻辑中,难以维护并且随着新页面的加入容易遗漏。

  • 写死配置文件:如需添加或更改要测速的页面,则需要修改本地配置文件,进行发版。


目标方案效果


  • 自动注入代码,无需手动写入代码与业务逻辑耦合。

  • 支持 Activity 和 Fragment 页面测速,并解决 ViewPager+Fragment 延迟加载时测速不准的问题。

  • 在 Application 的构造函数中开始冷启动时间计算。

  • 自动拉取和更新配置文件,可以实时的进行配置文件的更新。


实现

我们要实现一个自动化的测速插件,需要分为五步进行:


  1. 测速定义:确定需要测量的速度指标并定义其计算方式。

  2. 配置文件:通过配置文件确定代码中需要测量速度指标的位置。

  3. 测速实现:如何实现时间的计算和上报。

  4. 自动化实现:如何自动化实现页面测速,不需要手动注入代码。

  5. 疑难杂症:分析并解决特殊情况。


测速定义

我们把页面加载流程抽象成一个通用的过程模型:页面初始化 -> 初次渲染完成 -> 网络请求发起 -> 请求完成并刷新页面 -> 二次渲染完成。据此,要测量的内容包括以下方面:


  • 项目的冷启动时间:从 App 被创建,一直到我们首页初次绘制出来所经历的时间。

  • 页面的初次渲染时间:从 Activity 或 Fragment 的 onCreate() 方法开始,一直到页面 View 的初次渲染完成所经历的时间。

  • 页面的初始网络请求时间:Activity 或 Fragment 指定的一组初始请求,全部完成所用的时间。

  • 页面的二次渲染时间:Activity 或 Fragment 所有的初始请求完成后,到页面 View 再次渲染完成所经历的时间。


需要注意的是,网络请求时间是指定的一组请求全部完成的时间,即从第一个请求发起开始,直到最后一个请求完成所用的时间。根据定义我们的测速模型如下图所示:



配置文件

接下来要知道哪些页面需要测速,以及页面的初始请求是哪些 API,这需要一个配置文件来定义。


<page id="HomeActivity" tag="1">   <api id="/api/config"/>   <api id="/api/list"/></page><page id="com.test.MerchantFragment" tag="0">   <api id="/api/test1"/></page>
复制代码


我们定义了一个 XML 配置文件,每个 <page/> 标签代表了一个页面,其中 id 是页面的类名或者全路径类名,用以表示哪些 Activity 或者 Fragment 需要测速; tag 代表是否为首页,这个首页指的是用以计算冷启动结束时间的页面,比如我们想把冷启动时间定义为从 App 创建到 HomeActivity 展示所需要的时间,那么 HomeActivity 的 tag 就为 1;每一个 <api/> 代表这个页面的一个初始请求,比如 HomeActivity 页面是个列表页,一进来会先请求 config 接口,然后请求 list 接口,当 list 接口回来后展示列表数据,那么该页面的初始请求就是 config 和 list 接口。更重要的一点是,我们将该配置文件维护在服务端,可以实时更新,而客户端要做的只是在插件 SDK 初始化时拉取最新的配置文件即可。


测速实现

测速需要实现一个 SDK,用于管理配置文件、页面测速对象、计算时间、上报数据等,项目接入后,在页面的不同节点调用 SDK 提供的方法完成测速。


冷启动开始时间

冷启动的开始时间,我们以 Application 的构造函数被调用为准,在构造函数中进行时间点记录,并在 SDK 初始化时,将时间点传入作为冷启动开始时间。


//Applicationpublic MyApplication(){    super();    coldStartTime = SystemClock.elapsedRealtime();}//SDK初始化public void onColdStart(long coldStartTime) {    this.startTime = coldStartTime;}
复制代码


这里说明几点:


  • SDK 中所有的时间获取都使用 SystemClock.elapsedRealtime() 机器时间,保证了时间的一致性和准确性。

  • 冷启动初始时间以构造函数为准,可以算入 MultiDex 注入的时间,比在 onCreate() 中计算更为准确。

  • 在构造函数中直接调用 Java 的 API 来计算时间,之后传入 SDK 中,而不是直接调用 SDK 的方法,是为了防止 MultiDex 注入之前,调用到未注入的 Dex 中的类。


SDK 初始化

SDK 的初始化在 Application.onCreate() 中调用,初始化时会获取服务端的配置文件,解析为 Map<String,PageObject> ,对应配置中页面的 id 和其配置项。另外还维护了一个当前页面对象的 MAP<Integer, Object> ,key 为一个 int 值而不是其类名,因为同一个类可能有多个实例同时在运行,如果存为一个 key,可能会导致同一页面不同实例的测速对象只有一个,所以在这里我们使用 Activity 或 Fragment 的 hashcode() 值作为页面的唯一标识。


页面开始时间

页面的开始时间,我们以 Activtiy 或 Fragment 的 onCreate() 作为时间节点进行计算,记录页面的开始时间。


public void onPageCreate(Object page) {    int pageObjKey = Utils.getPageObjKey(page);    PageObject pageObject = activePages.get(pageObjKey);    ConfigModel configModel = getConfigModel(page);//获取该页面的配置    if (pageObject == null && configModel != null) {//有配置则需要测速        pageObject = new PageObject(pageObjKey, configModel, Utils.getDefaultReportKey(page), callback);        pageObject.onCreate();        activePages.put(pageObjKey, pageObject);    }}
//PageObject.onCreate()void onCreate() { if (createTime > 0) { return; } createTime = Utils.getRealTime();}
复制代码


这里的 getConfigModel() 方法中,会使用页面的类名或者全路径类名,去初始化时解析的配置 Map 中进行id 的匹配,如果匹配到说明页面需要测速,就会创建测速对象 PageObject 进行测速。


网络请求时间

一个页面的初始请求由配置文件指定,我们只需在第一个请求发起前记录请求开始时间,在最后一个请求回来后记录结束时间即可。


boolean onApiLoadStart(String url) {    String relUrl = Utils.getRelativeUrl(url);    if (!hasApiConfig() || !hasUrl(relUrl) || apiStatusMap.get(relUrl.hashCode()) != NONE) {        return false;    }    //改变Url的状态为执行中    apiStatusMap.put(relUrl.hashCode(), LOADING);    //第一个请求开始时记录起始点    if (apiLoadStartTime <= 0) {        apiLoadStartTime = Utils.getRealTime();    }    return true;}boolean onApiLoadEnd(String url) {    String relUrl = Utils.getRelativeUrl(url);    if (!hasApiConfig() || !hasUrl(relUrl) || apiStatusMap.get(relUrl.hashCode()) != LOADING) {        return false;    }    //改变Url的状态为执行结束    apiStatusMap.put(relUrl.hashCode(), LOADED);    //全部请求结束后记录时间    if (apiLoadEndTime <= 0 && allApiLoaded()) {        apiLoadEndTime = Utils.getRealTime();    }    return true;}private boolean allApiLoaded() {    if (!hasApiConfig()) return true;    int size = apiStatusMap.size();    for (int i = 0; i < size; ++i) {        if (apiStatusMap.valueAt(i) != LOADED) {           return false;        }    }    return true;}
复制代码


每个页面的测速对象,维护了一个请求 url 和其状态的映射关系 SparseIntArray ,key 就为请求 url 的 hashcode,状态初始为 NONE 。每次请求发起时,将对应 url 的状态置为 LOADING ,结束时置为 LOADED 。当第一个请求发起时记录起始时间,当所有 url 状态为 LOADED 时说明所有请求完成,记录结束时间。


渲染时间

按照我们对测速的定义,现在冷启动开始时间有了,还差结束时间,即指定的首页初次渲染结束时的时间;页面的开始时间有了,还差页面初次渲染的结束时间;网络请求的结束时间有了,还差页面的二次渲染的结束时间。这一切都是和页面的 View 渲染时间有关,那么怎么获取页面的渲染结束时间点呢?



由 View 的绘制流程可知,父 View 的 dispatchDraw() 方法会执行其所有子 View 的绘制过程,那么把页面的根 View 当做子 View,是不是可以在其外部增加一层父 View,以其 dispatchDraw() 作为页面绘制完毕的时间点呢?答案是可以的。


class AutoSpeedFrameLayout extends FrameLayout {    public static View wrap(int pageObjectKey, @NonNull View child) {        ...        //将页面根View作为子View,其他参数保持不变        ViewGroup vg = new AutoSpeedFrameLayout(child.getContext(), pageObjectKey);        if (child.getLayoutParams() != null) {            vg.setLayoutParams(child.getLayoutParams());        }        vg.addView(child, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));        return vg;    }    private final int pageObjectKey;//关联的页面key    private AutoSpeedFrameLayout(@NonNull Context context, int pageObjectKey) {        super(context);        this.pageObjectKey = pageObjectKey;    }    @Override    protected void dispatchDraw(Canvas canvas) {        super.dispatchDraw(canvas);        AutoSpeed.getInstance().onPageDrawEnd(pageObjectKey);    }}
复制代码


我们自定义了一层 FrameLayout 作为所有页面根 View 的父 View,其 dispatchDraw() 方法执行super 后,记录相关页面绘制结束的时间点。


测速完成

现在所有时间点都有了,那么什么时候算作测速过程结束呢?我们来看看每次渲染结束后的处理就知道了。


//PageObject.onPageDrawEnd()void onPageDrawEnd() {    if (initialDrawEndTime <= 0) {//初次渲染还没有完成        initialDrawEndTime = Utils.getRealTime();        if (!hasApiConfig() || allApiLoaded()) {//如果没有请求配置或者请求已完成,则没有二次渲染时间,即初次渲染时间即为页面整体时间,且可以上报结束页面了            finalDrawEndTime = -1;            reportIfNeed();        }        //页面初次展示,回调,用于统计冷启动结束        callback.onPageShow(this);        return;    }    //如果二次渲染没有完成,且所有请求已经完成,则记录二次渲染时间并结束测速,上报数据    if (finalDrawEndTime <= 0 && (!hasApiConfig() || allApiLoaded())) {        finalDrawEndTime = Utils.getRealTime();        reportIfNeed();    }}
复制代码



该方法用于处理渲染完毕的各种情况,包括初次渲染时间、二次渲染时间、冷启动时间以及相应的上报。这里的冷启动在 callback.onPageShow(this) 是如何处理的呢?


//初次渲染完成时的回调void onMiddlePageShow(boolean isMainPage) {    if (!isFinish && isMainPage && startTime > 0 && endTime <= 0) {        endTime = Utils.getRealTime();        callback.onColdStartReport(this);        finish();    }}
复制代码


还记得配置文件中 tag 么,他的作用就是指明该页面是否为首页,也就是代码段里的 isMainPage 参数。如果是首页的话,说明首页的初次渲染结束,就可以计算冷启动结束的时间并进行上报了。


上报数据

当测速完成后,页面测速对象 PageObject 里已经记录了页面(包括冷启动)各个时间点,剩下的只需要进行测速阶段的计算并进行网络上报即可。


//计算网络请求时间long getApiLoadTime() {    if (!hasApiConfig() || apiLoadEndTime <= 0 || apiLoadStartTime <= 0) {        return -1;    }    return apiLoadEndTime - apiLoadStartTime;}
复制代码


自动化实现

有了SDK,就要在我们的项目中接入,并在相应的位置调用 SDK 的 API 来实现测速功能,那么如何自动化实现 API 的调用呢?答案就是采用 AOP 的方式,在 App 编译时动态注入代码,我们实现一个 Gradle 插件,利用其 Transform 功能以及 Javassist 实现代码的动态注入。动态注入代码分为以下几步:


  • 初始化埋点:SDK 的初始化。

  • 冷启动埋点:Application 的冷启动开始时间点。

  • 页面埋点:Activity 和 Fragment 页面的时间点。

  • 请求埋点:网络请求的时间点。


初始化埋点

在 Transform 中遍历所有生成的 class 文件,找到 Application 对应的子类,在其 onCreate() 方法中调用 SDK 初始化 API 即可。


CtMethod method = it.getDeclaredMethod("onCreate")method.insertBefore("${Constants.AUTO_SPEED_CLASSNAME}.getInstance().init(this);")
复制代码


最终生成的 Application 代码如下:


public void onCreate() {    ...    AutoSpeed.getInstance().init(this);}
复制代码


冷启动埋点

同上一步,找到 Application 对应的子类,在其构造方法中记录冷启动开始时间,在 SDK 初始化时候传入 SDK,原因在上文已经解释过。


//Applicationprivate long coldStartTime;public MobileCRMApplication() {    coldStartTime = SystemClock.elapsedRealtime();}public void onCreate(){    ...    AutoSpeed.getInstance().init(this,coldStartTime);}
复制代码


页面埋点

结合测速时间点的定义以及 Activity 和 Fragment 的生命周期,我们能够确定在何处调用相应的 API。



Activity


对于 Activity 页面,现在开发者已经很少直接使用 android.app.Activity 了,取而代之的是 android.support.v4.app.FragmentActivity 和 android.support.v7.app.AppCompatActivity ,所以我们只需在这两个基类中进行埋点即可,我们先来看 FragmentActivity。


protected void onCreate(@Nullable Bundle savedInstanceState) {    AutoSpeed.getInstance().onPageCreate(this);    ...}public void setContentView(View var1) {    super.setContentView(AutoSpeed.getInstance().createPageView(this, var1));}
复制代码


注入代码后,在 FragmentActivity 的 onCreate 一开始调用了 onPageCreate() 方法进行了页面开始时间点的计算;在 setContentView() 内部,直接调用 super,并将页面根 View 包装在我们自定义的 AutoSpeedFrameLayout 中传入,用于渲染时间点的计算。然而在 AppCompatActivity 中,重写了setContentView()方法,且没有调用 super,调用的是 AppCompatDelegate 的相应方法。


public void setContentView(View view) {    getDelegate().setContentView(view);}
复制代码


这个 delegate 类用于适配不同版本的 Activity 的一些行为,对于 setContentView,无非就是将根 View 传入 delegate 相应的方法,所以我们可以直接包装 View,调用 delegate 相应方法并传入即可。


public void setContentView(View view) {    AppCompatDelegate var2 = this.getDelegate();    var2.setContentView(AutoSpeed.getInstance().createPageView(this, view));}
复制代码


对于 Activity 的 setContentView 埋点需要注意的是,该方法是重载方法,我们需要对每个重载的方法做处理。


Fragment


Fragment 的 onCreate() 埋点和 Activity 一样,不必多说。这里主要说下 onCreateView() ,这个方法是返回值代表根 View,而不是直接传入 View,而 Javassist 无法单独修改方法的返回值,所以无法像 Activity 的 setContentView 那样注入代码,并且这个方法不是 @CallSuper 的,意味着不能在基类里实现。那么怎么办呢?我们决定在每个 Fragment 的该方法上做一些事情。


//Fragment标志位protected static boolean AUTO_SPEED_FRAGMENT_CREATE_VIEW_FLAG = true;//利用递归包装根Viewpublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {    if(AUTO_SPEED_FRAGMENT_CREATE_VIEW_FLAG) {        AUTO_SPEED_FRAGMENT_CREATE_VIEW_FLAG = false;        View var4 = AutoSpeed.getInstance().createPageView(this, this.onCreateView(inflater, container, savedInstanceState));        AUTO_SPEED_FRAGMENT_CREATE_VIEW_FLAG = true;        return var4;    } else {        ...        return rootView;    }}
复制代码


我们利用一个 boolean 类型的标志位,进行递归调用 onCreateView() 方法:


  1. 最初调用时,会将标志位置为 false,然后递归调用该方法。

  2. 递归调用时,由于标志位为 false 所以会调用原有逻辑,即获取根 View。

  3. 获取根 View 后,包装为 AutoSpeedFrameLayout 返回。


并且由于标志位为 false,所以在递归调用时,即使调用了 super.onCreateView() 方法,在父类的该方法中也不会走 if 分支,而是直接返回其根 View。


请求埋点

关于请求埋点我们针对不同的网络框架进行不同的处理,插件中只需要配置使用了哪些网络框架即可实现埋点,我们拿现在用的最多的 Retrofit 框架来说。


开始时间点


在创建 Retrofit 对象时,需要 OkHttpClient 对象,可以为其添加 Interceptor 进行请求发起前 Request 的拦截,我们可以构建一个用于记录请求开始时间点的 Interceptor,在 OkHttpClient.Builder() 调用时,插入该对象。


public Builder() {  this.addInterceptor(new AutoSpeedRetrofitInterceptor());    ...}
复制代码


而该 Interceptor 对象就是用于在请求发起前,进行请求开始时间点的记录。


public class AutoSpeedRetrofitInterceptor implements Interceptor {    public Response intercept(Chain var1) throws IOException {        AutoSpeed.getInstance().onApiLoadStart(var1.request().url());        return var1.proceed(var1.request());    }}
复制代码


结束时间点


使用 Retrofit 发起请求时,我们会调用其 enqueue() 方法进行异步请求,同时传入一个 Callback 进行回调,我们可以自定义一个 Callback,用于记录请求回来后的时间点,然后在 enqueue 方法中将参数换为自定义的 Callback,而原 Callback 作为其代理对象即可。


public void enqueue(Callback<T> callback) {    final Callback<T> callback = new AutoSpeedRetrofitCallback(callback);    ...}
复制代码


该 Callback 对象用于在请求成功或失败回调时,记录请求结束时间点,并调用代理对象的相应方法处理原有逻辑。


public class AutoSpeedRetrofitCallback implements Callback {    private final Callback delegate;    public AutoSpeedRetrofitMtCallback(Callback var1) {        this.delegate = var1;    }    public void onResponse(Call var1, Response var2) {        AutoSpeed.getInstance().onApiLoadEnd(var1.request().url());        this.delegate.onResponse(var1, var2);    }    public void onFailure(Call var1, Throwable var2) {        AutoSpeed.getInstance().onApiLoadEnd(var1.request().url());        this.delegate.onFailure(var1, var2);    }}
复制代码


使用 Retrofit+RXJava 时,发起请求时内部是调用的 execute() 方法进行同步请求,我们只需要在其执行前后插入计算时间的代码即可,此处不再赘述。


疑难杂症

至此,我们基本的测速框架已经完成,不过经过我们的实践发现,有一种情况下测速数据会非常不准,那就是开头提过的 ViewPager+Fragment 并且实现延迟加载的情况。这也是一种很常见的情况,通常是为了节省开销,在切换 ViewPager 的 Tab 时,才首次调用 Fragment 的初始加载方法进行数据请求。经过调试分析,我们找到了问题的原因。


等待切换时间



该图红色时间段反映出,直到 ViewPager 切换到 Fragment 前,Fragment不会发起请求,这段等待的时间就会延长整个页面的加载时间,但其实这块时间不应该算在内,因为这段时间是用户无感知的,不能作为页面耗时过长的依据。


那么如何解决呢?我们都知道 ViewPager 的 Tab 切换是可以通过一个 OnPageChangeListener 对象进行监听的,所以我们可以为 ViewPager 添加一个自定义的 Listener 对象,在切换时记录一个时间,这样可以通过用这个时间减去页面创建后的时间得出这个多余的等待时间,上报时在总时间中减去即可。


public ViewPager(Context context) {    ...    this.addOnPageChangeListener(new AutoSpeedLazyLoadListener(this.mItems));}
复制代码


mItems 是 ViewPager 中当前页面对象的数组,在 Listener 中可以通过他找到对应的页面,进行切换时的埋点。


//AutoSpeedLazyLoadListenerpublic void onPageSelected(int var1) {    if(this.items != null) {        int var2 = this.items.size();        for(int var3 = 0; var3 < var2; ++var3) {            Object var4 = this.items.get(var3);            if(var4 instanceof ItemInfo) {                ItemInfo var5 = (ItemInfo)var4;                if(var5.position == var1 && var5.object instanceof Fragment) {                    AutoSpeed.getInstance().onPageSelect(var5.object);                    break;                }            }        }    }}
复制代码


AutoSpeed 的 onPageSelected() 方法记录页面的切换时间。这样一来,在计算页面加载速度总时间时,就要减去这一段时间。


long getTotalTime() {    if (createTime <= 0) {        return -1;    }    if (finalDrawEndTime > 0) {//有二次渲染时间        long totalTime = finalDrawEndTime - createTime;        //如果有等待时间,则减掉这段多余的时间        if (selectedTime > 0 && selectedTime > viewCreatedTime && selectedTime < finalDrawEndTime) {            totalTime -= (selectedTime - viewCreatedTime);        }        return totalTime;    } else {//以初次渲染时间为整体时间        return getInitialDrawTime();    }}
复制代码


这里减去的 viewCreatedTime 不是 Fragment 的 onCreate() 时间,而应该是 onViewCreated() 时间,因为从 onCreate 到 onViewCreated 之间的时间也是应该算在页面加载时间内,不应该减去,所以为了处理这种情况,我们还需要对 Fragment 的 onViewCreated 方法进行埋点,埋点方式同 onCreate()的埋点。


渲染时机不固定


此外经实践发现,由于不同 View 在绘制子 View 时的绘制原理不一样,有可能会导致以下情况的发生:


  • 没有切换至 Fragment 时,Fragment 的 View 初次渲染已经完成,即 View不可见的情况下也调用了dispatchDraw() 。

  • 没有切换至 Fragment 时,Fragment 的 View 初次渲染未完成,即直到 View 初次可见时 dispatchDraw() 才会调用。

  • 没有延迟加载时,当 ViewPager 没有切换到 Fragment,而是直接发送请求后,请求回来时更新 View,会调用 dispatchDraw() 进行二次渲染。

  • 没有延迟加载时,当 ViewPager 没有切换到 Fragment,而是直接发送请求后,请求回来时更新 View,不会调用 dispatchDraw() ,即直到切换到 Fragment 时才会进行二次渲染。


上面的问题总结来看,就是初次渲染时间和二次渲染时间中,可能会有个等待切换的时间,导致这两个时间变长,而这个切换时间点并不是 onPageSelected() 方法调用的时候,因为该方法是在 Fragment 完全滑动出来之后才会调用,而这个问题里的切换时间点,应该是指 View 初次展示的时候,也就是刚一滑动,ViewPager露出目标 View 的时间点。于是类比延迟加载的切换时间,我们利用 Listener 的 onPageScrolled() 方法,在 ViewPager 滑动时,找到目标页面,为其记录一个滑动时间点 scrollToTime 。


public void onPageScrolled(int var1, float var2, int var3) {    if(this.items != null) {        int var4 = Math.round(var2);        int var5 = var2 != (float)0 && var4 != 1?(var4 == 0?var1 + 1:-1):var1;        int var6 = this.items.size();        for(int var7 = 0; var7 < var6; ++var7) {            Object var8 = this.items.get(var7);            if(var8 instanceof ItemInfo) {                ItemInfo var9 = (ItemInfo)var8;                if(var9.position == var5 && var9.object instanceof Fragment) {                    AutoSpeed.getInstance().onPageScroll(var9.object);                    break;                }            }        }    }}
复制代码


那么这样就可以解决两次渲染的误差:


  • 初次渲染时间中, scrollToTime viewCreatedTime 就是页面创建后,到初次渲染结束之间,因为等待滚动而产生的多余时间。

  • 二次渲染时间中, scrollToTime apiLoadEndTime 就是请求完成后,到二次渲染结束之间,因为等待滚动而产生的多余时间。


于是在计算初次和二次渲染时间时,可以减去多余时间得到正确的值。


long getInitialDrawTime() {    if (createTime <= 0 || initialDrawEndTime <= 0) {        return -1;    }    if (scrollToTime > 0 && scrollToTime > viewCreatedTime && scrollToTime <= initialDrawEndTime) {//延迟初次渲染,需要减去等待的时间(viewCreated->changeToPage)        return initialDrawEndTime - createTime - (scrollToTime - viewCreatedTime);    } else {//正常初次渲染        return initialDrawEndTime - createTime;    }}long getFinalDrawTime() {    if (finalDrawEndTime <= 0 || apiLoadEndTime <= 0) {        return -1;    }    //延迟二次渲染,需要减去等待时间(apiLoadEnd->scrollToTime)    if (scrollToTime > 0 && scrollToTime > apiLoadEndTime && scrollToTime <= finalDrawEndTime) {        return finalDrawEndTime - apiLoadEndTime - (scrollToTime - apiLoadEndTime);    } else {//正常二次渲染        return finalDrawEndTime - apiLoadEndTime;    }}
复制代码


总结

以上就是我们对页面测速及自动化实现上做的一些尝试,目前已经在项目中使用,并在监控平台上可以获取实时的数据。我们可以通过分析数据来了解页面的性能进而做优化,不断提升项目的整体质量。并且通过实践发现了一些测速误差的问题,也都逐一解决,使得测速数据更加可靠。自动化的实现也让我们在后续开发中的维护变得更容易,不用维护页面测速相关的逻辑,就可以做到实时监测所有页面的加载速度。


参考文献


作者介绍

  • 文杰,美团前端 Android 开发工程师,2016年毕业于天津工业大学,同年加入美团点评到店餐饮事业群,从事商家销售端移动应用开发工作。


2020 年 2 月 20 日 21:51328

评论

发布
暂无评论
发现更多内容

RocketMQ 5(1),kafka面试题零拷贝

Java 程序员 后端

Spring Boot 精讲,看完你还敢说你不会 Spring Boot ?

Java 程序员 后端

Spring Boot 项目的这些文件都是干啥用的?,java电子书免费

Java 程序员 后端

Oracle 常用SQL语句大全(精),java框架学习顺序

Java 程序员 后端

P8大牛带你细谈架构中的限流与计数器的实现方式

Java 程序员 后端

Redis入门HelloWorld,java入门视频教程

Java 程序员 后端

Redis精通系列——info 指令用法,阿里+头条+抖音+百度+蚂蚁+京东面经

Java 程序员 后端

RocketMQ 5,linux端口转发技术

Java 程序员 后端

Spring Boot 快速入门(二),华为工程师面试经历

Java 程序员 后端

Spring boot记录sql探索,java堆和栈面试题

Java 程序员 后端

Spring Cloud原理详解,java程序员进阶

Java 程序员 后端

OpenKruise :SidecarSet 助力 Mesh 容器热升级

Java 程序员 后端

Redis事务详述,java多并发面试题

Java 程序员 后端

redis实现分布式限流 结合Lua脚本,Java开发还不会这些

Java 程序员 后端

SDS——Redis源码剖析,java工程师进阶书籍

Java 程序员 后端

Spring Cloud Gateway修改请求和响应body的内容

Java 程序员 后端

Redis 中 RDB 和 AOF 持久化有啥区别?看这儿,你就懂了

Java 程序员 后端

quartz-2,linux视频教程百度云

Java 程序员 后端

set集合框架,java消息中间件面试

Java 程序员 后端

Offer拿来吧你!秒杀系统?这不是必考的嘛,kafka与rabbitmq面试题

Java 程序员 后端

Redis 变慢了?那你这样试试,不行就捶我,java面试问职业规划

Java 程序员 后端

Redis从入门到精通,至少要看看这篇,java医疗管理系统技术描述

Java 程序员 后端

Spring Cloud Gateway自定义过滤器实战(观测断路器状态变化)

Java 程序员 后端

RabbitMQ实现即时通讯居然如此简单!后端代码都省得写了

Java 程序员 后端

Redis常用命令总结,kalilinux教程推荐

Java 程序员 后端

Redis与MySQL数据双写一致性工程落地案例,java最新技术百度云

Java 程序员 后端

Redis实战(五)-字符串,kafka基本原理

Java 程序员 后端

Socket和ServerSocket的简单介绍及例子,mybatis源码面试题

Java 程序员 后端

Spring Cloud入门-Zuul服务网关(Hoxton版本)

Java 程序员 后端

Spring MVC+Spring+Mybatis实现支付宝支付功能

Java 程序员 后端

Redis 最全性能监控指标:汇总实战,实战java虚拟机葛一鸣第二版pdf

Java 程序员 后端

Android自动化页面测速在美团的实践_文化 & 方法_美团技术团队_InfoQ精选文章