你想要的Android性能优化系列:启动优化 !
App启动优化
为什么要做App的启动优化?
网页端存在的一个定律叫8秒定律:即指用户访问一个网站时,如果等待打开的时间超过8秒,超过70%的用户将会放弃等待。
同样的,移动端也有一个8秒定律:如果一个App的启动时间超过8秒或有明显的卡顿,80%的用户将会退出应用并对程序员进行口吐芬芳。当然这是我瞎编的,但却不代表是不存在的。最起码肯定会影响App在市场上的评分,进而让更多的用户在对比过程中选择竞品。
知道了启动优化的重要性,那么接下来我们就来分析下如何优化App的启动,本文内容主要分为以下三部分:

分析优化方向
App应用主要有三种启动状态:冷启动、热启动和温启动。
1、 冷启动:耗时最长,也是主要的优化点;(恋爱前的女人)
冷启动前,系统主要做了三件事:
加载并启动应用。
在启动后立即显示应用的空白启动窗口。
创建应用进程。
创建应用进程后:
创建应用对象。
启动主线程。
创建主 Activity。
扩充视图。
布局屏幕
执行初始绘制

2、热启动:耗时最短,将activity从后台带到前台;(热恋中的女人)
3、温启动:耗时较长,重走了Actiivty的生命周期。(结婚后的女人)
从应用的启动状态中,我们可以分析得出,剥除系统本身的任务动作外(这部分我们是无法进行操作修改的),其实我们的启动优化方向主要就是:Application和Activity的生命周期、主视图的布局优化(这部分我们放到UI优化系列来讲)。
相关数据测量
优化App的启动速度前,我们得先获取App的一些启动数据,根据这些数据才能准确找到优化的点,才能对优化后的操作做一个准确的评估。(下面的相关代码我将会拿之前的一个旧项目来做演示,一是更贴近实际开发情况,比demo更加直观;二是顺手给优化了,何乐而不为呢?)
获取启动时间
adb命令法:adb shell am start -S -W packagename/activity(含包名)

ThisTime:最后一个 Activity 启动时间;
TotalTime:所有 Activity 启动耗时(这里只启动了一个 MainActivity);
WaitTime:AMS 启动 Activity 的总耗时;
adb 命令虽然简单好用,但还是有不少缺点的:
只能线下使用,而在实际开发过程中,用户的启动时间才是最好的参考指标;
非精确的时间,这里只是显示了 Activity 启动完毕的时间,但对于用户的直观体验来说,只有首页的数据展示出来,才算是真正的启动完成。
手动打点法
public class LaunchTimer {
private static long mTime;
//开启时间
public static void startTime() {
mTime = System.currentTimeMillis();
}
//结束时间
public static void endTime() {
LoggerManager.d("启动时间:" + (System.currentTimeMillis() - mTime));
}
}
在Application类的attachBaseContext()方法中打入开始启动时间点:
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
LaunchTimer.startTime();
}
在我们的首页第一条数据展示成功后打入结束时间点:(ps:网上很多文章都在onwindowfocuschanged()方法中打入结束时间,其实这个方法只是首帧时间,并不代表我们的页面数据等全部展示出来了。我们做优化,还是得以用户的实际体验来作为参考价值,不能仅仅KPI化)
//是否已经记录启动时间
private boolean mIsRecord = false;
@Override
protected void convert(BaseViewHolder helper, final HomeListBean.DataBean
item) {
if (helper.getPosition() == 1 && !mIsRecord) {
mIsRecord = true;
final View contentView = helper.getView(R.id.home_item_rl);
//监听第一条数据的绘制完成时间
contentView.getViewTreeObserver().addOnPreDrawListener(new
ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
contentView.getViewTreeObserver().removeOnPreDrawListener(this);
LaunchTimer.endTime();
return true;
}
});
}
}
运行我们的代码,可以看到启动时间是3111毫秒,正常来说,是会比用adb命令打出的时间要长点。

手动打点的好处:
可以线上使用,统计真实用户的启动时间
时间准确,结合用户真实体验,参考价值更高
2、其他优化分析工具
我经常使用的启动优化工具主要有Traceview(官方文档)和systrace(官方文档),Traceview虽然比较全面,但性能消耗太大,这里不做过多介绍,有兴趣的朋友可以自行查看官方文档,这里主要介绍systrace这个工具(使用前需得先安装python)。
先打点:
public void onCreate() {
super.onCreate();
//使用兼容的TraceCompat打入开始点
TraceCompat.beginSection("AppBegin");
if (instance == null) {
instance = this;
}
if (IS_DEBUG_ABLE) {
initLogger();
}
initBugly();
Tiny.getInstance().init(this);//初始化tiny图片压缩工具
initJPush();
initSkin();
RichText.initCacheDir(this);//设置缓存
initFragmentation();
MMKV.initialize(this);
TraceCompat.endSection();
//使用兼容的TraceCompat记录结束点
TraceCompat.endSection();
}
安装App,使用systrace命令:python systrace.py -b 32768 -t 10 -a packagename -o
outputfile.html sched gfx view wm am app (命令执行过程中点击启动App)
运行操作后,打开我们的html文件,可以看到我们app的启动相关数据

运行操作后,打开我们的html文件,可以看到我们app的启动相关数据:

图中红圈部分是我们需要注意的地方,AppBegin就是我们打点的代表区间,可以看到这段区间时间是732.127毫秒。最下面有两个数值,一个是WallDuration,这个就是我们代码的执行时间,另一个
CPUDuration是我们的CPU执行时间。
为什么两个会有时间差异呢?打个比方:CPU在执行代码的时候,遇到一些需要等待回调的数据才能继续往下执行情况的时候,CPU会处在等待情景,这个时候是不计算CPU执行时间,只有等回调数据回来了,再往下执行时,才算是调用了CPU的资源。
所以这里也点明了我们的优化方向之一:就是如何更好的利用CPU的资源。
PS:其实这段的数据主要都是启动时间的数据,但在实际开发中,我们可能还会去监测每个方法
所用的时间,看看有没有可优化的余地。方法监测的时间除了给每个方法单独打点外,还可以
使用Traceview工具来更好的监测。
优化技巧
终于讲到我们的优化技巧了,具体优化我们可以分为以下几种方式:
闪屏优化
业务优化
线程优化
UI优化
1、闪屏优化
在我们的App启动中,其实是低端机中,其实会有点击了应用,要等待一段时间,才会打开App页面的时间。这个现象对用户的体验非常不友好!那要怎样优化这个现象呢,这里可以采用theme切换的方式来达到视觉上的快速启动。
<!-- 新建layer-list的xml文件 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<item android:drawable="@android:color/white"/>
<item>
<bitmap
//这里是我们想要展示的开屏图片
android:src="@mipmap/longkong_splash"
android:gravity="fill"/>
</item>
</layer-list>
然后新建Theme:
<item name="android:windowBackground">@drawable/lanucher</item>
<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="windowNoTitle">true</item>
</style>
在我们的启动页设置这个Theme:
<activity
android:name=".business.MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:hardwareAccelerated="true"
//设置theme
android:theme="@style/ThemeActivitySplash"
android:windowSoftInputMode="stateUnspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
记得在页面onCreat中切换回我们原用的Theme:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(R.style.ThemeActivity);
super.onCreate(savedInstanceState);
}
现在点击桌面图标,简直是秒开App。当然,这只是视觉上的秒开,实际的启动时间还是没什么变化,下面才是真正优化启动时间的方法。
2、业务优化
我们在一接到优化任务的时候,不要想着立马着手就做一些异步线程优化之类的。第一步应该是先梳理我们的业务。
梳理清楚我们启动的每一个模块,看看哪些是必要的,哪些是可以切掉的,哪些是可以延迟加载的。
①可以切掉的:没什么可说的直接删除;
②可以延迟加载处理的:比如地图SDK、扫一扫等,这些个模块很多时候不一定是在首页就需要用到的,我们可以做一些延迟加载甚至是只有在使用到时才进行初始化的处理。
延迟加载的时机:可以在首页用户数据加载完成时去进行
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
//执行延迟加载的代码
return false;
}
});
IdleHandler的运行机制是:只有在CPU空闲的时候才会去执行操作,这样就不会造成首页用户操作时卡顿的情况。
③必要的:对于必要的代码,我们可以做以下业务相关优化操作:
使用更加优秀框架,比如我们的SharedPreferences,可以尝试夫换成腾讯的MMKV框架,在数据量大的情况下,优化效果非常明显;
使用更加优秀的算法,我们有些代码比如文件操作之类的,或许有更加优秀的算法代码,可以大大减少计算步骤;
作一些取舍,比如一些中低端的机型,或者其本身的性能没办法很好的运行我们的某些功能,在这个时间,我们可以尝试去跟产品经理沟通,做一些功能上的取舍。
3、线程优化
上面我们讲了一些必要代码的业务优化,在一些确实没有业务优化空间,或者优化了还不是很理想的代码,我们可以进行线程优化。
线程优化其实就是合理利用CPU的核心数,将几个耗时的任务进行并发处理,可以极大减少总的运行时间。
线程优化需要注意几点:
合理控制线程的数量:每台机子的核心数都不同,如果我们线程开得太多,可能会相互竞争CPU资源,除了要用线程池进行统一管理外,设置合适的线程数也很重要。
我们可以参照AsyncTask的源码来设置线程池的线程数:
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1,
4));//线程数
任务的依赖关系:有些任务可能是需要前一个任务执行完后再进行操作,如果在线程优化中,没有处理好这个相关,可能会造成空转,如下图:

如何处理好线程的依赖关系,可以参照或使用市面上的一些启动框架,比如阿里开源的Alpha启动框架。
我们这里使用自己自写的Task启动器,其原理都是一样的,最终都是构成一个有向无环图。
TaskDispatcher.init(this);
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
dispatcher.addTask(new InitBulyTask())
.addTask(new InitJPushTask())
.addTask(new InitRichTextTask())
.addTask(new InitSkinTask())
.addTask(new InitTinyTask())
.addTask(new InitFragmentTask())
.start();
dispatcher.await();
经过上面的优化技巧后,我们再来看一下现在的启动时间是多少:



从上面的三张图中,我们可以看出,优化的启动时间缩短了可观的50%左右。
4、UI优化
UI优化主要是对我们的视图布局进行优化,尽量减少绘制时间,对于一些界面复杂的项目,效果也是非常的显著,这里我们暂时不讨论,留待UI优化的文章来讲。
其他优化
除了上面的优化之处,还是有很多其他的优化技巧。比如根据我们具体的业务,还可以做一些类加载的优化,I/O上的优化,GC的优化,磁盘文件的优化,还可以通过保活来达到快速重启,甚至还有一些CPU锁频的黑科技。
总结
App启动优化是门无尽的学文,还是很多可以继续深挖的点。我们在实际开发中,也可以通过监控APM上的数据来进行更加针对性的优化。只有不断的进行实操,才会发现更多可以优化的方向。
最后
我整理了一份 Android 性能优化的学习手册文档 ,包含了:启动优化,UI 布局优化,卡顿优化和布局优化,优化 Glidel 加载超大 gif 图等等,有需要的可以私信【性能优化】或者【点击这里】

相关文章:

你想要的Android性能优化系列:启动优化 !
App启动优化为什么要做App的启动优化?网页端存在的一个定律叫8秒定律:即指用户访问一个网站时,如果等待打开的时间超过8秒,超过70%的用户将会放弃等待。同样的,移动端也有一个8秒定律:如果一个App的启动时间…...
python3的基础入门3:基本数据类型
基本数据类型 python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。 在 Python 中,变量就是变量,它没有类型,我们所说的"类型"是变量所指的内存中对象的类型。 等号(&…...

消息队列原理与实战-学习笔记
消息队列:保存消息的一个容器,本质是个队列,但是需要支持高吞吐、高并发、高可用。 1 前世今生 1.1 业界消息队列对比 Kafka:分布式的、分区的、多副本的日志提交服务,在高吞吐场景下发挥较为出色RocketMQ:低延迟、强一致、高性…...

Linux权限相关知识(大量图文展示,及详细操作)
Linux权限相关概念 Linux下有两种用户:一种是超级用户(root)、一种是普通用户。 超级用户:可以在linux系统下做任何事情,不受限制 普通用户:在linux下做有限的事情。 超级用户的命令提示符是“#”…...

Ep_操作系统面试题-什么是协程
协程 是一种 比线程更加轻量级的存 在,一个线程可以拥有多个协程。是一个特殊的 函数 ,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。协程 不是被操作系统内核所管理 , 而完全是由程序所控制(也就是在…...

在C#中使用互斥量解决多线程访问共享资源的冲突问题
在阿里云上对互斥量的概述:互斥量的获取是完全互斥的,即同一时刻,互斥量只能被一个任务获取。而信号量按照起始的计数值的配置,可以存在多个任务获取同一信号量的情况,直到计数值减为0,则后续任务无法再获取…...

JavaEE进阶第六课:SpringBoot配置文件
上篇文章介绍了SpringBoot的创建和使用,这篇文章我们将会介绍SpringBoot配置文件 目录1.配置文件的作用2.配置文件的格式2.1 .properties语法2.1.1.properties的缺点2.2 .yml语法2.2.1优点分析2.2.2配置与读取对象2.2.3配置与读取集合2.2.4补充说明3.设置不同环境的…...

MySQL基础(一)SQL分类、导入、SELECT语句,运算符
目录 MySQL安装以及相关工具 SQL分类 导入数据 最基本的SELECT语句 SELECT FROM 列的别名 去除重复行 着重号 查询常数 描述表结构 过滤数据(重要) 运算符 算数运算符 比较运算符 符号运算符 非符号运算符 逻辑运算符 位运算符 MySQL安…...

反激与正激的区别
之前学习了正激开关电源,但是对于正激和反激一直不是很清楚,网上找了一篇,觉得感觉该可以,以此记录。正激和反激是两种不同的开关电源技术一、正激(1)概述正激式开关电源是指使用正激高频变压器隔离耦合能量…...

王道操作系统课代表 - 考研计算机 第四章 文件管理 究极精华总结笔记
本篇博客是考研期间学习王道课程 传送门 的笔记,以及一整年里对 操作系统 知识点的理解的总结。希望对新一届的计算机考研人提供帮助!!! 关于对 “文件管理” 章节知识点总结的十分全面,涵括了《操作系统》课程里的全部…...

前端开发规范,你真的了解吗?一起来学习一下前端开发规范,让你的代码高级起来!
代码规范 1 编码风格规范 1.1 使用ES6风格编码源码 定义变量使用let ,定义常量使用const 使用export ,import 模块化 1.2 组件 props 原子化 提供默认值 使用 type 属性校验类型 使用 props 之前先检查该 prop 是否存在 1.3 避免 this.$parent 1.4 谨慎使用 …...

Licode—基于webrtc的SFU/MCU实现
1. webrtc浅析webrtc的前世今生、编译方法、行业应用、最佳实践等技术与产业类的文章在网上卷帙浩繁,重复的内容我不再赘述。对我来讲,webrtc的概念可以有三个角度去解释:(1).一个W3C和IETF制定的标准,约定…...

开发运维工具推荐 --- 解决远程访问局域网服务的问题。开发调试推荐
一、FastNat 可为您解决的问题1. 没公网服务器,需要发布本地的站点或网络程序到公网上,供他人访问;此项功能大大方面开发人员进行远程调试,微信小程序等开发工作进行。2. 需要远程到在其他网络中的设备,但两处的网络不…...
【华为OD机试 】单词倒序(C++ Java JS Python)
文章目录 题目描述输入描述输出描述备注用例题目解析C++ 解法JavaScript算法源码Java算法源码Python解法题目描述 输入单行英文句子,里面包含英文字母,空格以及,.?三种标点符号,请将句子内每个单词进行倒序,并输出倒序后的语句。 输入描述 输入字符串S,S的长度 1 ≤ N…...
PLC 诊断故障的基本原理
(1)东欢坨选煤厂机电设备故障信号主要取自开关量信号,PLC 通过开关量的通和断对设备进行故障诊断。PLC 对开关量的识别是通过输入模块来实现的。PLC 控制设备运行时,设备中的温度、压力、急停、跑偏、速度、过热以及各种按钮和行程开关的传感器与 PLC 输入模块相连接,输入模块的…...
QT打开外部程序并嵌入Qt子窗口的缺点
首先可以参考如下文章: QT打开外部程序并嵌入Qt界面_qt界面嵌入外部应用程序_初学小白Lu的博客-CSDN博客 Qt嵌入外部程序界面初探_qt嵌入其他程序窗口_liming4675的博客-CSDN博客 QT 如何把外部程序嵌入到QT界面_qt嵌入其他程序窗口_hellokandy的博客-CSDN博客 Qt界…...

如何系统地学习 C++ 语言?
C作为具有广泛适用性的编程语言,学习C的人越来越多,但是如何系统地学习C还是个问题,下面我们一起来看一下C学习的方法有哪些吧。 首先,要学习C,最重要的就是掌握C的基础知识。 比如数据结构、算法、微积分等。这些都是…...

【数据结构】单链表
链表1.为什么存在链表2.链表的概念3.单链表的实现4.测试1.为什么存在链表 我们在学习顺序表的时候,了解到顺序表有一定的缺陷:(1)在中间插入数据和删除数据需要挪动数据,时间复杂度是O(N)&…...

Windows 右键菜单扩展容器 [开源]
今天给大家分享一个我做的小工具,可以自定义扩展右键菜单的功能来提高工作效率,效果图如下: 如上图,右键菜单多了几个我自定义的菜单: 复制文件路径 复制文件夹路径 我的工具箱 <走配置文件动态创建子菜单&#x…...

爆文制造机!小红书热榜3个方向,告诉你选题诀窍!
我们知道,不论是达人创作内容,还是品牌制定Brief,都需要提前调研筛选海量信息,这时候如果有一个自己的内容素材库,就省事多啦。按照内容需求,我们可以按3个角度划分小红书内容素材:笔记类型、竞…...

手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...
【位运算】消失的两个数字(hard)
消失的两个数字(hard) 题⽬描述:解法(位运算):Java 算法代码:更简便代码 题⽬链接:⾯试题 17.19. 消失的两个数字 题⽬描述: 给定⼀个数组,包含从 1 到 N 所有…...

LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
【git】把本地更改提交远程新分支feature_g
创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...