🎉 🎂 🍰 TesterHome 创立 6 周年纪念日 🍰 🎂 🎉

移动性能测试 基于 LifecycleCallbacks 的 Activity/Fragment 页面加载的耗时统计 (附实现)

alexknight · 2018年08月12日 · 最后由 alexknight 回复于 2018年08月18日 · 1525 次阅读
本帖已被设为精华帖!

前言

启动时间/页面加载(Activity/Fragment)时间统计的话,如果需要精确统计,一般都是在业务代码上插桩,或者从用户体检角度看的话,则是通过录制视频再做图像对比。这样灵活性都比较差,而且每个业务模块都需要自己去插桩,增加了复杂度。在这里,提供一种在Android生命周期里提供的注入点的一种方案-即基于实现ActivityLifecycleCallbacksFragmentLifecycleCallbacks的回调接口,从而达到统计启动时间跟页面加载时间的方法。如果不需要测的太细,只需要监听Activity的生命周期即可,因为Fragment需要绑定在Activity的生命周期内。

由于本人对Android内部运行机制了解尚浅,关于统计的起始结束点,如果有争议,欢迎指出,如果合理,我会做出对应修正。

博客原文地址http://www.coderlife.site/android/2018/08/12/android-starttime.html

一.需要提前了解的知识点

1.Activity/Fragment生命周期

2.LifecycleCallbacks接口说明

Application通过ActivityLifecycleCallbacks使用接口提供了一套回调方法,用于让开发者对Activity的生命周期事件进行集中处理。 ActivityLifecycleCallbacks接口回调可以简化监测Activity的生命周期事件,在一个类中作统一处理。 ActivityLifecycleCallbacks使用要求API 14+Android 4.0+)。

(1)Application.ActivityLifecycleCallbacks接口定义如下

public interface ActivityLifecycleCallbacks {
void onActivityCreated(Activity activity, Bundle savedInstanceState);
void onActivityStarted(Activity activity);
void onActivityResumed(Activity activity);
void onActivityPaused(Activity activity);
void onActivityStopped(Activity activity);
void onActivitySaveInstanceState(Activity activity, Bundle outState);
void onActivityDestroyed(Activity activity);
}

(2)FragmentManager.FragmentLifecycleCallbacks抽象类定义如下

public abstract static class FragmentLifecycleCallbacks {
public void onFragmentPreAttached(FragmentManager fm, Fragment f, Context context) {}
public void onFragmentAttached(FragmentManager fm, Fragment f, Context context) {}
public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {}
public void onFragmentActivityCreated(FragmentManager fm, Fragment f,
Bundle savedInstanceState) {}
public void onFragmentViewCreated(FragmentManager fm, Fragment f, View v,
Bundle savedInstanceState) {}
public void onFragmentStarted(FragmentManager fm, Fragment f) {}
public void onFragmentResumed(FragmentManager fm, Fragment f) {}
public void onFragmentPaused(FragmentManager fm, Fragment f) {}
public void onFragmentStopped(FragmentManager fm, Fragment f) {}
public void onFragmentSaveInstanceState(FragmentManager fm, Fragment f, Bundle outState) {}
public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {}
public void onFragmentDestroyed(FragmentManager fm, Fragment f) {}
public void onFragmentDetached(FragmentManager fm, Fragment f) {}
}

二.Activity加载时间计算

1.LauchActivity启动时间统计

(1)LauchActivity首次启动的起始点
首次lauchActivity启动点放置在SDK初始化的流程中,在这个阶段,将ActivityLifecycleCallbacks注册进去

private void init(){
long time = System.currentTimeMillis();
env = new Env.Builder().setAppStartTime(time)
.setBootActivity(AppUtils.getLauncherActivity(this.mContext))
.build();
...
// Activity生命周期监听注册
if (mContext instanceof Application) {
((Application) mContext).registerActivityLifecycleCallbacks(this);
}
fragmentLifeCallbacks = new FragmentLifeCallbacks(fragmentInfos);
Stats.IS_ROOT = RootUtil.isRooted();
}

(2)LauchActivity首次启动的结束点
onActivityStarted的回调方法中实现当前view的回调方法onWindowFocusChanged,通过获取view焦点的时间,作为结束点

@Override
public void onActivityStarted(Activity activity) {
...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
view.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus) {
if (activityName.equals(env.getLaunchActivity()) && pageInfo.isFirstStart()){
bootCost = System.currentTimeMillis()-env.getAppStartTime();
Log.d(MonitorType.LOG_TYPE_PAGE_LOAD_TIME,
activityName+"的lanchActivity启动时间: " + bootCost);
}
...
}
...
}
...
}
}
}

2.普通Activity的首次启动时间统计

(1)普通Activity启动起始点
目前时间统计放在onActivityCreated中,其实应该更靠前一点,但没有找到比较合适的hook

public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
...
PageInfo pageInfo = pageMap.get(activityName);
if (pageInfo == null){
pageInfo = new PageInfo();
pageInfo.setFirstStart(true);
pageInfo.setFirstCreateTime(System.currentTimeMillis());
pageInfo.setActivityName(activityName);
pageMap.put(activityName,pageInfo);
} else {
...
}
if (pageInfo.getFragmentInfos().isEmpty()){
pageInfo.setFragmentInfos(fragmentInfos);
}
}

(2)普通Activity启动结束点

@Override
public void onActivityStarted(Activity activity) {
...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
view.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus) {
...
if (pageInfo != null){
if (pageInfo.isFirstStart()){
if (!pageInfo.getActivityName().equals(env.getLaunchActivity())){
// 如果当前activityName不等于launchActivity时
pageInfo.setFirstLoadTime(System.currentTimeMillis()-pageInfo.getFirstCreateTime());
Log.d(MonitorType.LOG_TYPE_PAGE_LOAD_TIME,
activityName+"的首次加载时间: " + pageInfo.getFirstLoadTime());
}else{
...
}
pageInfo.setStartCount(1);
pageInfo.setFirstStart(false);
pageInfo.setBootIndex(++bootIndex);
} else {
...
}
}
}
}
});
}
}

3.Activity的非首次启动时间统计

(1)Activity非首次启动的起始点
Activity非首次启动时,会先执行onActivityResumed,我们只要将ActivityName对应的PageInfo对象做判断,如果不是首次启动,则可以将此处可以作为我们的起始时间点计算

@Override
public void onActivityResumed(Activity activity) {
...
PageInfo resumePageInfo = pageMap.get(activity.getClass().getName());
if (resumePageInfo != null && !resumePageInfo.isFirstStart()){
resumePageInfo.setCreateTime(System.currentTimeMillis());
}
}

(2)Activity非首次启动的结束点
结束点位置差不多,同样是在onActivityStartedview的焦点获取到的回调方法中统计

@Override
public void onActivityStarted(Activity activity) {
...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
view.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus) {
...
if (pageInfo != null){
if (pageInfo.isFirstStart()){
...
} else {
// 非启动的activity结束点
pageInfo.setLoadTime(System.currentTimeMillis()-pageInfo.getCreateTime());
Log.d(MonitorType.LOG_TYPE_PAGE_LOAD_TIME,
activityName+"的第"+ pageInfo.getStartCount() +"次加载时间: " + pageInfo.getLoadTime());
pageInfo.setStartCount(pageInfo.getStartCount()+1);
pageInfo.setBootIndex(++bootIndex);
}
}
}
}
}

三.Fragment加载时间计算

由于Fragment生命周期是绑定在Activity中的,因此Fragment加载时间统计其实算是页面加载的细化处理。

1.Fragment页面首次加载时间统计

(1)起始点
起始点放在onFragmentPreAttached中,所有Fragment首次启动都会进行Activity绑定

public void onFragmentPreAttached(FragmentManager fm, android.support.v4.app.Fragment f, Context context) {
// Fragment首次启动
curFragmentName = f.getClass().getName();
// Activity的Fragment首次启动时还没有来得及setFragment属性
if (!mFragmentInfos.containsKey(curFragmentName)){
FragmentInfo aFragmentInfo = new FragmentInfo();
aFragmentInfo.setFirstCreateTime(System.currentTimeMillis());
aFragmentInfo.setIsFirstBoot(true);
aFragmentInfo.setFragmentName(curFragmentName);
mFragmentInfos.put(curFragmentName, aFragmentInfo);
}

(2)结束点
Fragment中,也可以用获取焦点的方式判断Fragment是否加载完成

public void onFragmentViewCreated(FragmentManager fm, android.support.v4.app.Fragment f, View v,
final Bundle savedInstanceState) {
...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
view.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus) {
if (Stats.topActivityName != null){
...
if (mFragmentInfos.get(curFragmentName).getIsFirstBoot()){
mFragmentInfos.get(curFragmentName).setFirstLoadTime(
System.currentTimeMillis() - mFragmentInfos.get(curFragmentName).getFirstCreateTime());
Log.d(MonitorType.LOG_TYPE_PAGE_LOAD_TIME,
"fragmnet=>" + curFragmentName + " 首次启动时间: "
+ mFragmentInfos.get(curFragmentName).getFirstLoadTime());
...
}
}
...
}
}
});
}
}

2.Fragment页面非首次加载时间统计

(1)起始点
首次启动会先执行onFragmentPreAttached,而保存状态后的Fragment不会,因此起始时间点为onFragmentAttached

public void onFragmentAttached(FragmentManager fm, android.support.v4.app.Fragment f, Context context) {
// 非首次启动起始时间点(onFragmentPreAttached[首次]->onFragmentAttached)
curFragmentName = f.getClass().getName();
if(!mFragmentInfos.get(curFragmentName).getIsFirstBoot()){
mFragmentInfos.get(curFragmentName).setCreateTime(System.currentTimeMillis());
}
}

(2)结束点
经过debug,并没有重新绘制的流程。因此非首次启动的结束点在onFragmentStarted,而不是onFragmentViewCreated

public void onFragmentStarted(FragmentManager fm, android.support.v4.app.Fragment f) {
// 已保存状态的首次启动结束点
curFragmentName = f.getClass().getName();
if (!mFragmentInfos.get(curFragmentName).getIsFirstBoot()){
mFragmentInfos.get(curFragmentName).setLoadTime(System.currentTimeMillis() - mFragmentInfos.get(curFragmentName).getCreateTime());
Log.d(MonitorType.LOG_TYPE_PAGE_LOAD_TIME,
"fragmnet=>" + curFragmentName + "非首次加载时间为: " + mFragmentInfos.get(curFragmentName).getFirstLoadTime());
}
}
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 5 条回复 时间 点赞
simple 将本帖设为了精华贴 08月12日 22:12

欢迎大家参与讨论!

请问楼主是自己做了SDK,然后嵌入app中监控页面生命周期的么?

aitutu 回复

是啊,还在写。。很多功能还没写完。

hi,楼主好,有个问题请教下哈,假如有一个 A activity, 一个 B activiy, B activity继承自A, 重载了其中部分生命周期方法,做了一些耗时操作,这种情况下,通过这个组件获取到的B activity的启动耗时精准吗?

chengying2009 回复

准的,因为是实现了的Application.ActivityLifecycleCallbacks的回调,只要你在onActivityCreated阶段做一次B Activity的启动时间统计,然后在start阶段拿到view的focus作为结束点相减就行了,文章的非launch activity启动统计有说明。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册