「性能优化系列」APP启动优化理论与实践(上)

本文涉及的产品
阿里云百炼推荐规格 ADB PostgreSQL,4核16GB 100GB 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 尤其在如今的快时代,一款秒开的App比一款启动需要耗费好几秒的App更容易被用户喜爱和长期使用,整的不好还容易被用户永久拉入黑名单。这时候,应用的启动优化就必不可少了

应用启动的时间作为应用的门面,重要性可想而知。尤其在如今的快时代,一款秒开的App比一款启动需要耗费好几秒的App更容易被用户喜爱和长期使用,整的不好还容易被用户永久拉入黑名单。这时候,应用的启动优化就必不可少了。那么接下来就来了解下关于启动优化的一些注意事项。

一、应用启动类型

1.1.冷启动


冷启动是指应用程序从零开始,系统的进程在此启动之前没有创建应用程序的进程,或者由于系统杀死了应用后再启动。在冷启动开始时,系统有三个任务。这些任务包括:


  • 加载并启动应用程序。
  • 启动后立即显示一个空白的启动窗口。
  • 创建应用程序app进程。


一旦app进程创建完成,系统就开始下一阶段:


  • 创建app对象;
  • 启动主线程。
  • 创建主Activity
  • 开始对View进行布局。


1.2.热启动


热启动不同于冷启动,热启动在启动应用时,系统中已经有了该应用的进程,启动时也就少了创建进程等一系列耗时的操作。


1.3.温启动


温启动的启动速度处于冷启动和热启动之间,温启动会重新走Activity的onCreate生命周期。


二、应用启动流程


优化应用的启动速度主要是在于冷启动时,应用的启动耗时。冷启动时应用会从零开启,这就需要先了解一下当我们点击Launcher上app图标后,进程之间做了什么处理?


2.1.启动基本流程



  1. 点击App图标,Launcher进程向SystemServer进程发起startActivity请求;
  2. SystemServer进程收到请求后,向Zygote进程发送创建App进程的请求;
  3. Zygote进程fork出新的App进程,App进程就开始向SystemServer进程发出attachApplication请求,在此同时,App进程会执行bindApplication,即创建Application,调用Application的onCreate;
  4. SystemServer在收到attachApplication请求后,再次向App进程发送scheduleLauncherActivity请求;
  5. App进程收到请求后,通过handler向主线程发送LAUNCHE_ACTIVITY消息,主线程收到消息后通过反射机制创建目标Activity,并回调Activity.OnCreate()等方法。到此,App才正式启动,开始Activity的生命周期。


从应用的启动流程可以发现,应用的启动其实是App进程与SystemServer进程,Zygote进程相互配合的过程。对于启动速度的优化,应用层我们所要关注并且能够干预的也就是Application和Activity的创建。


2.2.Application


App运行时,会首先自动创建Application类并实例化 Application对象,且只有一个。Application的创建时间比Activity要早,在上面应用的启动流程中也提到,在启动App时,会创建Application,那么就需要先去了解下它的生命周期。


  • attachBaseContext():得到应用上下文的Context,在应用创建时会首先调用;
  • onCreate():同样在应用创建时调用,但比attachBaseContext()要晚;
  • onTerminate():应用结束时调用;
  • onConfigurationChange():系统配置发生变化时调用;
  • onLowMemory():系统低内存时调用;
  • onTrimMemory():系统要求应用释放内存时调用。


从Application的生命周期可以看到,应用创建时会依次调用attachBaseContext()和onCreate(),这两个生命周期包含在应用的启动流程中,启动速度优化可以以此为一个切入点。


2.3.Activity


从点击图标到用户看见前台数据所经历的生命周期,也就是Activity的onCreate(),onResume()。众所周知,Activity会在onCreate()中加载布局以及进行数据的初始化。既然包含在应用的启动中,那么也可以作为一个切入点。


2.4.小结


从上面的启动流程到Application和Activity的介绍,应用启动优化的切入点也就如下图所示:



作为应用层所能监控并且能处理的,第一点是属于Application的创建,第二点就是Activity的创建。那么接下来就需要去监测各个部分所耗费的时间,再针对性的进行优化。


三、启动耗时的监测


3.1.logcat生成所有log


在连接上设备后可以在串口或者adb中利用命令打印出所有的log:


  1. 生成log文件:logcat > /data/xxx.txt
  2. 利用adb将文件pull出来:adb pull /data/xxx.txt


执行完上面两条命令后,就会生成一个全log的txt文件,pull出文件后,可以在文件中查看所有的信息,包括启动发生的时间,类名,线程号等。


logcat生成的xxx.txt虽然包含了很详细的信息,但是还需要我们自己去计算各个生命周期间所耗费的时间。


3.2.adb命令执行


除了3.1提到的用logcat打印全log外,adb还有一条命令可以直接生成应用启动的时间。adb shell am start -W [packageName]/[AppstartActivity]


执行完后台会生成ThisTimeTotalTimeWaitTime这三个时间,ThisTime代表一连串启动 Activity 的最后一个 Activity 的启动耗时;TotalTime表示应用的启动时间,包括创建进程,Application初始化和Activity初始化到界面显示,一般来说与ThisTime一样;WaitTime则表示AMS启动Activity的总耗时,一般比TotalTime大。


对于监测应用的启动速度,我们只需要关注TotalTime这个值。


如上所述,利用adb命令得到启动时间,也只是一个阶段的总时间,却不能如3.1一样监测到每个生命周期所耗费的时间,无法得到具体的耗时,无疑对启动速度针对性优化没有多大的帮助。


3.3.代码打点


代码打点是通过代码编写一个工具类,通过代码的形式获取每个方法的执行的时间,这个方法与3.1所达到的目的是一致的,都能得到每个周期具体的耗时。唯一不同的是3.1的方式需要不断敲击命令,再在文件中去查找有效信息,无疑是耗费人力的,而通过代码打点的方式可以实时监测每个方法的耗时,也可以生成信息上传到服务器。


下面是一个基本的代码打点的案例:


public class TimeMonitorManager {

   private static final String TAG = "TimeMonitorManager";


   private HashMap<String, Long> mTimeTagMap = new HashMap<>();

   private long mStartTime = 0;

   private static volatile TimeMonitorManager mMonitorManager;

   

   private TimeMonitorManager() {


   }


   public static TimeMonitorManager getInstance() {

       if (mMonitorManager == null) {

           synchronized (TimeMonitorManager.class) {

               if (mMonitorManager == null) {

                   mMonitorManager = new TimeMonitorManager();

               }

           }

       }

       return mMonitorManager;

   }


   /**

    * 开始监听.

    */

   public void startMonitor() {

       if (mTimeTagMap.size() > 0) {

           mTimeTagMap.clear();

       }


       mStartTime = System.currentTimeMillis();


   }



   /**

    * 结束监听.

    * @param tag 所要打印的tag.

    */

   public void endMonitor(String tag) {

       if (mTimeTagMap.get(tag) != null) {

           mTimeTagMap.remove(tag);

       }


       long time = System.currentTimeMillis() - mStartTime;

       mTimeTagMap.put(tag, time);

       showData();

   }


   private void showData() {

       if (mTimeTagMap.size() <= 0) {

           return;

       }


       for (String tag: mTimeTagMap.keySet()

            ) {

           long time = mTimeTagMap.get(tag);

           Log.d(TAG, tag + ": " + time);

       }

   }


}


在需要打点开始的地方调用startMonitor(),在结束的地方调用endMonitor(String tag),例如Activity在加载布局,也就是setContentView(R.layout.activity_main)时是耗时的,根据打点规则,在setContentView(R.layout.activity_main);前后调用TimeMonitorManager的方法,以达到监测setContentView所耗费的时间.如下:


   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       TimeMonitorManager.getInstance().startMonitor();

       setContentView(R.layout.activity_main);

       TimeMonitorManager.getInstance().endMonitor(TAG + " onCreate setContentView");

   }


运行后Logcat中打印的效果如下:


TimeMonitorManager: MainActivity onCreate setContentView: 60


可以发现,Activity在setContentView()时所耗费的时间大约为60ms.


利用TimeMonitorManager打点的好处在于可以清楚的监测到每一处的耗时,更加精准;


从第二节应用启动流程就了解到,应用启动耗时能够监测的切入点为Application的创建和Activity的创建,我们就可以通过代码打点的方式在以下这些地方进行打点:


  • Application的onCreate()
  • Activity的onCreate(),onStart(),onResume()
  • 初始化对象的方法,注册等方法
  • 一些耗时的操作


四、优化方向


从上面三节可以了解到,影响应用启动时间的要素一般可分为Application里一些数据的准备,Activity的布局以及在初始化的耗时操作。下面也将从这几个方面分别描述下优化策略。


4.1.布局优化


4.1.1.按需选择布局方式


我们都知道onCreate()里的setContentView()是用来加载布局,应用启动时会去解析xml中的结构,当一个xml里结构嵌套过多,系统需要去解析的时间就大大增加了。


当布局比较复杂,可以使用ConstraintLayout布局,ConstraintLayout是Android Studio 2.2新增的一个功能,它的一大特点就是为了解决布局嵌套。具体使用方法可参考


另外,当版本较低且布局复杂,RelativeLayout布局优化的效果是要优于LinearLayout。但是当布局简单时,LinearLayout却优于RelativeLayout,所以大家可依照具体情况进行选择。


4.1.2.< include >、< merge >


< include >与< merge >是布局优化的两个利器。< include >标签是可以允许在一个布局当中引入另外一个布局,当多个布局中有用到相同的部分,就可以采用< include >标签将相同的部分提取出来,利用< include >将公告部分替代。


而< merge >标签的作用是作为< include >标签的一种辅助扩展来使用的,它的主要作用是为了防止在引用布局文件时产生多余的布局嵌套。


4.1.3.ViewStub


ViewStub是一个比较轻量级的控件,没有大小,不需要绘制,同时也不参与布局,所以消耗的资源是非常小的。ViewStub的使用就在于当我们存在时而需要显示时而不显示的view的时候,就可以使用它。例如我们进行网络请求时的loading bar,请求时,会显示,当请求结束,loading bar就会消失,这个时候就可以使用ViewStub,减少资源的消耗的同时也减少了布局解析的时间。


另外,针对布局的优化,AS提供了Profile和Hierarchy View两个工具分别检查View的绘制和分析布局。


具体使用可参考


4.2.逻辑加载优化


逻辑耗时一般分为Application中和Activity中的逻辑加载。在Application或者Activity中进行初始化的时候,有些的逻辑初始化是必要的,而有些初始化非必要,可以适当的延时去加载。例如在最近的项目中,应用启动时需要提前去连接服务并且去注册回调接口,为了提前连接上服务,将连接的操作就放置在了Application里进行初始化。这就是属于必要的逻辑操作。对于不同优先级的逻辑,我们可以大致分为以下几点:


4.2.1.异步加载(必要且耗时)


有些逻辑处理的优先级比较高,并且初始化耗时,可以采用异步加载的方式,利用RxJava,HandlerThread,IntentService等在后台进行加载,这样就不会阻塞主线程,UI展现到用户眼前的时间也会缩短。


4.2.2.延时加载(非必要且耗时)


当逻辑操作的优先级不是很高时,可以采取延时加载的方式,也就是应用启动过程中暂时不去初始化这些逻辑,将之前在Application或者Activity onCreate()中的操作移除,在主线程空闲的时候再进行加载操作。


另外,MessageQueue内部有一个接口IdleHandler,可以很好的处理延时问题。IdleHandler在looper里面的message都处理完了的时候就会回调这个接口,返回false,就会移除它,返回true就会在下次message处理完了的时候继续回调。


举个例子,一般情况下,应用启动时会去绘制布局,会去调用measure, layout, draw等方法,在执行这些操作后,用户才会去看见UI,而之前优先级不高的初始化,就可以延时在这些操作后加载,那么主要的问题就是我们如何去判断measure, layout, draw等操作已经完成了?延时加载的时机在哪?IdleHandler就帮我们解决了这个问题,上面也都知道IdleHandler是在队列为空的时候会去回调它,measure, layout, draw都可以作为一个个message,IdleHandler就会在他们执行完成后响应。这个时候就可以进行之前需要延时的初始化操作。


另外,当代码中同时有UI绘制和逻辑加载,可以在IdleHandler回调中再去处理逻辑加载,UI绘制与逻辑分开操作,可以减少数据空白时间长的问题。


4.2.3.分步加载


当初始化对象有很多时,且必要,可以采取分步加载的方式,将逻辑的优先级区分开来,优先级高的先加载。


五、总结


启动优化需要针对不同的业务做出不同的优化方式,例如可以采用Multidex预加载优化,但是虚拟机在5.0以上默认就使用ART,对于项目是5.0以上版本就不需要去优化此方面。


总的来说,优化方向可以分为布局优化,减少解析xml和绘制的时间;逻辑优化,将必要且耗时的操作异步加载,将非必要的采用延时加载,另外,将操作优先级高的可以优先加载。


最后,启动速度优化是一个大工程,后续还需要针对具体场景继续深度挖掘。


参考:《Android应用性能优化最佳实践》

相关实践学习
阿里云百炼xAnalyticDB PostgreSQL构建AIGC应用
通过该实验体验在阿里云百炼中构建企业专属知识库构建及应用全流程。同时体验使用ADB-PG向量检索引擎提供专属安全存储,保障企业数据隐私安全。
AnalyticDB PostgreSQL 企业智能数据中台:一站式管理数据服务资产
企业在数据仓库之上可构建丰富的数据服务用以支持数据应用及业务场景;ADB PG推出全新企业智能数据平台,用以帮助用户一站式的管理企业数据服务资产,包括创建, 管理,探索, 监控等; 助力企业在现有平台之上快速构建起数据服务资产体系
目录
相关文章
|
12天前
|
数据采集 网络协议 算法
移动端弱网优化专题(十四):携程APP移动网络优化实践(弱网识别篇)
本文从方案设计、代码开发到技术落地,详尽的分享了携程在移动端弱网识别方面的实践经验,如果你也有类似需求,这篇文章会是一个不错的实操指南。
32 1
|
3月前
|
API Android开发
Android P 性能优化:创建APP进程白名单,杀死白名单之外的进程
本文介绍了在Android P系统中通过创建应用进程白名单并杀死白名单之外的进程来优化性能的方法,包括设置权限、获取运行中的APP列表、配置白名单以及在应用启动时杀死非白名单进程的代码实现。
64 1
|
3月前
|
Web App开发 移动开发 前端开发
如何优化运行在webkit上的web app
如何优化运行在webkit上的web app
|
3月前
|
XML 安全 Java
App安全检测实践基础——工具
App安全检测实践基础——工具
95 0
|
4月前
|
XML 缓存 Android开发
🎯解锁Android性能优化秘籍!让你的App流畅如飞,用户爱不释手!🚀
【7月更文挑战第28天】在移动应用竞争中,性能是关键。掌握Android性能优化技巧对开发者至关重要。
42 2
|
5月前
|
ARouter IDE 开发工具
Android面试题之App的启动流程和启动速度优化
App启动流程概括: 当用户点击App图标,Launcher通过Binder IPC请求system_server启动Activity。system_server指示Zygote fork新进程,接着App进程向system_server申请启动Activity。经过Binder通信,Activity创建并回调生命周期方法。启动状态分为冷启动、温启动和热启动,其中冷启动耗时最长。优化技巧包括异步初始化、避免主线程I/O、类加载优化和简化布局。
83 3
Android面试题之App的启动流程和启动速度优化
|
5月前
|
缓存 JSON 网络协议
Android面试题:App性能优化之电量优化和网络优化
这篇文章讨论了Android应用的电量和网络优化。电量优化涉及Doze和Standby模式,其中应用可能需要通过用户白名单或电池广播来适应限制。Battery Historian和Android Studio的Energy Profile是电量分析工具。建议减少不必要的操作,延迟非关键任务,合并网络请求。网络优化包括HTTPDNS减少DNS解析延迟,Keep-Alive复用连接,HTTP/2实现多路复用,以及使用protobuf和gzip压缩数据。其他策略如使用WebP图像格式,按网络质量提供不同分辨率的图片,以及启用HTTP缓存也是有效手段。
91 9
|
5月前
|
XML 监控 安全
Android App性能优化之卡顿监控和卡顿优化
本文探讨了Android应用的卡顿优化,重点在于布局优化。建议包括将耗时操作移到后台、使用ViewPager2实现懒加载、减少布局嵌套并利用merge标签、使用ViewStub减少资源消耗,以及通过Layout Inspector和GPU过度绘制检测来优化。推荐使用AsyncLayoutInflater异步加载布局,但需注意线程安全和不支持特性。卡顿监控方面,提到了通过Looper、ChoreographerHelper、adb命令及第三方工具如systrace和BlockCanary。总结了Choreographer基于掉帧计算和BlockCanary基于Looper监控的原理。
93 3
|
6月前
uni-app 171部分小细节优化
uni-app 171部分小细节优化
29 1
|
6月前
|
开发框架 JavaScript 前端开发
深入探讨Vue.js核心技术及uni-app跨平台开发实践
深入探讨Vue.js核心技术及uni-app跨平台开发实践
137 0

热门文章

最新文章

下一篇
无影云桌面