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

    xiaoxiao2024-04-12  123

    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"。

    相关资源:android应用性能优化最佳实践.pdf
    最新回复(0)