阅读 255

探索App性能优化之启动速度优化

一.启动速度优化的意义

举一个栗子

如果我们去一家餐厅吃饭,在点餐的时候等了半天都没有服务人员过来,可能就没有耐心等待直接走了。

对于App来说,也是同样如此,如果用户点击App后,半天都打不开,用户就可能失去耐心卸载应用。

启动速度是用户对我们App的第一体验,打开应用后才能去使用其中提供的强大功能,就算我们应用的内部界面设计的再精美,功能再强大,如果启动速度过慢,用户第一印象就会很差。因此,拯救App的启动速度,迫在眉睫。

下面,我们来逐步深入探索提升Android App启动速度的奥秘。

二.启动方式

Android应用的启动方式分为三种:冷启动、暖启动、热启动,不同的启动方式决定了应用UI对用户可见所需要花费的时间长短。 冷启动消耗的时间最长。基于冷启动方式的优化工作也是最考验产品用户体验的地方。谈及优化之前,下面我们来看看这三种启动方式的应用场景,以及启动过程中系统都做了些什么工作。

(一) 冷启动

在Android系统中,系统为每个运行的应用至少分配一个进程 (多进程应用申请多个进程)。从进程角度上讲,冷启动就是在启动应用前,系统中没有该应用的任何进程信息 (包括 Activity、Service 等) 。

所以,冷启动产生的场景就很容易理解了,比如设备开机后应用的第一次启动,系统杀掉应用进程 (如:系统内存吃紧引发的kill 和用户主动产生的kill)后的再次启动等。那么这种方式下,应用的启动时间最长,因为相比另外两种启动方式,系统和我们的应用要做的工作最多。

启动速度优化主要是针对冷启动方式,所以我们说的启动速度优化就是指冷启动速度优化。其他两种启动方式也要了解下。

(二) 暖启动

当应用中的Activity被销毁,但在内存中常驻时,应用的启动方式就会变为暖启动。 相比冷启动,暖启动过程减少了对象初始化、布局加载等工作,启动时间更短。但启动时,系统依然会展示一个空白背景,直到第一个 Activity 的内容呈现为止。

(三) 热启动

相比暖启动,热启动时应用做的工作更少,启动时间更短。热启动产生的场景很多,常见如:用户使用返回键退出应用,然后马上又重新启动应用。

三.应用启动流程

从技术角度来说,当用户点击桌面图标开始,系统会立即为这个App创建独立的专属进程,然后显示启动窗口,直到App 在自己的进程里面完成了程序的创建以及主线程完成了Activity的初始化显示操作,再然后系统进程就会把启动窗口替换成App 的显示窗口。

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

1.用户在Launcher上点击App Icon

2.系统为App创建进程,显示启动窗口,创建应用进程信息

3.App在进程中创建自己的组件

    - 初始化应用中的对象(比如 Application 中的工作);
    - 启动主线程(UI 线程);
    - 创建第一个 Activity;
    - 加载内容视图(Inflating);
    - 计算视图在屏幕上的位置排版(Laying out);
    - 绘制视图(draw);
复制代码

只有当应用完成第一次绘制,系统当前展示的空白背景才会消失,才会被Activity的内容视图替换掉。也就是这个时候,用户才能和我们的应用开始交互。这个过程可以用下面这幅图来描述:

上述流程里面的红色框部分是由系统控制的,跟ROM相关的,我们无法处理。对于启动速度,我们能够控制的是Application的创建过程,所以需要特别关注的地方主要有三处。

(二) Application的创建过程,需要特别关注的三处:

1.Application

Application的onCreate流程,对于大型的App来说,通常会在这里做大量的通用组件的初始化操作。

2.Activity

Activity的onCreate流程,特别是UI的布局与渲染操作,如果布局过于复杂很可能导致严重的启动性能问题。

3.闪屏(主题)

修改主题优化启动时白屏/黑屏,这里可以做成品牌宣传界面或者是给用户提供一种程序已经启动的视觉效果。

四.启动时间的测量

(一) DisplayTime和requestFullyDrawn

在API19之后,Android在系统Log中打印,通过过滤ActivityManager以及Displa这两个关键字,可以找到系统中的这个Log:

$ adb logcat | grep “ActivityManager”
ActivityManager: Displayed com.example.launcher/. LauncherActivity: +999ms
ActivityManager: Fully drawn com.example.launcher/. LauncherActivity: +1s999ms
复制代码

DisplayTime和requestFullyDeawn的区别

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

requestFullyDrawn:是由开发人员手动调用的,一般在数据全部加载完毕后。

(二) 计算启动时间——adb命令

adb shell am start -W packagename/主activity的全路径
例如:
adb shell am start -W com.hx.clent/com.yt.hxmobile.MainActivity
复制代码
➜  ~  adb shell am start -W com.xys.preferencetest/.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xys.preferencetest/.MainActivity }
Status: ok
Activity: com.xys.preferencetest/.MainActivity
ThisTime: 1047
TotalTime: 1047
WaitTime: 1059
Complete
复制代码

WaitTime:返回从 startActivity 到应用第一帧完全显示这段时间。就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;

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

TotalTime:表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。 开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。

(三) 注意

如果是本地调试的话,统计启动时间还是很简单的,通过命令行方式即可。当 App 发到线上之后,想要统计 App 在用户手机上的启动速度,就不能通过命令行的方式进行统计了,基本上都是通过打 Log 的方式将启动时间发送上来。

五.优化思路

作为普通应用,App进程的创建等环节我们是无法主动控制的。开发人员唯一能做的就是在Application 和 第一个 Activity 中,减少 onCreate() 方法的工作量,从而缩短冷启动的时间。像应用中嵌入的一些第三方 SDK,都建议在 Application 中做一些初始化工作,开发人员不妨采取懒加载的形式移除这部分代码,而在真正需要用到第三方 SDK 时再进行初始化。

Google也给出了启动加速的方向:

1.避免在启动时做密集沉重的初始化

2.利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验

3.定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等

六.优化方案

(一) 主题切换

通过启动窗口主题的方式来替换系统默认的启动窗口时白屏/黑屏,通过这种方式只是使用「障眼法」弱化了用户对启动时间的感知,但本质上并没有对启动速度做什么优化。

第一步:自定义背景名为start_window.xml

<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">
    <item android:drawable="@android:color/holo_blue_dark" />
    <item>
        <bitmap
            android:gravity="center"
            android:src="@mipmap/soaryuan_background" />
    </item>
</layer-list>
复制代码

第二步:自定义一个名为SplashTheme的主题,设置为上面的start_window背景

<!-- 启动页主题 -->
<style name="SplashTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
    <item name="android:windowBackground">@drawable/start_window</item>
</style>
复制代码

最后:在AndroidManifest.xml设置启动页使用我们自定义的主题SplashTheme

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.soaryuan.client">

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">
    
<!-- 启动页 -->
<activity
    android:name=".SplashActivity"
    android:theme="@style/SplashTheme">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

</manifest>
复制代码

(二) 避免Application的onCreate进行太多的工作

在Application初始化的地方做太多繁重的事情是可能导致严重启动性能问题的元凶之一。Application里面的初始化操作不结束,其他任意的程序操作都无法进行。Application的onCreate中会做大量第三方组件的初始化工作,其实很多组件是需要做区别对待的,有些可以做延迟加载,有些可以放到其他的地方做初始化操作,特别需要留意包含Disk IO操作,网络访问等严重耗时的任务,他们会严重阻塞程序的启动。

注意点:

  • 项目是多进程架构,只在主进程执行Application的onCreate();
  • 流程梳理,延后执行;
  • 异步加载、延时加载、懒加载

如何判断第三方的库是不是能放在子线程里面:

需要初始化的第三方SDK一般分为两种:

  • 一种是第三方平台的SDK(推送、分享、反馈、统计等) 这个可以通过看其SDK文档, 结合业务需求考虑。 例如分享, 反馈一般不是必须要应用一开启就能用的,这类业务一般层级比较深,有足够的理由让它们在后台异步初始化。

  • 另一种是第三方的库, 建议阅读其源码, 了解其实现原理, 再决定是否放在后台初始化。

项目修改:

  • 将友盟、Bugly、听云、GrowingIO、BlockCanary等组件放在WorkThread中初始化;
  • 延迟地图定位、ImageLoader、自有统计等组件的初始化:地图及自有统计延迟4秒,此时应用已经打开;而ImageLoader因为调用关系不能异步以及过久延迟,初始化从Application延迟到SplashActivity;而EventBus因为再Activity中使用所以必须在Application中初始化。
  • 有一些第三方框架的初始化,如果能放线程,就尽量的放入线程中,最简单的,可以直接new Thread(),也可以通过公共的线程池来进行异步初始化工作,

(三) 避免首个Activity的onCreate进行太多的工作

提升Activity的创建速度是优化App启动速度的首要关注目标。从桌面点击App图标启动应用开始,程序会显示一个启动窗口等待Activity的创建加载完毕再进行显示。 Activity的创建加载过程中,会执行很多的操作,例如设置页面的主题,初始化页面的布局,加载图片,获取网络数据,读写SharePreference等。

  • 使用延迟加载。确保在Activity的页面显示出来之后再进行加载数据,避免过早或过晚的加载导致页面空白时间过长。可采用以下 代码实现延迟加载。在Activity的onCreate方法中:
getWindow().getDecorView().post(new Runnable() {
  @Override
  public void run() {
    myHandler.post(mLoadingRunnable);
  }
});
复制代码

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

  • 优化布局耗时。一个布局层级越深,里面包含需要加载的元素越多,就会耗费更多的初始化时间。

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

public class InitIntentService extends IntentService {

    private static final String ACTION = "com.xys.startperformancedemo.action";

    public InitIntentService() {
        super("InitIntentService");
    }

    public static void start(Context context) {
        Intent intent = new Intent(context, InitIntentService.class);
        intent.setAction(ACTION);
        context.startService(intent);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        SystemClock.sleep(2000);
        Log.d(TAG, "onHandleIntent: ");
    }
 }
复制代码

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

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    InitIntentService.start(this);
}
复制代码

七.最后

OK,App的启动优化基本如上,其重点过程,依然是分析耗时的操作,以及如何设计合理的启动顺序,希望各位能够通过文中介绍的方式来进行App的启动优化。以上也是最常用的三种方式(三板斧),我还会继续探索新方法,比如二进制重排等。敬请期待...

关注下面的标签,发现更多相似文章
评论