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

李雷雷 · 2018年08月12日 · 最后由 YLM 回复于 2022年12月12日 · 4396 次阅读
本帖已被设为精华帖!

前言

启动时间/页面加载(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());
    }
}
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 6 条回复 时间 点赞
simple 将本帖设为了精华贴 08月12日 22:12

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

LoveFat 回复

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

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

__承_影__ 回复

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

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44
simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08

欢迎大家参与讨论!

hi 楼主,你好,请问你监听 fragment 是通过 registerFragmentLifecycleCallbacks 注册监听的吗?关于 fragment 监听的完整代码可以发出来看一下吗?

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