一触即发App启动优化最佳实践

本文涉及的产品
阿里云百炼推荐规格 ADB PostgreSQL,4核16GB 100GB 1个月
简介:

文中的很多图都是Google性能优化指南第六季中的一些截图

Google给出的优化指南来镇楼

https://developer.android.com...

闪屏定义

Android官方的性能优化典范,从第六季开始,发起了一系列针对App启动的优化实践,地址如下:

https://www.youtube.com/watch...

可想而知,App的启动性能是非常重要的。同时,Google针对App闪屏,也给出了非常详细的设计定义,如下所示。

https://material.google.com/p...

其实最早的时候,闪屏是用来在App未完全启动的时候,让用户不至于困惑App是否启动而加入的一个设计。而现在的很多App,基本上都把闪屏当做一个广告、宣传的页面了,貌似已经失去了原本的意义,但闪屏,不管怎么说,在一个App启动的时候,都是非常重要的,设计的事情,交给UE吧,开发要做的,就是让App的启动体验,做到最好。

App启动流程

App启动的整个过程,可以分解成下面几个过程:

  1. 用户在Launcher上点击App Icon
  2. 系统为App创建进程,显示启动窗口
  3. App在进程中创建自己的组件

这个过程可以用下面这幅图来描述:

而我们能够优化的,也就是下面Application的创建部分,系统的进程分配以及一些窗口切换的动画效果等,都是跟ROM相关的,我们无法处理。所以,我们需要把重点放到Application的创建过程。

上面是官方的说明,下面我们用更加通俗的语言来解释一遍。

当用户点击桌面icon的时候,系统准备好了,给App分配进程空间,就好像去酒店开房,但是你又不能直接进入房间,你得坐电梯去房间,那么你坐电梯的这个时间,实际上就是系统的准备时间,那么系统的这个准备时间一般来说不会太长,但假如的开的是一个总统套房呢,系统就得花不少时间来打理,所以系统给所有用户都准备了一个过渡界面,这个界面,就是启动时的黑屏白屏,也就是你坐电梯里面看的小广告,看完小广告,你就到房间了,然后你想干嘛都可以了,这个想干嘛的速度,就完全取决于你开门的速度了,你门开得快,自然那啥快,所以这里是开发者可以优化的地方,有些开发者掏个钥匙要好几秒,有的只要几百毫秒,完全影响了后面那啥的效率。

那么一般来说,故事到这里就结束了,但是,系统,也就是这个酒店,并不是一个野鸡酒店,他也想尽量做得让顾客满意,这样才会有回头客啊,所以,酒店做了一个优化,可以让每个顾客自己定义在坐电梯的时候想看什么!也就是说,系统在加载App的时候,首先是加载了资源文件,这里就包括了要启动的Activity的Theme,而这个Theme呢,是可以自定义的,也就是顾客在坐电梯时想看的东西,而不是千篇一律的白屏或者黑屏,他可以定制很多东西,例如ActionBar、背景、StatBar等等。

启动时间的测量

关于Activity启动时间的定义

对于Activity来说,启动时,首先执行的是onCreate()、onStart()、onResume()这些生命周期函数,但即使这些生命周期方法回调结束了,应用也不算已经完全启动,还需要等View树全部构建完毕,一般认为,setContentView中的View全部显示结束了,算作是应用完全启动了。

Display Time

从API19之后,Android在系统Log中增加了Display的Log信息,通过过滤ActivityManager以及Display这两个关键字,可以找到系统中的这个Log:


  
  
  1. $ adb logcat | grep “ActivityManager” 

ActivityManager: Displayed com.example.launcher/.LauncherActivity: +999ms

抓到的Log如图所示:

那么这个时间,实际上是Activity启动,到Layout全部显示的过程,但是要注意,这里并不包括数据的加载,因为很多App在加载时会使用懒加载模式,即数据拉取后,再刷新默认的UI。

reportFullyDrawn

前面说了,系统日志中的Display Time只是布局的显示时间,并不包括一些数据的懒加载等消耗的时间,所以,系统给我们定义了一个类似的『自定义上报时间』——reportFullyDrawn。

同样是借用Google的一张图来说明:

reportFullyDrawn是由我们自己调用的,一般在数据全部加载完毕后,手动调用,这样就会在Log中增加一条日志:


  
  
  1. $ adb logcat | grep “ActivityManager” 
  2. ActivityManager: Displayed com.example.launcher/. LauncherActivity: +999ms 
  3. ActivityManager: Fully drawn com.example.launcher/. LauncherActivity: +1s999ms  

一般来说,使用的场景如下:


  
  
  1. public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> { 
  2.  
  3.     @Override 
  4.     protected void onCreate(Bundle savedInstanceState) { 
  5.         super.onCreate(savedInstanceState); 
  6.         setContentView(R.layout.activity_main); 
  7.     } 
  8.  
  9.     @Override 
  10.     public void onLoadFinished(Loader<Void> loader, Void data) { 
  11.         // 加载数据 
  12.         // …… 
  13.         // 上报reportFullyDrawn 
  14.         reportFullyDrawn(); 
  15.     } 
  16.  
  17.     @Override 
  18.     public Loader<Void> onCreateLoader(int id, Bundle args) { 
  19.         return null
  20.     } 
  21.  
  22.     @Override 
  23.     public void onLoaderReset(Loader<Void> loader) { 
  24.  
  25.     } 
  26.  

但是要注意,这个方式需要API19+,所以,这里需要对SDK版本进行判断。

计算启动时间——ADB

通过ADB命令可以统计应用的启动时间,指令如下所示:


  
  
  1.   ~  adb shell am start -W com.xys.preferencetest/.MainActivity 
  2. Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xys.preferencetest/.MainActivity } 
  3. Status: ok 
  4. Activity: com.xys.preferencetest/.MainActivity 
  5. ThisTime: 1047 
  6. TotalTime: 1047 
  7. WaitTime: 1059 
  8. Complete  

该指令一共给出了三个时间:

  • ThisTime:最后一个启动的Activity的启动耗时
  • TotalTime:自己的所有Activity的启动耗时
  • WaitTime: ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和自己Activity的启动)

这三个时间不是很好理解,我们可以把整个过程分解

1.上一个Activity的onPause()——2.系统调用AMS耗时——3.第一个Activity(也许是闪屏页)启动耗时——4.第一个Activity的onPause()耗时——5.第二个Activity启动耗时

那么,ThisTime表示5(最后一个Activity的启动耗时)。TotalTime表示3.4.5总共的耗时(如果启动时只有一个Activity,那么ThisTime与TotalTime应该是一样的)。WaitTime则表示所有的操作耗时,即1.2.3.4.5所有的耗时。

每次给出的时间可能并不一样,而且应用从首次安装启动到后面每次正常启动,时间都会不同,区别于系统是否要分配进程空间。

计算启动时间——Screen Record

通过录屏进行启动的分析,是一个很好的办法,在API21+,Android给我们提供了一个更加方便、准确的方式:


  
  
  1.  ~ adb shell screenrecord --bugreport /sdcard/test.mp4 

Android在screenrecord中新增了一个参数——bugreport,那么加了这个参数之后,录制出来的视频,在左上角就会增加一行数字的显示,如图所示。

在视频开始前,会显示设备信息和一些参数:

视频开始后,左上角会有一行数字:

例如图中的:15:31:22.261 f=171(0)

其中,前面的4个数字,就是时间戳,即15点31分22秒261,f=后面的数字是当前的帧数,注意,不是帧率,而是代表当前是第几帧,括号中的数字,代表的是『Dropped framescount』,即掉帧数。

有了这个东西,再结合视频就可以非常清楚的看见这些信息了。

启动时间的调试

模拟启动延时

在测试的时候,我们可以通过下面的方式来进行启动的延迟模拟:


  
  
  1. SystemClock.sleep(2000) 

或者直接通过:


  
  
  1. try { 
  2.     Thread.sleep(2000); 
  3. } catch (InterruptedException e) { 
  4.     e.printStackTrace(); 
  5.  

或者通过:


  
  
  1. new Handler().postDelayed(new Runnable() { 
  2.     @Override 
  3.     public void run() { 
  4.         // Delay 
  5.     } 
  6.  
  7. }, 2000);  

这些方案都可以进行启动延迟的模拟。

强制冷启动

在『开发者选项』中的Background Process Limit中设置为No Background Processes

优化点

Static Block

很多代码中的Static Block,都是做一些初始化工作,特别是ContentProvider中在Static Block中初始化一些UriMatcher,这些东西可以做成懒加载模式。

Application

Application是程序的主入口,特别是很多第三方SDK都会需要在Application的onCreate里面做很多初始化操作,不得不说,各种第三方SDK,都特别喜欢这个『兵家必争之地』,再加上自己的一些库的初始化,会让整个Application不堪重负。

优化的方法,无非是通过以下几个方面:

  • 延迟初始化
  • 后台任务
  • 界面预加载

阻塞

阻塞有很多种情况,例如磁盘IO阻塞(读写文件、SharedPerfences)、网络阻塞(现在应该不会了)以及高CPU占用的代码(加解密、渲染、解析等等)。

View层级

见《Android群英传》

耗时方法

通过使用TraceView && Systrace && Method Tracing工具来进行排查,见《Android群英传:神兵利器》

App启动优化的一般过程

  1. 通过TraceView、Systrace来分析耗时的方法与组件。
  2. 梳理启动加载的每一个库、组件。
  3. 将梳理出来的库,按功能和需求进行划分,设计该库的启动时机。
  4. 与交互沟通,设计启动画面,按前文方法进行优化。

解决方案

Theme

当系统加载一个Activity的时候,onCreate()是一个耗时过程,那么在这个过程中,系统为了让用户能有一个比较好的体验,实际上会先绘制一些初始界面,类似于PlaceHolder。

系统首先会读取当前Activity的Theme,然后根据Theme中的配置来绘制,当Activity加载完毕后,才会替换为真正的界面。所以,Google官方提供的解决方案,就是通过android:windowBackground属性,来进行加载前的配置,同时,这里不仅可以配置颜色,还能配置图片,例如,我们可以使用一个layer-list来作为android:windowBackground要显示的图:

start_window.xml


  
  
  1. <layer-list xmlns:android="http://schemas.android.com/apk/res/android" 
  2.             android:opacity="opaque"
  3.     <item android:drawable="@android:color/darker_gray"/> 
  4.     <item> 
  5.         <bitmap 
  6.             android:gravity="center" 
  7.             android:src="@mipmap/ic_launcher"/> 
  8.     </item> 
  9. </layer-list>  

可以看见,这里通过layer-list来实现图片的叠加,让开发者可以自由组合。

配置中的android:opacity="opaque"参数是为了防止在启动的时候出现背景的闪烁。

接下来可以设置一个新的Style,这个Style就是Activity预加载的Style。


  
  
  1. <resources> 
  2.  
  3.     <!-- Base application theme. --> 
  4.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"
  5.         <!-- Customize your theme here. --> 
  6.         <item name="colorPrimary">@color/colorPrimary</item> 
  7.         <item name="colorPrimaryDark">@color/colorPrimaryDark</item> 
  8.         <item name="colorAccent">@color/colorAccent</item> 
  9.     </style> 
  10.  
  11.     <style name="StartStyle" parent="AppTheme"
  12.         <item name="android:windowBackground">@drawable/start_window</item> 
  13.     </style> 
  14. </resources>  

OK,下面在Mainifest中给Activity指定需要预加载的Style:


  
  
  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android" 
  3.           package="com.xys.startperformancedemo"
  4.  
  5.     <application 
  6.         android:allowBackup="true" 
  7.         android:icon="@mipmap/ic_launcher" 
  8.         android:label="@string/app_name" 
  9.         android:supportsRtl="true" 
  10.         android:theme="@style/AppTheme"
  11.         <activity 
  12.             android:name=".MainActivity" 
  13.             android:theme="@style/StartStyle"
  14.             <intent-filter> 
  15.                 <action android:name="android.intent.action.MAIN"/> 
  16.  
  17.                 <category android:name="android.intent.category.LAUNCHER"/> 
  18.             </intent-filter> 
  19.         </activity> 
  20.     </application> 
  21.  
  22. </manifest>  

这里需要注意下,一定是Activity的Theme,而不是Application的Theme。

最后,我们在Activity加载真正的界面之前,将Theme设置回正常的Theme就好了:


  
  
  1. public class MainActivity extends AppCompatActivity { 
  2.  
  3.     @Override 
  4.     protected void onCreate(Bundle savedInstanceState) { 
  5.         setTheme(R.style.AppTheme); 
  6.         super.onCreate(savedInstanceState); 
  7.         SystemClock.sleep(2000); 
  8.         setContentView(R.layout.activity_main); 
  9.     } 
  10.  

在这个Activity中,我使用SystemClock.sleep(2000),模拟了一个Activity加载的耗时过程,在super.onCreate(savedInstanceState)调用前,将主题重新设置为原来的主题。

通过这种方式设置的效果如下:

启动的时候,会先展示一个画面,这个画面就是系统解析到的Style,等Activity加载完全完毕后,才会加载Activity的界面,而在Activity的界面中,我们将主题重新设置为正常的主题,从而达到一个友好的启动体验,这种方式其实并没有真正的加速启动过程,而是通过交互体验来优化了展示的效果。

异步初始化

这个很简单,就是让App在onCreate里面尽可能的少做事情,而利用手机的多核特性,尽可能的利用多线程,例如一些第三方框架的初始化,如果能放线程,就尽量的放入线程中,最简单的,你可以直接new Thread(),当然,你也可以通过公共的线程池来进行异步的初始化工作,这个是最能够压缩启动时间的方式

延迟初始化

延迟初始化并不是减少了启动时间,而是让耗时操作让位、让资源给UI绘制,将耗时的操作延迟到UI加载完毕后,所以,这里建议通过mDecoView.post方法,来进行延迟加载,代码如下:


  
  
  1. getWindow().getDecorView().post(new Runnable() { 
  2.  
  3.   @Override public void run() { 
  4.     …… 
  5.   } 
  6. });  

我们的ContentView就是通过mDecoView.addView加入到根布局的,所以,通过这种方式,可以让延迟加载的内容,在ContentView初始化完毕后,再进行执行,保证了UI绘制的流畅性。

IntentService

IntentService是继承于Service并处理异步请求的一个类,在IntentService的内部,有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样,同时,当任务执行完后,IntentService会自动停止,而不需要去手动控制。


  
  
  1. public class InitIntentService extends IntentService { 
  2.  
  3.     private static final String ACTION = "com.xys.startperformancedemo.action"
  4.  
  5.     public InitIntentService() { 
  6.         super("InitIntentService"); 
  7.     } 
  8.  
  9.     public static void start(Context context) { 
  10.         Intent intent = new Intent(context, InitIntentService.class); 
  11.         intent.setAction(ACTION); 
  12.         context.startService(intent); 
  13.     } 
  14.  
  15.     @Override 
  16.     protected void onHandleIntent(Intent intent) { 
  17.         SystemClock.sleep(2000); 
  18.         Log.d(TAG, "onHandleIntent: "); 
  19.     } 
  20.  

我们将耗时任务丢到IntentService中去处理,系统会自动开启线程去处理,同时,在任务结束后,还能自己结束Service,多么的人性化!OK,只需要在Application或者Activity的onCreate中去启动这个IntentService即可:


  
  
  1. @Override 
  2. protected void onCreate(Bundle savedInstanceState) { 
  3.     super.onCreate(savedInstanceState); 
  4.     setContentView(R.layout.activity_main); 
  5.     InitIntentService.start(this); 
  6.  

最后不要忘记在Mainifest注册Service。

使用ActivityLifecycleCallbacks

Framework提供的这个方法可以监控到所有Activity的生命周期,在这里,我们就可以通过onActivityCreated这样一个回调,来将一些UI相关的初始化操作放到这里,同时,通过unregisterActivityLifecycleCallbacks来避免重复的初始化。同时,这里onActivityCreated回调的参数Bundle,可以用来区别是否是被系统所回收的Activity。


  
  
  1. public class MainApplication extends Application { 
  2.  
  3.     @Override 
  4.     public void onCreate() { 
  5.         super.onCreate(); 
  6.         // 初始化基本内容 
  7.         // …… 
  8.         registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { 
  9.             @Override 
  10.             public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 
  11.                 unregisterActivityLifecycleCallbacks(this); 
  12.                 // 初始化UI相关的内容 
  13.                 // …… 
  14.             } 
  15.  
  16.             @Override 
  17.             public void onActivityStarted(Activity activity) { 
  18.             } 
  19.  
  20.             @Override 
  21.             public void onActivityResumed(Activity activity) { 
  22.             } 
  23.  
  24.             @Override 
  25.             public void onActivityPaused(Activity activity) { 
  26.             } 
  27.  
  28.             @Override 
  29.             public void onActivityStopped(Activity activity) { 
  30.             } 
  31.  
  32.             @Override 
  33.             public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 
  34.             } 
  35.  
  36.             @Override 
  37.             public void onActivityDestroyed(Activity activity) { 
  38.             } 
  39.         }); 
  40.     } 
  41.  

资源优化

有几个方面,一个自然是优化布局、布局层级,一个是优化资源,尽可能的精简资源、避免垃圾资源,这些可以通过混淆和tinyPNG这些工具来实现。

甩锅方案

下面是两种不同的方案,都是在Style中进行配置:


  
  
  1. <item name="android:windowDisablePreview">true</item> 


  
  
  1. <item name="android:windowIsTranslucent">true</item> 
  2. <item name="android:windowNoTitle">true</item>  

我们先来看看这样做的效果:

设置效果类似,即通过取消、透明化系统的统一的加载页面来达到启动的『加速』,实际上,是一个『甩锅』的过程。强烈建议开发者不要通过这种方式去做『所谓的启动加速』,这种方式虽然看上去自己的App启动非常快,瞬间就完成了,但实际上,是将真正的启动界面给隐藏了。

系统说:这锅,我们不背!

无解

对应5.0以下的65535问题,目前只能通过Multidex来进行处理,而在5.0以下的机器上,系统在加载前的合并Dex的过程,有可能非常长,这也是暂时无解的问题,只能希望后面Multidex进行优化。

OK,App的启动优化基本如上,其重点过程,依然是分析耗时的操作,以及如何设计合理的启动顺序,希望各位能够通过文中介绍的方式来进行App的启动优化。





作者:tomcat的猫
来源:51CTO
相关实践学习
阿里云百炼xAnalyticDB PostgreSQL构建AIGC应用
通过该实验体验在阿里云百炼中构建企业专属知识库构建及应用全流程。同时体验使用ADB-PG向量检索引擎提供专属安全存储,保障企业数据隐私安全。
AnalyticDB PostgreSQL 企业智能数据中台:一站式管理数据服务资产
企业在数据仓库之上可构建丰富的数据服务用以支持数据应用及业务场景;ADB PG推出全新企业智能数据平台,用以帮助用户一站式的管理企业数据服务资产,包括创建, 管理,探索, 监控等; 助力企业在现有平台之上快速构建起数据服务资产体系
目录
相关文章
|
5月前
|
JSON Dart 安全
Flutter App混淆加固、保护与优化原理
Flutter App混淆加固、保护与优化原理
104 0
|
5月前
|
架构师 Java
jvm性能调优实战 - 35电商APP后台系统如何对Full GC进行深度优化
jvm性能调优实战 - 35电商APP后台系统如何对Full GC进行深度优化
93 0
|
12天前
|
监控 安全 Apache
构建安全的URL重定向策略:确保从Web到App平滑过渡的最佳实践
【10月更文挑战第2天】URL重定向是Web开发中常见的操作,它允许服务器根据请求的URL将用户重定向到另一个URL。然而,如果重定向过程没有得到妥善处理,可能会导致安全漏洞,如开放重定向攻击。因此,确保重定向过程的安全性至关重要。
24 0
|
2月前
|
Web App开发 移动开发 前端开发
如何优化运行在webkit上的web app
如何优化运行在webkit上的web app
|
4月前
|
ARouter IDE 开发工具
Android面试题之App的启动流程和启动速度优化
App启动流程概括: 当用户点击App图标,Launcher通过Binder IPC请求system_server启动Activity。system_server指示Zygote fork新进程,接着App进程向system_server申请启动Activity。经过Binder通信,Activity创建并回调生命周期方法。启动状态分为冷启动、温启动和热启动,其中冷启动耗时最长。优化技巧包括异步初始化、避免主线程I/O、类加载优化和简化布局。
72 3
Android面试题之App的启动流程和启动速度优化
|
4月前
|
缓存 JSON 网络协议
Android面试题:App性能优化之电量优化和网络优化
这篇文章讨论了Android应用的电量和网络优化。电量优化涉及Doze和Standby模式,其中应用可能需要通过用户白名单或电池广播来适应限制。Battery Historian和Android Studio的Energy Profile是电量分析工具。建议减少不必要的操作,延迟非关键任务,合并网络请求。网络优化包括HTTPDNS减少DNS解析延迟,Keep-Alive复用连接,HTTP/2实现多路复用,以及使用protobuf和gzip压缩数据。其他策略如使用WebP图像格式,按网络质量提供不同分辨率的图片,以及启用HTTP缓存也是有效手段。
79 9
|
4月前
|
XML 监控 安全
Android App性能优化之卡顿监控和卡顿优化
本文探讨了Android应用的卡顿优化,重点在于布局优化。建议包括将耗时操作移到后台、使用ViewPager2实现懒加载、减少布局嵌套并利用merge标签、使用ViewStub减少资源消耗,以及通过Layout Inspector和GPU过度绘制检测来优化。推荐使用AsyncLayoutInflater异步加载布局,但需注意线程安全和不支持特性。卡顿监控方面,提到了通过Looper、ChoreographerHelper、adb命令及第三方工具如systrace和BlockCanary。总结了Choreographer基于掉帧计算和BlockCanary基于Looper监控的原理。
79 3
|
5月前
uni-app 171部分小细节优化
uni-app 171部分小细节优化
26 1
|
5月前
|
存储 缓存 算法
jvm性能调优实战 - 34十万QPS的社交APP 如何优化GC性能提升3倍?
jvm性能调优实战 - 34十万QPS的社交APP 如何优化GC性能提升3倍?
140 0
jvm性能调优实战 - 34十万QPS的社交APP 如何优化GC性能提升3倍?
|
10月前
|
JSON Dart 安全
Flutter App混淆加固、保护与优化原理
在移动应用程序开发中,保护应用程序的代码和数据安全至关重要。本文将探讨如何对Flutter应用程序进行混淆、优化和保护,以提高应用程序的安全性和隐私。