问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Android应用启动优化完全指南:从流程分析到实战技巧

创作时间:
作者:
@小白创作中心

Android应用启动优化完全指南:从流程分析到实战技巧

引用
CSDN
1.
https://m.blog.csdn.net/it_android/article/details/142100475

本章主要围绕App的启动流程如何优化进行讲解。将启动优化,首先要了解的就是App的启动流程,只有清晰并完善的了解了启动流程才能更好的进行优化。

App 启动流程

在将AMS的时候,其实已经讲解了App的启动流程,感兴趣的可以翻看下之前的文章。这里我们贴一张启动流程图:

整体流程就是当我们点击桌面图标启动某一个应用层的时候,首先会在Launcher进程中通过ActivityManagerProxy跨进程通信发送startActivtity并代理到system_server(AMS)进程,AMS发现这个Activity所在的进程已经存在,则直接启动这个Activity(这就是所谓的热启动),如果不存在,则通知Zygote进程fork出一个进程给目标App使用,并通知AMS,由AMS来启动目标Activity;而启动目标Activity之前,先启动Application,在Application启动之后才会启动Activity。

整体可以分为三个大的阶段:

  1. 点击桌面Launcher的应用图标,通过与AMS通信,启动应用的过程;
  2. 应用Application执行过程;
  3. 启动MainActivity执行过程;

所以这里的启动其实是有三种状态的:冷启动、温启动、热启动

启动方式

冷启动

冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动

热启动

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

温启动

包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:

  • 用户在退出应用后又重新启动应用。进程可能未被销毁,继续运行,但应用需要执行onCreate()从头开始重新创建Activity;
  • 系统将应用从内存中释放,然后用户又重新启动它。进程和Activity需要重启,但传递到onCreate()的已保存的实例statebundle对于完成此任务有一定助益;

启动时长统计

Displayed

app启动完成之后,ActivityManager会打印一个Displayed展示启动时间;

adb 命令

adb shell am start -s -w 「packageName/ .activityName」

  • waitTime 总的耗时,包括前一个应用Activity pause的时间和新应用启动的时间
  • thisTime 表示一连串启动Activity的最后一个Activity的启动耗时
  • totalTime 表示新应用启动的耗时,包括新进程的启动和Activity的启动,但不包括前一个应用Activity pause的耗时;

插桩 + systrace

通过插桩,我们可以看到应用主线程和其他线程的函数调用流程;

CPU Profile

AS之后,我们一般都是通过CPUProfile对App的启动耗时进行采样优化;

当我们需要对一个app进行采样的时候,我们需要在EditConfiguration中进行相关配置信息的打开

选择MethodTrace之后,使用profile的方式启动App

开启trace之后,采集一段时间(跳转到第一个页面之后)可以点击stop停止采集;

之后生成对应的trace信息;

其中橙色表示系统方法执行时间,绿色表示app方法执行时间,蓝色表示三方sdk执行时间;

每一个方法,x轴越大,表示花费的时间越多,通过放大,可以看到我们每一个方法的执行时间,包括Application类加载等的创建时间

重点关注绿色区域,逐个的分支查看app中耗时的方法;

右侧区域分为四种分析方式:Summary、TopDown、FlameChat、BottomUp

Summary并不是很方便的查看方法的细节;

我们切换到FlameChat(火焰图)来看下:

这个就是和Summary反向的分析图,从下往上分析;

我们切换到TopDown(主要分析耗时)来看下:

TopDown比较直观的看到每个方法的执行耗时,以及内部方法的执行耗时;

App 启动优化方式

根据启动流程的三大阶段,对应的三个阶段的优化方式;

第一阶段的优化 主要是桌面Launcher应用与AMS的交互,以及AMS启动应用的过程,这个阶段主要是系统Framework层在做,优化空间基本没有;

第二阶段的优化,对于Application的优化,主要包括三部分的优化,一是attachBaseContext的优化,二是onCreate回调方法的优化,三是应用执行到MainActivity之前的白屏处理;

第三阶段的优化,主要是第一个Activity运行的阶段,直到Activity执行完成onResume函数,对应的就是onCreate、onStart、onResume的优化;

应用执行到MainActivity之前的白屏处理

Activity真正展示的是Window,对应的唯一实例就是PhoneWindow,也就是到PhoneWindow展示之后,用户才能看到实际的内容;

这个流程其实是:点击Launcher桌面图图标到第一个Activity的PhoneWindow展示之前,这个期间内应该展示什么来规避黑屏或者白屏的case;

产生这个黑白屏的case其实跟我们设置的启动的第一个Activity的主题有关系,也就是windowBackground依赖这个theme,如果theme设置的是白色主题,那么windowBackground默认就是白色,启动就会白屏;

根据流程图,我们进入PhoneWindowManager的addSplashScreen方法看下,黑屏开始的地方在哪里?

public StartingSurface addSplashScreen(IBinder appToken, int userId, String packageName,
        int theme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
        int icon, int logo, int windowFlags, Configuration overrideConfig, int displayId) {
      
    // 省略部分代码
    // 黑白屏开始的地方,就是addView添加要显示的View的时候;
    wm.addView(view, params);
    return view.getParent() != null ? new SplashScreenSurface(view, appToken) : null;
}

那么,怎么优化这个黑白屏的问题,我们可以通过PhoneWindowManager的源码来看下:

private void addSplashscreenContent(PhoneWindow win, Context ctx) {
    final TypedArray a = ctx.obtainStyledAttributes(R.styleable.Window);
    final int resId = a.getResourceId(R.styleable.Window_windowSplashscreenContent, 0);
    a.recycle();
    if (resId == 0) {
        return;
    }
    final Drawable drawable = ctx.getDrawable(resId);
    if (drawable == null) {
        return;
    }
    // We wrap this into a view so the system insets get applied to the drawable.
    final View v = new View(ctx);
    v.setBackground(drawable);
    win.setContentView(v);
}

可以看到,PhoneWindowManager通过获取theme中定义的windowSplashscreenContent来获取一个drawable设置给PhoneWindow;

PS:这个API需要API>=26,如果最低版本小于26则还是通过windowBackground来设置;

那么,可能会有人有疑问了,这个windowSplashscreenContent属性比windowBackground强大在了哪些地方呢?

windowBackground只能设置一张图片,而windowSplashscreenContent借助Jetpack的SplashScreen可以展示一个开屏动画;

attachBaseContext 优化

可以参考字节的MutilDex优化启动速度,核心是:去掉了dex转zip的操作,优化了启动速度,而不是多进程;

onCreate 优化

onCreate方法中,如果使用setContentView方式,目前只能通过减少xml层级的方式来降低启动耗时,因为setContentView中充斥着大量的反射逻辑来创建View;

所以,如果xml的绘制比较简单,建议使用newView的方式,通过addView来实现View的创建和绘制;

onResume 优化

如果页面布局是ViewPager+Fragment的方式,通常采用懒加载的方式,来进行页面渲染的优化;

布局层级的优化

使用约束布局替换普通的布局,优化渲染层级,减少绘制时长;

使用布局的异步加载AsynLayoutInflater

AsyncLayoutInflater(this).inflate(R.layout.activity_debug, null, object : OnInflateFinishedListener{
    override fun onInflateFinished(view: View, p1: Int, p2: ViewGroup?) {
        setContentView(view)
    }
})

但是AsyncLayoutInflater的使用也是有一些限制的,我们可以先尝试下能不能在实际的项目中使用它;

延迟任务优化

通过handler的addIdleHandler方法添加延迟任务,会在主线程空闲的时候执行;

线程优化

线程的优化主要在于减少CPU调度带来的波动,让应用的启动时间更加稳定;

  • 控制线程数量;线程数量太多会相互竞争CPU资源,因此要有统一的线程池,并且根据机器性能来控制数量;
  • 检查线程间的锁;
  • 防止主线程因为其他线程的锁而等待空转
  • 为各个任务建立依赖关系,最终构成一个有向无环图。对于可以并发的任务,会通过线程池最大程度提升启动速度,通过启动框架进行优化,例如:AndroidStartUp、Aplha、mmkernel

GC 优化

  • 在启动过程,要尽量减少GC的次数,避免造成主线程长时间的卡顿;
  • 通过systrace单独查看整个启动过程GC的时间;
  • 启动过程避免进行大量的字符串操作,特别是序列化跟反序列化过程。一些频繁创建的对象,例如网络库和图片库中的Byte数组、Buffer可以复用。如果一些模块实在需要频繁创建对象,可以考虑移到Native实现;
  • 监控启动过程总GC的耗时情况,特别是阻塞式同步GC的总次数和耗时
// GC使用的总耗时,单位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式GC的总耗时
Debug.getRuntimeStat("art.gc.blocking-gc-time");

系统调优优化

在启动过程,我们尽量不要做系统调用,例如PackageManagerService操作、Binder调用等待;

  • 在启动过程也不要过早地拉起应用的其他进程,SystemServer和新的进程都会竞争CPU资源。特别是系统内存不足的时候,当我们拉起一个新的进程,可能会成为“压死骆驼的最后一根稻草”。它可能会触发系统的lowmemorykiller机制,导致系统杀死和拉起(保活)大量的进程,从而影响前台进程的CPU;

I/O 优化

  • 启动过程不建议出现网络I/O;
  • 启动过程尽可能的减少磁盘I/O,只解析启动过程用到的数据项则会很大程度减少解析时间,启动过程适合使用随机读写的数据结构
  • 可以将ArrayMap改造成支持随机读写、延时解析的数据存储方式

数据重排

原理:Dex文件用的到的类和安装包APK里面各种资源文件一般都比较小,但是读取非常频繁。我们可以利用系统这个机制将它们按照读取顺序重新排列,减少真实的磁盘I/O次数;

  • 类重排
  • 第一步、启动过程类加载顺序可以通过复写ClassLoader得到
  • 第二步、然后通过ReDex(facebook开源)的Interdex调整类在Dex中的排列顺序;
  • 资源文件重排
  • 通过修改Kernel源码,单独编译了一个特殊的ROM;
  • 支付宝App构建优化解析:通过安装包重排布优化Android端启动性能

类加载优化

通过Hook的方式去掉类加载的过程中verifyclass的步骤;

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号