Android启动速度优化一启动速度测量

随着手机硬件的发展,手机硬件配置越来越高,计算速度,硬件性能越来越好,导致在开发过程中很容易让开发者不太去关注启动速度和性能问题。但是在发布到市场上后就会有用户反馈说启动速度慢,体验不好的问题。实际上性能问题、启动速度问题在高端机上依然存在,例如在手机内存吃紧的时候,再去启动一个APP的话还是会遇到这类问题,在低端机上就更不用说了。
这里是自己在开发过程中的一些经验积累,记录下来方便自己日常查阅,本篇是启动速度优化第一篇,主要记录APP启动速度的测量方法。

一、adb命令查看启动时间

Android本身提供有可以查看APP启动速度的命令,日常开发和调试中可以使用它们快速的查看信息,方便日常的开发。但是有一个缺点是adb命令查看的时间无法反应出启动过程中每个环节的时间消耗

1.1 adb shell am start -W

我们可以使用这个命令来启动目标Activity,启动后终端会显示对应的启动信息

adb shell am start -W packageName/ComponentName
demo
adb shell am start -W com.snail.memo/com.snail.memo.NoteStartActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.snail.memo/.NoteStartActivity }
Status: ok
Activity: com.snail.memo/.NoteStartActivity
ThisTime: 332
TotalTime: 332
WaitTime: 359
Complete

可以看到,在终端输入的这段信息,各信息详细意思

Status

启动状态,有两种取值,ok和timeout,正常启动为ok,启动超时或者异常为timeout

Activty

启动的目标Activity名

ThisTime

表示一连串启动 Activity 的最后一个 Activity 的启动耗时。 注:Android Q之后不再有该属性

TotalTime

所有Activity启动耗时,包括进程创建,Application初始化,Activity实例化并初始化,到界面显示整个过程的时间

WaitTime

执行本条命令开始到Activity启动完成的总时间,包括整个启动过程的所有时间(AMS内部的逻辑时间和Activity真正启动的时间),统计的方法实际上是在执行命令前后分别记录了startTime和endTime,执行完成后取两者的差值

adb shell am这条命令执行后在java层的调用代码如下:
代码路径:

frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java

执行代码:

@Override
    public int onCommand(String cmd) {
        if (cmd == null) {
            return handleDefaultCommands(cmd);
        }
        final PrintWriter pw = getOutPrintWriter();
        try {
            switch (cmd) {
                case "start":
                case "start-activity":
                    return runStartActivity(pw);
........//其他命令代码
          }
      }
}

runStartActivity方法的开头会调用makeIntent方法,改方法会解析我们传递进来的option -W,匹配上-W后,把mWaitOption设置为true。然后在后面真正启动Activity的时候利用mWaitOption来处理不同的启动方式。
makeIntent方法中的关键代码:

  return Intent.parseCommandArgs(this, new Intent.CommandOptionHandler() {
            @Override
            public boolean handleOption(String opt, ShellCommand cmd) {
                if (opt.equals("-D")) {
                    mStartFlags |= ActivityManager.START_FLAG_DEBUG;
                } else if (opt.equals("-N")) {
                    mStartFlags |= ActivityManager.START_FLAG_NATIVE_DEBUGGING;
                } else if (opt.equals("-W")) {
                    mWaitOption = true;

执行完以上逻辑后就会进入runStartActivity真正启动Activity的过程
runStartActivity启动Activity代码片段,利用mWaitOption判断是调用AMS的哪个方法启动Activity,-W被指定时mWaitOption为true,因此就会调用startActivityAndWait方法进行启动Activity的操作

if (mWaitOption) {
                result = mInternal.startActivityAndWait(null, null, intent, mimeType,
                        null, null, 0, mStartFlags, profilerInfo,
                        options != null ? options.toBundle() : null, mUserId);
                res = result.result;
            } else {
                res = mInternal.startActivityAsUser(null, null, intent, mimeType,
                        null, null, 0, mStartFlags, profilerInfo,
                        options != null ? options.toBundle() : null, mUserId);
            }

后面的事情就是启动Activity的流程,不在这里进行深入。
看到这里发现一个问题,只是单独指定-W的时候,如果要测试APP的冷启动时间,每次都要手动把进程停止掉,很麻烦。如果系统可以有更加便捷的方法提供给我们就完美了。果不其然,真的有提供这个操作,那就是-S 操作

adb shell am start -S -W packageName/ComponentName

如此一来没执行一次命令之前系统会先forceStop掉目标进程,就可以很方便的调试冷启动时间了
原因在于makeIntent方法中解析option的时候,如果有设置了-S,会把mStopOption设置为true。runStartActivity方法执行启动Activity之前会判断这个属性是否为true,为true的话就先调用forceStop方法停止掉目标进程

if (mStopOption) {
                String packageName;
                if (intent.getComponent() != null) {
                    packageName = intent.getComponent().getPackageName();
                } else {
                    // queryIntentActivities does not convert user id, so we convert it here first
                    int userIdForQuery = mInternal.mUserController.handleIncomingUser(
                            Binder.getCallingPid(), Binder.getCallingUid(), mUserId, false,
                            ALLOW_NON_FULL, "ActivityManagerShellCommand", null);
                    List<ResolveInfo> activities = mPm.queryIntentActivities(intent, mimeType, 0,
                            userIdForQuery).getList();
                    if (activities == null || activities.size() <= 0) {
                        getErrPrintWriter().println("Error: Intent does not match any activities: "
                                + intent);
                        return 1;
                    } else if (activities.size() > 1) {
                        getErrPrintWriter().println(
                                "Error: Intent matches multiple activities; can't stop: "
                                + intent);
                        return 1;
                    }
                    packageName = activities.get(0).activityInfo.packageName;
                }
                pw.println("Stopping: " + packageName);
                pw.flush();
                mInterface.forceStopPackage(packageName, mUserId);
                try {
                    Thread.sleep(250);
                } catch (InterruptedException e) {
                }
            }

1.2 adb logcat

Activity启动的时候AMS会打印出对应的log信息,我们可以过滤出需要的信息来知道启动的时间

adb logcat -s ActivityManager:I | grep Displayed

或者在AndroidStudio中自己过滤Displayed信息出来

ActivityManager: Displayed com.snail.memo/.NoteStartActivity: +119ms

二、代码打点统计启动耗时

打点统计启动时间的方式比较简单,主要还是要先只掉Activity和APP的启动流程,然后在对应的方法中加入打点代码片段即可。
我们知道冷启动过程中APP端最先被调用到的方法是Application的attachBaseContext方法,然后才会到onCreate,后面才会到Activity相关的周期函数。因此在统计的时候我们可以从这里入手。但是代码打点需要注意的是,启动时间优化我们是要关注的不知冷冰冰的统计数据,而是用户从点击桌面图标到真正看到APP界面的时间。因此打点的结束时间应该是视图界面绘制完成的时间。而不是Activity onCreate方法结束的时间或者onResume被调用到的时间

2.1 直接插入代码统计

伪代码片段

public class TimeTrace{
    public static long sStart;
    public static long sEnd;

   public static void startTracing(){
      sStart = System.currentTimeMillis() 
  }
  public static void endTracing(){
    Log.d("StartTime","TotalTime:"+(System.currentTimeMillis() - sStart));
   }

}

在Application的attachBaseContext记录开始时间

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
                TimeTrace.startTracing();
    }

在MainActivity的onCreate方法中找到一个对用于视觉感官比较强的View,在这个View进行绘制的时候进结束时间的打点和输出

findViewById(R.id.logo).getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
            @Override
            public void onDraw() {
                if( !hasCounted ){
                    hasCounted = true;
                    TimeTrace.startTracing();
                }

            }
        });

2.2 AOP

2.1节中的方式并没有什么问题,只是需要修改原来的业务逻辑代码,不利于维护。网上有很多文章有介绍到使用AOP的形式进行统计。自己demo之后发现确实可行。这里不再重复。
提供一个快速集成AOP的插件,可以直接使用,非常方便:
https://github.com/alexluotututu/aop_plugin

三、Systrace

实际上可以对Android 性能进行检测的有很多工具,例如Profiler,TraceView等,但是这些工具因为统计的信息比较多,个人觉得无法真正对优化的方向进行指导,比如TraceView本身在统计信息的时候自己就会滑掉一部分时间。

因此我会直接使用Systrace来查看启动时间
Systrace记录APP启动时间

如上图所示,冷启动时间我们从bindApplicaiton开始,到第二帧绘制结束即可。
启动分析过程中需要对每个方法进行耗时统计的话可以自己在目标方法中加入trace统计方法即可,例如我要统计initData这个方法的耗时,可以这样做

    private void initData(){
        TraceCompat.beginSection("InitData");
        //do something
        TraceCompat.endSection();
    }

使用以下命令生成trace文件,利用Chrome浏览器打开,就可以查看详细信息了

python systrace.py -t 10 sched gfx view wm am app webview -a "com.snail.memo" -o ~/Documents/systrace_file/start_time.html 

-t: 表示统计的时长
-a: 指定目标包名
-o: 指定输出文件

四、高速相机

高速相机主要是模拟人眼的场景,记录从用户点击桌面图标到看到真正画面的时间。测试工程师一般会用这个时间类评估APP冷启动时间。但是因为个人比较穷,没有用过,在此不做过多叙述

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354