Android性能优化之启动速度

2,901 阅读11分钟

优化思路

优化标准

优化的过程标准要从用户体验出发,从点击图标到用户真正可以操作的整个过程。

业务梳理

首先需要梳理清楚当前启动过程正在运行的每一个模块,哪些是一定需要的、哪些可以砍掉、哪些可以懒加载。保证启动期间加载的每个功能和业务都是必须的,这对中低端机上的表现会有很大的改进。

业务优化

业务梳理完成后,剩下的都是启动过程一定要用的模块。这个时候就只能硬着头皮去做进一步的优化。优化前期需要“抓大放小”,先看看主线程究竟耗时在哪里。

比如考虑这些耗时任务是不是可以通过异步线程预加载实现,但需要注意的是过多的线程预加载会让我们的逻辑变得更复杂,建议衡量修改后的维护成本再决定是否使用这种方法。另外,懒加载要防止集中化,否则容易出现首页显示出来但用户无法操作的情形。

应用启动过程分析

启动的三种状态

应用启动的三种状态:冷启动、温启动或热启动

在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。

最好在冷启动的基础上进行优化,这样同时也可以提升温启动和热启动的性能。

冷启动

冷启动开始,系统首先要做三个任务:

冷启动系统要做的任务.png

系统创建应用进程后,应用进程就负责后续阶段:

冷启动应用进程负责的任务.png

整个冷启动流程图如下:

App冷启动过程.png

热启动

应用的热启动比冷启动简单得多,开销也更低。

在热启动中,系统的所有工作就是将您的Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局填充和呈现。

温启动

温启动包含了在冷启动期间发生的部分操作,它的开销要比热启动高。以下情况可视为温启动:

  • 用户在退出应用后又重新启动应用。进程可能已继续运行,但应用必须通过调用 onCreate() 从头开始重新创建Activity。应用只会重走Activity的生命周期,而不会重走进程的创建,Application的创建与生命周期等。
  • 系统将您的应用从内存中逐出,然后用户又重新启动它。进程和Activity需要重启,但传递到 onCreate()state bundle实例已保存。

启动过程的常见问题

1.点击应用图标很久没有响应

系统在拉起应用进程之前,会先根据应用的 Theme 属性创建预览窗口,这会耗费一定的时间,尤其在低中端机比较明显。

解决方法:可以禁用预览窗口或者将预览窗口指定为透明,但用户在这段时间看到的还会是桌面,给用户的感觉会是怎么没反应,是没点中图标吗?体验不是很好。

2.应用首页显示太慢了

随着应用的业务越来越复杂,用到各种框架和闪屏广告,这些都要在应用启动阶段的时候去做,这就会出现首页需要很长的时间才会显示出来。

3.首页显示出来了也没法操作

对于问题2,容易想到的一个方法是,尽量把启动阶段的初始化任务异步化执行。

要注意的是,那些需要准备好才能正常使用的资源如果异步处理的话,很可能会造成首页出现白屏,或首页出现后却没法操作的问题。

工具善其事,必先利其器

日志和ADB

1.查看日志

在 Android 4.4(API 级别 19)及更高版本中,logcat包含一个输出行,其中包含名为 Displayed 的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。

因为提供此日志的是系统服务器,不是应用本身,所以记得查看是关闭过滤器

可以看到类似这样的日志:

I/ActivityManager: Displayed com.xxx.xxx/.activity.xxxActivity: +1s539ms

2.adb命令行

adb shell am start -S -W  com.xxx.xxx/com.xxx.xxx.activity.xxxActivity

-S:在启动 Activity 前,强行停止目标应用

-W:等待启动完成

启动成功后,可以看到启动时间数据:一般只要关心TotalTime即可,这个时间才是自己应用真正启动的耗时。

Status: ok
Activity: com.xxx.xxx/com.xxx.xxx.activity.xxxActivity
ThisTime: 1539  
TotalTime: 1539 
WaitTime: 1621  
Complete

ThisTime:表示一连串启动Activity的最后一个Activity`的启动耗时

TotalTime:表示新应用启动的耗时,包括新进程的启动和Activity的启动,但不包括前一个应用Activity pause的耗时

WaitTime:总的耗时,包括前一个应用Activity pause的时间和新应用启动的时间

如果对这条命令是如何得出的三个时间感兴趣的,可以看知乎Groffa的回答:

怎么计算apk的启动时间?

一款合适的启动优化分析工具

常用的启动分析工具有Traceviewsystrace

Traceview性能损耗太大,得出的结果并不真实。

systrace可以很方便地追踪关键系统调用的耗时情况,但是不支持应用程序代码的耗时分析。

TraceView试图收集某个阶段所有函数的运行信息,它希望在你并不知道哪个函数有问题的时候直接定位到关键函数;但可惜的是,收集所有信息这个是不现实的,它的运行时开销严重干扰了运行环境。

Systrace的思路是反过来的,它会统计出一些基本的信息,让开发者通过假设-分析-验证 的过程一步一步找出问题的原因。

选择systrace,再配合上函数插桩,是一个优秀的方式。

systrace + 函数插桩 。通过对需要统计耗时的函数进行插桩后,就可以借助systrace生成HTML报告进行分析。

函数插桩很简单,借助系统自带的Trace类即可,具体使用下面就详细介绍~

优化过程分析

Systrace

systraceAndroid4.1 中新增的性能数据采样和分析工具。它可帮助开发者收集 Android 关键子系统(如 SurfaceFlinger/SystemServer/Kernel/Input/Display 等 Framework 部分关键模块、服务,View系统等)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。

systrace脚本文件位置:xxx/Android/sdk/platform-tools/systrace

执行以下命令就可以生成HTML报告:

python xxx/Android/sdk/platform-tools/systrace/systrace.py -o mynewtrace.html sched ss dalvik am

通过命令参数,可以查看常用的数据,分析启动过程,dalvikschedssam类型是我们比较关心的:

  • sched:CPU Scheduling, CPU调度的信息,可看出CPU在每个时间段在运行什么线程,线程调度情况,比如锁信息。
  • ss:System Server
  • dalvik: Dalvik VM,虚拟机相关信息,比如GC停顿
  • am: Activity Manager,分析Activity的启动过程很有用

以上的类型可能在某些机型不支持,adb连接到测试机,通过以下命令可查看systrace支持的类型,

python systrace.py --list-categories

systrace命令行使用

Perfetto分析报告

直接在浏览器打开上述的HTML文件,可以查看报告

mac电脑上,Chrome直接打开上述生成的HTML是空白的。

解决办法:在chrome地址栏中输入chrome:tracing,然后点击load按钮选择你的trace.html文件。

不过更推荐使用Perfetto工具:

Perfetto 是 Android 10 中引入的全新平台级跟踪工具。这是适用于 Android、Linux 和 Chrome 的更加通用和复杂的开源跟踪项目。与 Systrace 不同,它提供数据源超集,可让您以 protobuf 编码的二进制流形式记录任意长度的跟踪记录。

我们的设备可能是Android10以下,不能使用Perfetto提供的新追踪方式。但它也支持打开通过systrace生成的trace文件,它的UI交互使用起来更舒服。

Trace跟踪

通过Trace类,可跟踪方法到调用流程,配合systrace视图分析,非常好用,比如跟踪下onResume方法

protected void onResume() {
  super.onResume();
  Trace.beginSection("START_TEST");
  // 你的操作
  ...
  Trace.endSection();
  Log.i(TAG, "[onResume]");
}

注意: 如果要想再systrace查看到跟踪阶段,需要在之前的命令行加上 -a 【包名】

python xxx/Android/sdk/platform-tools/systrace/systrace.py -o mynewtrace.html sched ss dalvik am -a 【包名】

Perfetto打开生成mynewtrace.html文件,找到【包名】的进程,可以看到我们添加的Trace跟踪出现了(蓝颜色条目):

trace跟踪.png

小栗子

运行一个demo程序,先退出应用。执行命令:

python xxx/Android/sdk/platform-tools/systrace/systrace.py -o mynewtrace.html sched ss dalvik am -a 【包名】

启动demo程序,页面成功打开后,结束命令,会生成一份HTML报告,使用Perfetto打开该文件,找到该应用进程,里面有应用启动的多个阶段:

ActivityStart耗时.png

选择其中一个阶段,可以看到很多时间统计,有两个比较重要的时间指标:

  • Wall Duration : 代码持续耗时时间,即这段代码的耗时时间
  • CPU Duration : 这段代码在CPU上真正的耗时

如果发现Wall DurationCPU Duration的时间差很大,就说明这部分代码有明显的耗时,可以考虑优化了。

比如上图的ActivityStart过程的耗时情况:

Wall Duration78.282 ms
CPU Duration71.004 ms

可以看到代码耗时和CPU实际耗时差距不大

接下来再分析bindApplication过程:

bindApplication耗时.png

Wall Duration3,053.505 ms
CPU Duration49.767 ms

发现Wall Duration用了3s+,CPU实际耗时才49ms,这就不对头了,CPU这个阶段并没有一直在做事。

此时就要从代码上分析bindApplication过程,应用层就可以分析Application的代码,我的代码是这样的:

public class SampleApplication extends Application {
    private static Context sContext;
  
    @Override
    public void onCreate() {
        super.onCreate();
        sContext = this;
        try {
          // 模拟耗时操作
          Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  
    public static Context getContext(){
        return sContext;
    }
}

显而易见,原因是我在ApplicationonCreate做了3s睡眠。当然这里只是简单模拟了耗时操作,实际业务肯定比这复杂的,但整体的分析思路是一样的。

统计方法耗时方案

如何查看每一个方法执行所耗时呢?

通过AOP统计方法耗时并打印

如果只是想更方便的统计方法耗时,可以通过AOP方式插入统计耗时的代码,在每个方法上添加一行注解就可以获取到该方法的耗时日志打印。

具体实现方式可以参考笔者的这篇博客写给Android工程师的AOP知识

自定义插件

如果想通过systrace工具查看每个方法的耗时,就需要在每个方法的前后加Trace检测代码

如果通过手动添加,这肯定是个体力活,不是一个对摸鱼有追求的程序猿做的事。这个时候就要想办法自动化,我们可以通过自定义插件的方法,去插桩我们的Trace代码。

好在这个插件已经有大佬写了,我们可以参考学习下:

插件使用

该插件的使用方式在插件readme写的很详细了,这里就不copy了。

插件解析

插件的源码比较多,笔者抽出最核心的结构代码剖析下:

1.自定义Extension:该扩展类支持在build.gradle下使用的属性

class SystraceExtension {
    boolean enable
    String baseMethodMapFile
    String blackListFile
    String output
​
    SystraceExtension() {
        enable = true
        baseMethodMapFile = ""
        blackListFile = ""
        output = ""
    }
}

2.应用自定义的Plugin:

@Override
void apply(Project project) {
    project.extensions.create("systrace", SystraceExtension)
    if (!project.plugins.hasPlugin('com.android.application')) {
        throw new GradleException('Systrace Plugin, Android Application plugin required')
    }
    project.afterEvaluate {
      def android = project.extensions.android
      def configuration = project.systrace
      android.applicationVariants.all { variant ->
        String output = configuration.output
         if (Util.isNullOrNil(output)) {
            configuration.output = project.getBuildDir().getAbsolutePath() + File.separator + "systrace_output"
            Log.i(TAG, "set Systrace output file to " + configuration.output)
        }
        Log.i(TAG, "Trace enable is %s", configuration.enable)
        // 判断是否配置了开启
        if (configuration.enable) {
           SystemTraceTransform.inject(project, variant)
         }
        }
    }
}

3.自定义Transform:对需要插桩的方法,插桩Trace方法

public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
  transformInvocation.inputs.each { TransformInput input ->
     input.directoryInputs.each { DirectoryInput dirInput ->
         collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental)
     }
     input.jarInputs.each { JarInput jarInput ->
         if (jarInput.getStatus() != Status.REMOVED) {
             collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental)
         }
     }
  }
  MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector)
  // 收集源代码和jar文件中的所有方法
  HashMap<String, TraceMethod> collectedMethodMap = methodCollector.collect(
    scrInputMap.keySet().toList(), jarInputMap.keySet().toList())
  MethodTracer methodTracer = new MethodTracer(traceConfig, collectedMethodMap, methodCollector.getCollectedClassExtendMap())
  // 对所有的方法插桩代码
  methodTracer.trace(scrInputMap, jarInputMap)
  origTransform.transform(transformInvocation)
}

4.遍历所有的方法插桩trace代码

# MethodTracer
private void innerTraceMethodFromSrc(File input, File output) {
  for (File classFile : classFileList) {
    if (mTraceConfig.isNeedTraceClass(classFile.getName())) {
      is = new FileInputStream(classFile);
      ClassReader classReader = new ClassReader(is);
      ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
      ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
      classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
      is.close();
      if (output.isDirectory()) {
          os = new FileOutputStream(changedFileOutput);
      } else {
          os = new FileOutputStream(output);
      }
      os.write(classWriter.toByteArray());
      os.close();
    }
  }
}
​
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
                                 String signature, String[] exceptions) {
    if (isABSClass) {
        return super.visitMethod(access, name, desc, signature, exceptions);
    } else {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
        return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className,
                isMethodBeatClass);
    }
}

5.利用ASM工具修改字节码

public final static String MATRIX_TRACE_METHOD_BEAT_CLASS = "com/sample/systrace/TraceTag";
# MethodTracer
private class TraceMethodAdapter extends AdviceAdapter {
  // 进入方法
  protected void onMethodEnter() {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    if (traceMethod != null) {
        traceMethodCount.incrementAndGet();
        String sectionName = methodName;
        int length = sectionName.length();
        if (length > TraceBuildConstants.MAX_SECTION_NAME_LEN) {
            // 先去掉参数
            int parmIndex = sectionName.indexOf('(');
            sectionName = sectionName.substring(0, parmIndex);
            // 如果依然更大,直接裁剪
            length = sectionName.length();
            if (length > TraceBuildConstants.MAX_SECTION_NAME_LEN) {
                sectionName = sectionName.substring(length - TraceBuildConstants.MAX_SECTION_NAME_LEN);
            }
        }
      // visitLdcInsn:加载到堆栈上的常量
      mv.visitLdcInsn(sectionName);
      // visitMethodInsn:调用方法的指令,这里调用TraceTag的i方法
      mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_METHOD_BEAT_CLASS, "i", "(Ljava/lang/String;)V", false);
    }
  }
  
  // 退出方法
  protected void onMethodExit(int opcode) {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    if (traceMethod != null) {
      traceMethodCount.incrementAndGet();
      // visitMethodInsn:调用方法的指令,这里调用TraceTag的o方法
      mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_METHOD_BEAT_CLASS, "o", "()V", false);
    }
  }
}