Android应用性能优化最佳实践.2.5 启动优化

2.5 启动优化


随着应用的功能越来越丰富、启动时需要初始化的工作多、界面的元素复杂等,启动速度不可避免地受到影响,比如一开始单击时出现黑屏或者白屏,甚至在低端机型上出现假死的现象,本节通过学习应用的启动流程、启动速度的监控,发现影响启动速度的问题所在,并优化启动的逻辑,提高应用的启动速度。

2.5.1 应用启动流程

Android应用程序的载体是APK文件,其中包含了组件和资源,APK文件可能运行在一个独立的进程中,也有可能产生多个进程,还可以多个APK运行在同一个进程中,可以通过不同的方式来实现。但有两点需要注意,第一,每个应用只对应一个Application对象,并且启动应用一定会产生一个Application对象;第二,应用程序可视化组件Activity是应用的基本组成之一,因此要分析启动的性能,就有必要了解这两个对象的工作流程和生命周期。

1.?Application

Application是Android系统框架中的一个系统组件,Android程序启动时,系统会创建一个Application对象,用来存储系统的一些信息。Android系统会自动在每个程序运行时创建一个Application类的对象,并且只创建一个,可以理解为Application是一个单例类。

应用可以不指定一个具体的Application,系统会自动创建,但一般在开发中都会创建一个继承于系统Application的类实现一些功能,比如一些数据库的创建、模块的初始化等。但这个派生类必须在AndroidManifest.xml中定义好,在application标签增加name属性,并添加自己的Application的类名,代码如下:

<application

android:name=".GmfApplication"

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

AndroidManifest.xml中的application标签很多,这些标签的说明在官网有详细的介绍,这里就不做讲解。

启动Application时,系统会创建一个PID,即进程ID,所有的Activity都会在此进程上运行。在Application创建时初始化全局变量,同一个应用的所有Activity都可以取到这些全局变量的值,Application对象的生命周期是整个程序中最长的,它的生命周期就等于这个应用程序的生命周期,因为它是全局的单例的,所以在不同的Activity或者Service中获得的对象都是同一个对象。因此在安卓中要避免使用静态变量来存储长久保存的值,可以用Application,但并不建议使用太多的全局变量。

AndroidManifest.xml文件上的application标签指定了重写的Application类后,看看该类可以重载的几个抽象接口,代码清单2-7是一个自定义的Application。

代码清单2-7 自定义的Application

    public class GmfApplication extends Application {

    private static Context mContext = null;

    @Override

    protected void attachBaseContext(Context base) {

        super.attachBaseContext(base);

        mContext = this;

    }

    @Override

    public void onCreate() {

        super.onCreate();

    }

    @Override

    public void onTerminate() {

        super.onTerminate();

    }

    @Override

    public void onConfigurationChanged(Configuration newConfig) {

        super.onConfigurationChanged(newConfig);

    }

    @Override

    public void onLowMemory() {

        super.onLowMemory();

    }

    @Override

    public void onTrimMemory(int level) {

        super.onTrimMemory(level);

    }

    public static Context getContext(){

        return mContext;

    }

}

从代码清单2-7中可以看到几个重要的抽象接口,这些接口的调用时机如下:

attachBaseContext(Context base):得到应用上下文的Context,在应用创建时首先调用。

onCreate():应用创建时调用,晚于attachBaseContext()方法。

onTerminate():应用结束时调用。

onConf?igurationChanged():系统配置发生变化时调用。

onLowMemory():系统低内存时调用。

onTrimMemory(int level):系统要求应用释放内存时调用,level为级别。

从上面的抽象方法可以看出,这些方法都在这个应用生命周期之中,attachBaseContext和onCreate在应用创建时必须调用,而其他需要满足一定的触发时机。

在开发过程中,尽量使用Application中的Context实例,因为使用Activity中的Context可能会导致内存泄漏。也可以使用Activity的getApplicationContext方法。

2.?Activity

Activity大家都非常熟悉了,这里也不做太多解释,只需要理解它的生命周期,因为这在启动优化的过程中非常重要。

在Activity的生命周期中,系统会按类似于阶梯金字塔的顺序调用一组核心的生命周期方法,如图2-37所示。也就是说,Activity生命周期的每个阶段就是金字塔上的一阶。当系统创建一个新Activity实例时,每个回调方法会将Activity状态向顶端移动一阶。金字塔顶端是Activity在前台运行并且用户可以与其交互的时间点。当用户开始离开Activity时,系统调用其他方法在金字塔中将Activity状态下移,从而销毁Activity。在有些情况下,Activity将只在金字塔中部分下移并等待(如当用户切换到其他应用时),Activity可从该点开始移回顶端(如果用户返回到该Activity),并在用户停止的位置继续。

 

图2-37 Activty生命周期金字塔模型

大多数应用包含若干不同的Activity,用户可通过这些Activity执行不同的操作。无论Activity是用户单击应用图标时创建的主Activity,还是应用在响应用户操作时开始的其他Activity,系统都会调用其onCreate()方法创建Activity的每个新实例。因此必须实现onCreate()方法,执行后,在Activity整个生命周期中只需要出现一次基本应用启动逻辑。例如,onCreate()的实现应定义用户界面并且可能实例化某些类范围变量、声明用户界面(在XML布局文件中定义)、定义成员变量,以及配置某些UI。

onCreate()方法包括一个savedInstanceState参数,在有关重新创建Activity中非常有用。

从Application和Activity的介绍中,可以总结出应用启动的流程,如图2-38所示。

其中,启动分为两种类型:冷启动和热启动。

冷启动:因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化Main-Activity类(包括一系列的测量、布局、绘制),最后显示在界面上,如图2-38所示。

热启动:因为会从已有的进程中启动,所以热启动不会再创建和初始化Application,而是直接创建和初始化MainActivity(包括一系列的测量、布局、绘制),即Application只会初始化一次,只包含Activity中的生命周期流程。

2.5.2 启动耗时监测

因为一个应用在启动或者跳入某个页面时是否流畅,时间是否太长,仅仅通过肉眼来观察是非常不准确的,并且在不同设备和环境会有完全不同的表现,所以要准确知道耗时,就需要有效准确的数据,首先通过shell来获取启动耗时。

1.?adb shell am

应用启动的时间会受到很多因素的影响,比如首次安装后需要解压apk文件,绘制时GPU的耗时等,所以在应用层很难获取到启动耗时,但借助ADB可以得到准确的启动时间。

使用adb shell获得应用真实的启动时间,代码如下:

adb shell am start -W [packageName]/[packageName.AppstartActivity]

执行后可以得到三个时间:

ThisTime:一般和TotalTime时间一样,如果在应用启动时开了一个过度的全透明的页面(Activity)预先处理一些事,再显示出主页面(Activity),这样将比TotalTime小。

TotalTime:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。

WaitTime:一般比TotalTime大些,包括系统影响的耗时。

但这个方法只能得到固定的某一个阶段的耗时,不能得到具体哪个方法的耗时,下面介绍第二个方案:代码打点输出耗时。

2.?代码打点

通过代码打点来准确获取记录每个方法的执行时间,知道哪些地方耗时,然后再有针对性地优化,下面通过一个简单的例子来讲解打点的方案。

以下代码是一个统计耗时的数据结构,通过这个数据结构记录整个过程的耗时情况。

    public class TimeMonitor {

    private final String TAG = "TimeMonitor";

    private int monitorId = -1;

    // 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间

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

    private long mStartTime = 0;

 

    public TimeMonitor(int id) {

        GLog.d(TAG,"init TimeMonitor id:" + id);

        monitorId = id;

    }

 

    public int getMonitorId() {

        return monitorId;

    }

 

    public void startMoniter() {

        // 每次重新启动,都需要把前面的数据清除,避免统计到错误的数据

        if (mTimeTag.size() > 0) {

            mTimeTag.clear();

        }

        mStartTime = System.currentTimeMillis();

    }

 

    // 打一次点,tag交线需要统计的上层自定义

    public void recodingTimeTag(String tag) {

        // 检查是否保存过相同的tag

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

            mTimeTag.remove(tag);

        }

        long time = System.currentTimeMillis() - mStartTime;

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

        mTimeTag.put(tag, time);

    }

    public void end(String tag,boolean writeLog){

        recodingTimeTag(tag);

        end(writeLog);

    }

    public void end(boolean writeLog) {

        if (writeLog) {

            // 写入到本地文件

        }

        testShowData();

    }

    public void testShowData(){

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

            GLog.e(TAG,"mTimeTag is empty!");

            return;

        }

        Iterator iterator = mTimeTag.keySet().iterator();

        while (iterator != null && iterator.hasNext()){

            String tag = (String)iterator.next();

            GLog.d(TAG,tag + ":" +  mTimeTag.get(tag));

        }

    }

    public HashMap<String, Long> getTimeTags() {

        return mTimeTag;

    }

}

这个对象可以用在很多需要统计的地方,不仅可以统计应用启动的耗时,还可以统计其他模块,如统计一个Activity的启动耗时和一个Fragment的启动耗时。流程为:在创建这个对象时,需要传入一个ID,这个ID是需要统计的模块或者一个生命周期流程的ID,ID自定义并且是唯一的,一个TimeMonitor对应一个ID。其中end(Boolean writeLog)方法表示这个监控的流程结束,其中writeLog表示是否需要写入本地,建议实现这个方法,可以统计一系列的数据,最好上传到服务器,用来监控这个应用在外网的实际启动状况。

上传到服务器时建议抽样上报,比如根据用户ID的尾号来抽样上报,虽然不影响性能,但还是尽量不要全部上报,用后台下发抽样比较好。

比如现在要统计启动应用在各阶段的耗时,就自定义一个ID,为了使代码更好管理,编写一个专门定义所有ID的类,方便以后的维护,代码如下:

    public class TimeMonitorConfig {

    // 应用启动耗时

    public static final int TIME_MONITOR_ID_APPLICATION_START = 1;

}

因为耗时统计可能会在多个模块和类中需要打点,所以需要一个单例类来管理各个耗时统计的数据,这里使用了一个单例类来实现:TimeMonitorManager,代码如下:

public class TimeMonitorManager {

    private static TimeMonitorManager mTimeMonitorManager = null;

    private static Context mContext  = null;

    private HashMap<Integer,TimeMonitor> timeMonitorList = null;

    public synchronized static TimeMonitorManager getInstance(){

        if(mTimeMonitorManager == null){

            mTimeMonitorManager = new TimeMonitorManager();

        }

        return mTimeMonitorManager;

    }

    public TimeMonitorManager(){

        timeMonitorList = new HashMap<Integer,TimeMonitor>();

    }

    // 初始化某个打点模块

    public void resetTimeMonitor(int id){

        if(timeMonitorList.get(id) != null){

            timeMonitorList.remove(id);

        }

        getTimeMonitor(id);

    }

    // 获取打点器

    public TimeMonitor getTimeMonitor(int id){

        TimeMonitor monitor = timeMonitorList.get(id);

        if(monitor == null){

            monitor = new TimeMonitor(id);

            timeMonitorList.put(id,monitor);

        }

       return monitor;

    }

}

在有需要的地方通过这个方法进行打点,为了得到有效的数据,总结起来主要在两个方面需要打点:

应用程序的生命周期节点,如Application的onCreate、Activity或Fragment的回调函(onCreate、onResume等)。

启动时需要初始化的重要方法,如数据库初始化、读取本地的一些数据等。

其他耗时的一些算法。

例如,在启动时加入统计,在Application和第一个Activity加入打点统计,结合前面讲过的启动生命周期,首先进入的是Application的attachBaseContext()方法,然后在Oncreate结束时打第一个点,在AppstartActivity结束打第二个点,在AppstartActivity中的onStart()打最后一个点,代码如下:

Application:

@Override

protected void attachBaseContext(Context base) {

    super.attachBaseContext(base);      TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START);

}

@Override

public void onCreate() {

    super.onCreate();

    InitModule(); TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recodingTimeTag("ApplicationCreate");

}

第一个Activity:

@Override

protected void onCreate(Bundle savedInstanceState) {

    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recodingTimeTag("AppStartActivity_create");

    super.onCreate(savedInstanceState);

 

    setContentView(R.layout.activity_app_start);

    mLogo = (ImageView) this.findViewById(R.id.logo);

    // mStartHandler.sendEmptyMessageDelayed(0,1000);

    // useAnimation();

    useAnimator();

    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recodingTimeTag("AppStartActivity_createOver");

}

 

    @Override

protected void onStart() {

    super.onStart();

    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("AppStartActivity_start",false);

}

结果如下所示:

 

可以在项目中核心基类的关键回调函数和核心方法加入打点,另外插桩也是一种不错的方式。

2.5.3 启动优化方案

在Android应用开发中,应用启动速度对用户体验非常重要,也是一个应用给用户的第一个性能方面的体验,因此应用启动优化是非常有必要的。应用启动优化的核心思想就是要快,在启动过程中做尽量少的事。但是应用功能越丰富,模块越多,需要初始化的地方也越多,导致了应用启动变慢。

为了优化启动的速度,首先要了解启动时做了什么,来看一个例子,启动源码中性能优化的启动应用(应用源码在http://github.com/lyc7898/AndroidTech),通过打点,统计从单击打开应用到首页显示完成的时间,后面章节会讲到打点的注意事项和具体实现。表2-4是通过打点获取到这个应用启动时,各个模块占用的时间。

因为这个应用比较简单,没有什么模块和数据,所以大头是在绘制工作上,也就是闪屏页(显示启动LOGO的页面)和进入后首页的布局上,其次是一些初始化工作,但实际上一个稍大型的应用,模块初始化和数据准备工作的占比会高很多,因为这块的优化也是非常有必要。

从总体上看,启动主要完成三件事:UI布局、绘制和数据准备,因此启动速度的优化就是需要优化这三个过程,我们也可以通过一些启动界面策略进行优化。接下来从启动耗时最高的UI布局和启动加载逻辑两个方向优化,达到降低启动耗时的目的。因为这个应用非常简单,所以不具有代码性,但优化的流程和方案是通用的。

1.?UI布局

这个应用启动过程为:启动应用→Application初始化→AppstartActivity→HomePageActivity。AppstartActivity是应用的闪屏页(也叫启动页),我们看到大部分应用都有这么一个页面,为什么要有闪屏页呢?闪屏页的存在主要有两个好处:一是可以作为品牌宣传展示,如节日运营或热点事件运营,也可以做广告展示(不要太低端);其二,因为闪屏一般需要停留一段时间,在这段时间可以做很多事情,比如底层模块的初始化、数据的预拉取等。

首先需要优化AppstartActivity的布局,从前面的章节可以知道,要提高显示的效率,一是减少布局层级,二是避免过度绘制,因为前面已经有很详细的例子了,这里不做过多介绍,优化的步骤如下:

使用Prof?ile GPU Rendering检查启动时是否有严重的掉帧,见2.2.1节。

使用Hierarchy View检查布局文件(XML)分析布局并优化,见2.3.1节。

2.?启动加载逻辑优化

一个应用越大,涉及的模块越多,包含的服务甚至进程就会越多,如网络模块的初始化、底层数据初始化等,这些加载都需要提前准备好,有些不必要的就不要放到应用中。可以用以下四个维度分整理启动的各个点:

必要且耗时:启动初始化,考虑用线程来初始化。

必要不耗时:首页绘制。

非必要耗时:数据上报、插件初始化。

非必要不耗时:不用想,这块直接去掉,在需要用的时再加载。

把数据整理出来后,按需实现加载逻辑,采取分步加载、异步加载、延期加载策略,如图2-39所示。

 

图2-39 启动优化方向

要提高应用的启动速度,核心思想是在启动过程中少做事情,越少越好。

在应用中,增加启动默认图或者自定义一个Theme,在Activity首先使用一个默认的界面可以解决部分启动短暂黑屏问题,如android:theme="@style/Theme.AppStartLoad"。

时间: 2024-08-31 14:25:48

Android应用性能优化最佳实践.2.5 启动优化的相关文章

Android应用性能优化最佳实践.

移动开发 Android应用性能优化最佳实践 罗彧成 著 图书在版编目(CIP)数据 Android应用性能优化最佳实践 / 罗彧成著. -北京:机械工业出版社,2017.1 (移动开发) ISBN 978-7-111-55616-9 I. A- II. 罗- III. 移动终端-应用程序-程序设计 IV. TN929.53 中国版本图书馆CIP数据核字(2016)第315986号 Android应用性能优化最佳实践 出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037

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

一触即发 App启动优化最佳实践 文中的很多图都是Google性能优化指南第六季中的一些截图 Google给出的优化指南来镇楼 https://developer.android.com/topic/performance/launch-time.html 闪屏定义 Android官方的性能优化典范,从第六季开始,发起了一系列针对App启动的优化实践,地址如下: https://www.youtube.com/watch?v=Vw1G1s73DsY&index=74&list=PLWz5r

Web前端优化最佳实践之Server篇

Web 前端优化最佳实践第二部分面向 Server .目前共计有 6 条实践规则.[注,这最多算技术笔记,查看最原始内容,还请访问:Exceptional Performance : Best Practices for Speeding Up Your Web Site ] 1. 使用 CDN (Use a Content Delivery Network) 国内 CDN 的普及还不够.不过我们有独特的电信.网通之间的问题,如果针对这个作优化,基本上也算能收到 CDN 或类似的效果吧(假装如此

Web前端优化最佳实践之Mobile(iPhone)篇

Web 前端优化最佳实践最后一部分是针对移动应用的,其实只是针对 iPhone 的,目前只有两条规则. 1. 单个数据对象小于 25K (Keep Components under 25K) 这个似乎只是针对 iPhone 研究的.建议保持单个 Web 数据对象在 25 K 以下.为什么是 25K? Apple 官方信息指出可缓存到内存中的 Web 对象最大支持到 10M,但经过测试,发现也就是 25K 左右. iPhone 在市场上的优异表现,让 Web 人员不得不考虑如何针对其进行优化.相信

Web前端优化最佳实践之图象篇

Web 前端优化最佳实践第六部分面向 图片(Image),这部分目前有 4 条规则.在最近的 Velocity 2008 技术大会上,Yahoo! 的 Stoyan Stefanov 做的 Image Optimization: How Many of These 7 Mistakes Are You Making 也非常有参考价值.结合一起说一下. 1. 优化图片 (Optimize Images) 使用 GIF .JPG 还是 PNG 格式的图片? 尽可能的使用 PNG 格式的图片,更多的功

Web前端优化最佳实践之JavaScript篇

Web 前端优化最佳实践之 JavaScript 篇,这部分有 6 条规则,和 CSS 篇 重复的有几条.前端优化最佳实践,最重要的还是"实践",要理解这东西容易得很,关键是要去"实践",去"执行",去"反馈",去获取受益. 1. 脚本放到 HTML 代码页底部 (Put Scripts at the Bottom) 当一个脚本在下载的时候,浏览器干不了其它的事儿(串行了).所以,把它扔到最后面去处理.对于一些功能性的脚本,可

Web前端优化最佳实践之CSS篇

Web 前端优化最佳实践第四部分面向 CSS.目前共计有 6 条实践规则.另请参见 Mozilla 开发者中心的文章:Writing Efficient CSS 1. 把 CSS 放到代码页上端 (Put Stylesheets at the Top) 官方的解释我觉得多少有点语焉不详.这一条其实和用户访问期望有关.CSS 放到最顶部,浏览器能够有针对性的对 HTML 页面从顶到下进行解析和渲染.没有人喜欢等待,而浏览器已经考虑到了这一点. 2. 避免 CSS 表达式 (Avoid CSS Ex

Web前端优化最佳实践之Cookie篇

Web 前端优化最佳实践第三部分面向 Cookie .目前只有 2 条实践规则. 1. 缩小 Cookie (Reduce Cookie Size) Cookie 是个很有趣的话题.根据 RFC 2109 的描述,每个客户端最多保持 300 个 Cookie,针对每个域名最多 20 个 Cookie (实际上多数浏览器现在都比这个多,比如 Firefox 是 50 个) ,每个 Cookie 最多 4K,注意这里的 4K 根据不同的浏览器可能不是严格的 4096 .别扯远了,对于 Cookie

RDS MySQL空间优化最佳实践

在前三期介绍了RDS for MySQL参数优化,锁问题以及延迟优化最佳实践之后,本期将介绍存储空间相关的最佳实践. 存储空间是RDS很重要的一个指标,在RDS的工单问题中,空间问题的咨询可以排在top 5,当RDS的实际使用空间超过了购买的空间后,实例就会被锁定了,这样就会导致应用无法再写入,更新数据,造成应用的报错.在RDS的控制台中可以设定空间的报警阀值,当实例空间到达报警阀值后用户就会收到报警短信,这个时候用户则需要对判断当前的空间增长是否合理.如果增长合理则需要对实例的进行弹性升级,这

Android应用性能优化最佳实践导读

前 言 为什么写这本书 一个好的应用,除了要有吸引人的功能和交互之外,在性能上也应该有高的要求,即使应用非常具有特色,或者功能和业务具有唯一性,在产品前期可能吸引了部分用户,但用户体验不好的话,也会给产品带来很差的口碑,如果有在体验上更好的竞品,用户也会很快转移.那么一个好的应用应该如何定义呢?主要有三方面: 业务/功能 符合逻辑的交互 优秀的性能 众所周知,Android系统作为以移动设备为主的一款操作系统,硬件配置有一定的限制,虽然配置现在越来越高级,但仍然无法和PC相比,在CPU和内存上的