写点什么

Android 自动化页面测速在美团的实践,百度、阿里、滴滴、新浪的面试心经总结

用户头像
Android架构
关注
发布于: 刚刚

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


这里说明几点:


SystemClock.elapsedRealtime()onCreate()


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;//关联的页面 keyprivate AutoSpeedFrameLayout(@NonNull Context context, int pageObjectKey) {super(context);this.pageObjectKey = pageObjectKey;}@Overrideprotected void dispatchDraw(Canvas canvas) {super.dispatchDraw(canvas);AutoSpeed.getInstance().onPageDrawEnd(pageObjectKey);}}


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


测速完成


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


//PageObject.onPageDrawEnd()void onPageDrawEnd() {if (initialDrawEndTime <= 0) {//初次渲染还没有


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


完成 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() 方法:


<pre style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">


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);

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android自动化页面测速在美团的实践,百度、阿里、滴滴、新浪的面试心经总结