Android 性能优化——APP启动优化

一、APP启动流程

首先在《Android系统和APP启动流程》中我们介绍了 APP 的启动流程,但都是 FW 层的流程,这里我们主要分析一下在 APP 中的启动流程。要了解 APP 层的启动流程,首先要了解 APP 启动的分类。

1、启动分类

冷启动

应用从头开始启动,即应用的首次启动。需要做大量的工做,耗费的时间最多。

热启动

当活动有驻留在内存中,系统只是把该活动放到前台,无需重复对象初始化、布局扩充和呈现。例如按了home键,相对于冷启动,开销较低。

温启动

用户退出应用程序,随后又从新启动,可是活动的进程是有驻留在后台的。例如按了back键退出应用。

2、生命周期

冷启动

对于冷启动的耗时计算比较复杂,除了 Activity 本身的生命周期外,还要考虑 Application onCreate 的耗时操作。所以对于冷启动来说,一般从进程创建(即 Application 的attachBaseContext 方法)开始计时,到完成视图的第一次绘制(即 Activity 的 onWindowFocusChanged 方法)停止计时。

冷启动 Activity 生命周期:

onCreate() -> onStart() -> onResume() -> onWindowFocusChanged()

热启动

相比于冷启动,热启动省去了 Application 的初始化,以及 Activity 的 onCreate。热启动生命周期:

Home:onPause() -> onStop()

热启动:onStart() -> onResume()

温启动

温启动生命周期:

Back:onPause() -> onStop() -> onDestory()

热启动:onCreate() -> onStart() -> onResume()

可以看到温启动相比热启动,Activity 生命周期增加了一个 onCreate() 方法,在使用 Back 时也比 Home 多执行一个 onDestory()。

二、启动时间统计

1、埋点统计

根据上面的生命周期,在不同方法中打印当前时间,通过查看 Log 计算不通启动方式的启动耗时。

2、查找Log

通过查找 Log 关键字 Displayed 查看 APP 入口信息,即包含启动时间。

adb命令

adb logcat | grep "Displayed"

结果输出

I/ActivityManager( 384): Displayed com.test.myapp/com.test.myapp.MainActivity: 1734 ms (total 1734 ms)

3、adb命令统计

adb shell am start -W 包名/启动类的全限定名

实际使用

adb shell am start -W com.test.myapp/com.test.myapp.MainActivity

冷启动需要杀掉最近任务进程,再使用上面的命令。热启动需要 Home 键退出应用后使用上面命令。

结果输出

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.test.myapp/.MainActivity }
Warning: Activity not started, its current task has been brought to the front
Status: ok
LaunchState: HOT
Activity: com.test.myapp/.MainActivity
ThisTime: 79
TotalTime: 79
WaitTime: 82
Complete

LaunchState:启动方式 COLD(冷启动)、 HOT(热启动)和 WARM(温启动)。

ThisTime:启动多个 Activity 的最后一个 Activity 的启动耗时。

TotalTime:启动多个 Activity 总耗时,包括新进程的启动和 Activity 的启动。也就是说,开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。

WaitTime:应用进程的创建过程 + TotalTime,就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间。

如果只关心某个应用自身启动耗时,参考TotalTime;如果关心系统启动应用耗时,参考WaitTime;如果关心应用有界面Activity启动耗时,参考ThisTime。

多次测试

adb shell am start -S -R 10 -W com.test.myapp/com.test.myapp.MainActivity

其中 -S 表示每次启动前先强行停止,-R表示重复测试次数,注意反斜杠、包名、类名。所以可以通过 -S 设置冷/热启动。

三、方法耗时统计

traceview 统计可以用代码统计。也可以用 Android Studio自带的 cup profiler 来统计。

1、Debug Trace

java 复制代码
@Override
public void onCreate() {
    super.onCreate();
    Debug.startMethodTracing("main_trace");
    ......
    Debug.stopMethodTracing();
}

生成文件路径:storage/emulated/0/Android/data/packagename/files/main_trace。

然后直接将该文件拖入到 Android Studio 中,就能看到对应方法执行的时长,从而进行相关的优化,如是否可以在异步线程操作该方法或者考虑懒加载的方式,根据自己的业务找到合适的方法。

2、CPU Profiler

Profiler 是 Android Studio 内置的工具。如下配置:

1)run -> edit configurations;

2)勾选start recording a method trace on startup;

3)从菜单中选择cpu记录配置(profiling菜单下勾选两个复选框);

4)apply --> profile模式部署。

详细使用参考:Android app的启动优化总结

3、systrace

java 复制代码
@Override
public void onCreate() {
  super.onCreate();
  Trace.beginSection("Launcher");
  ......
  Trace.endSection();
}

命令行终端进入如下目录:

/Users/tian/Library/Android/sdk/platform-tools/systrace

输入如下命令进入监听状态:

python systrace.py -o main_trace sched freq idle am wm gfx view binder_driver hal dalvik camera input res

此时运行代码,完成之后在命令行窗口按 Enter 键结束监听,然后会生成目标文件 main_trace,同样使用 Android Studio 的 Profiler 工具进行分析。

三、优化策略

1、优化onCreate, onStart,onResume函数

由于许多内容在 Activity 的 UI 初始化和生命周期中需要用到,所以大部分 Activity 中的成员需要在 onCreate 中通过 new 的方式赋值。这就要求 new 的类的构造函数应该尽可能简单,不要有耗时操作,以便快速执行。

不要在这些函数中 new 暂时用不到的内容,比如一些提醒的 dialog,可以在需要提醒的地方再去创建。

2、优化布局文件

减少UI的布局嵌套层数,从而减少 layout 时间。 简化XML布局,界面布局时,层次越多,加载的时间就越长。因此应该尽可能的减少布局层次。如果实在层次太多并且无法简化,建议不使用XML布局,直接在代码中进行布局。

判断嵌套布局是否可以优化的方法:

1)借助工具Hierarchy Viewer,可以看到layout比较耗时的节点。

2)直接review xml布局文件。

布局优化方案:

1)尽量使用 ConstraintLayout 和 RelativeLayout 替换 LinearLayout。

2)尽量为所有分辨率创建资源,减少不必要的硬件缩放,这会降低UI的绘制速度。

3)首次不需要显示的节点,尽量设置为GONE。

3、优化draw过程

去掉不必要的背景,比如如果子节点和父节点size一样,那么父节点的background可以不设或者设为null。

尽可能少用或者不用高质量图片,以提高运行效率。

4、优化数据访问

有些属性需要在 onCreate 就获取,而这些属性保存在 ContentProvider 中。可以从下面两方面进行优化:

1)少用 cursor.getColumnIndex。可以在建表的时候用 static 变量记住某列的 index,直接调用相应index而不是每次查询。

2)查询时返回更少的结果集及更少的字段。只返回需要的字段和结果集,更多的结果集和字段会消耗更多的时间及内存。

5、优化自定义控件或UI部件

自定义控件和UI部件,不管这些控件是否支持 xml 化,实现它们的代码质量很重要,要尽可能简化它们的构造过程。

6、代码方面的优化

1)使用缓存。尽量将需要频繁访问或访问一次消耗较大的数据存储在缓存中。

2)使用多线程。比较耗时的过程,尽可能的使用异步加载。避免UI主线程阻塞,发生长时间不响应。

3)只需要获取图片的高宽时,可以设置 InJustDecodeBounds 为 true。这样就不会去 decode 图片,减少了图片解析的时间。

4)判断语句如果较多时,尽量使用 switch..case..,而不是使用 if..else..。因为 if..else.. 是从上到下进行判断,而 switch..case.. 有对判断条件进行优化。

5)for() 循环中有 if() 判断,考虑实现为将 if() 判断语句放在 for() 语句外面,减少判断次数,for 语句可以快速执行。

6)String的拼接尽量使用io流。

7)数据类型和数据结构的选择。比如:hash 系列数据结构查询速度更优,ArrayList 存储有序元素。

7、延迟执行

对于 onCreate, onStart,onResume 函数中的数据或变量初始化,不着急使用的可以放到使用时在进行初始化,或者放到子线程中处理。

8、空闲时间利用

合理利用程序的空闲时间,等待页面都完全渲染完毕之后再执行。例如:使用IdleHandler,具体使用如下:

java 复制代码
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override
    public boolean queueIdle() {
        // 执行你的任务
        return false;
    }
});

空闲时执行多个任务:

java 复制代码
public class DelayTaskQueue {
    private final Queue<Runnable> mDelayTasks = new LinkedList<>();
    
    private final MessageQueue.IdleHandler mIdleHandler = () -> {
        if (mDelayTasks.size() > 0) {
            Runnable task = mDelayTasks.poll();
            if (task != null) {
                task.run();
            }
        }
        // mDelayTasks非空时返回ture表示下次继续执行,为空时返回false系统会移除该IdleHandler不再执行
        return !mDelayTasks.isEmpty();
    };

    public DelayTaskQueue addTask(Runnable task) {
        mDelayTasks.add(task);
        return this;
    }

    public void start() {
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }
}

9、锁优化

锁是我们解决并发的重要手段,但是如果滥用锁的话,很可能造成执行效率下降,更严重的可能造成死锁等无法挽回的场景。

当我们需要处理高并发的场景时,同步调用尤其需要考量锁的性能损耗:

1)能用无锁数据结构,就不要用锁。

2)缩小锁的范围。能锁区块,就不要锁住方法体;能用对象锁,就不要用类锁。

10、内存优化

内存优化的核心是避免内存抖动。不合理的内存分配、内存泄漏、对象的频繁创建和销毁,都会导致内存发生抖动,最终导致系统的频繁 GC。

1)解决应用的内存泄漏问题。这里我们可以使用LeakCanary 或者 Android Profile 等工具来检查我们查询可能存在的内存泄漏。平时编码应当注意避免内存泄漏。

2)如避免全局静态变量和常量、单例持有资源对象(Activity,Fragment,View等),资源使用完立即释放或者recycle(回收)等。

3)避免创建大内存对象,频繁创建和释放对象(尤其是在循环体内),频繁创建的对象需要考虑复用或者使用缓存。

4)加载图片可以适当降低图片质量,小图标尽量使用SVG,大图/复杂的图片考虑使用webp。尽量使用图片加载框架,如glide,这些框架都会帮我们进行加载优化。

5)避免大量bitmap的绘制。

6)避免在自定义View的onMeasure、onLayout和onDraw中创建对象。

7)使用 SpareArray、ArrayMap 替代 HashMap。

8)避免进行大量的字符串操作,特别是序列化和反序列化。不要使用+(加号)进行字符串拼接。

9)使用线程池(可设置适当的最大线程池数)执行线程任务,避免大量Thread的创建及泄漏。

11、线程优化

当我们创建一个线程时,需要向系统申请资源,分配内存空间,这是一笔不小的开销,所以我们平时开发的过程中都不会直接操作线程,而是选择使用线程池来执行任务。所以线程优化的本质是对线程池的优化。

12、IO优化

IO优化的核心是减少IO次数。

网络请求优化

1)避免不必要的网络请求。对于那些非必要执行的网络请求,可以延时请求或者使用缓存。

2)对于需要进行多次串行网络请求的接口进行优化整合,控制好请求接口的粒度。比如后台有获取用户信息的接口、获取用户推荐信息的接口、获取用户账户信息的接口。这三个接口都是必要的接口,且存在先后关系。如果依次进行三次请求,那么时间基本上都花在网络传输上,尤其是在网络不稳定的情况下耗时尤为明显。但如果将这三个接口整合为获取用户的启动(初始化)信息,这样数据在网络中传输的时间就会大大节省,同时也能提高接口的稳定性。

磁盘IO优化

1)避免不必要的磁盘IO操作。这里的磁盘IO包括:文件读写、数据库(sqlite)读写和SharePreference等。

2)对于数据加载,选择合适的数据结构。可以选择支持随机读写、延时解析的数据存储结构以替代 SharePreference。

3)避免程序执行出现大量的序列化和反序列化(会造成大量的对象创建)。

参考:Android APP启动速度优化

相关推荐
Winston Wood4 分钟前
Android Parcelable和Serializable的区别与联系
android·序列化
清风徐来辽8 分钟前
Android 项目模型配置管理
android
帅得不敢出门35 分钟前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
problc1 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
前端青山8 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
帅得不敢出门12 小时前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
我又来搬代码了13 小时前
【Android】使用productFlavors构建多个变体
android
青云交14 小时前
大数据新视界 -- 大数据大厂之 Impala 性能优化:应对海量复杂数据的挑战(上)(7/30)
大数据·性能优化·impala·数据分区·查询优化·海量复杂数据·经典案例
德育处主任15 小时前
Mac和安卓手机互传文件(ADB)
android·macos
芦半山15 小时前
Android“引用们”的底层原理
android·java