• 启动, 打开APP的必经之路, 第一体验,关系到用户留存和转化率等核心数据;

启动分析

启动类型

  • Android Vitals可以对应用冷,热,温启动时间做监控。
  • 通过adb shell am start -W ... 命令执行启动并打印启动耗时信息,下面的启动监控中会详细讲解

1. 冷启动

  • 应用从头开始启动,系统进程在冷启动后才创建应用进程
  • 启动流程:Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl
  • 冷启动阶段系统的三个任务:
    1. 加载并启动应用
    2. 显示应用的空白启动窗口
    3. 创建应用进程
  • 应用进程负责后续阶段:
    1. 创建应用对象(Application)
    2. 启动主线程
    3. 创建主Activity
    4. 扩充视图/加载布局
    5. 布局屏幕
    6. 执行初始绘制/首帧绘制
  • 应用进程完成第一次绘制,系统进程就会换掉当前显示的启动窗口,替换为主 Activity。此时,用户可以开始使用应用。

2. 热启动

  • 系统的所有工作就是将Activity带到前台,
  • 只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局膨胀和呈现;

但是,如果一些内存为响应内存整理事件(如 onTrimMemory())而被完全清除,则需要为了响应热启动事件而重新创建相应的对象;

  • 热启动显示的屏幕上行为和冷启动场景相同:在应用完成 Activity 呈现之前,系统进程将显示空白屏幕。

3. 温启动

  • 包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高
  • 场景1:用户在退出应用后又重新启动应用(进程可能存活,通过 onCreate() 从头开始重新创建Activity)
  • 场景2:系统将应用从内存中逐出,然后用户又重新启动(进程和Activity都需要重启,但传递到onCreate()的已保存的实例state bundle对于完成此任务有一定助益)
  • 下面说到的启动一般指冷启动

启动过程

  1. (桌面) 点击响应,应用解析
  2. (系统) 预览窗口显示(根据Theme属性创建,如果Theme中指定为透明,看到的仍然是桌面)
  3. (应用) Application创建, 闪屏页/启动页 Activity创建(一系列的inflateView、onMeasure、onLayout)
  4. (系统) 闪屏显示
  5. (应用) MainActivity创建界面准备
  6. (系统) 主页/首页 显示
  7. (应用) 其他工作(数据的加载,预加载,业务组件初始化)
  8. 窗口可操作

启动问题分析

  • 由启动过程可以推测出用户可能遇到的三个问题

1. 点击桌面图标无响应:

  • 原因:theme中禁用预览窗口或指定了透明背景
//优点:避免启动app时白屏黑屏等现象
//缺点:容易造成点击桌面图标无响应
//(可以配合三方库懒加载,异步初始化等方案使用,减少初始化时长)
//实现如下
//0. appTheme<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"><!-- Customize your theme here. --><item name="colorPrimary">@color/c_ff000000</item><item name="colorPrimaryDark">@color/c_ff000000</item><item name="colorAccent">@color/c_ff000000</item><item name="android:windowActionBar">false</item><item name="android:windowNoTitle">true</item>
</style>
//1. styles.xml中设置
//1.1 禁用预览窗口
<style name="AppTheme.Launcher"><item name="android:windowBackground">@null</item><item name="android:windowDisablePreview">true</item>
</style>
//1.2 指定透明背景
<style name="AppTheme.Launcher"><item name="android:windowBackground">@color/c_00ffffff</item><item name="android:windowIsTranslucent">true</item>
</style>
//2. 为启动页/闪屏页Activity设置theme
<activityandroid:name=".splash.SplashActivity"android:screenOrientation="portrait"android:theme="@style/AppTheme.Launcher"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter>
</activity>
//3. 在该Activity.onCreate()中设置AppTheme(设置布局id之前)
//比如我是基类中单独抽取的获取布局id方法,那么在启动页中重写此方法时加入如下配置:@Override
protected int getContentViewId() {setTheme(R.style.AppTheme_Launcher);return R.layout.activity_splash;
}复制代码

2. 首页显示慢

  • 原因:启动流程复杂,初始化的组件&三方库过多

3. 首页显示后无法操作

  • 原因:同上

启动优化

  • 方法和卡顿优化基本相同,只是启动太过重要,需要更加精打细算;

优化工具

  • Traceview 性能损耗太大,得出的结果并不真实;
  • Nanoscope 非常真实,不过暂时只支持 Nexus 6P 和 x86 模拟器,无法针对中低端机做测试;
  • Simpleperf 的火焰图并不适合做启动流程分析;
  • systrace 可以很方便地追踪关键系统调用的耗时情况,但是不支持应用程序代码的耗时分析;
  • 综合来看,在卡顿优化中提到“systrace + 函数插桩” 似乎是比较理想的方案(可以参考课后作业),拿到整个启动流程的全景图之后,我们可以清楚地看到这段时间内系统、应用各个进程和线程的运行情况;

优化方法

1. 闪屏优化:

  • 预览闪屏(今日头条),预览窗口实现成闪屏效果,高端机上体验非常好,不过低端机上会拉长总的闪屏时长(建议在Android6.0以上才启用此方案);
//优点:避免点击桌面图标无响应
//缺点:拉长总的闪屏时长
//(可以配合三方库懒加载,异步初始化等方案使用,减少初始化时长)
//1. 就是给windowBackground设置一个背景图片
<style name="AppTheme.Launcher"><item name="android:windowBackground">@drawable/bg_splash</item><item name="android:windowFullscreen">true</item>
</style>
//2. bg_splash文件如下(使用layer-list实现)
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"><item android:drawable="@color/color_ToolbarLeftItem" /><item><bitmapandroid:antialias="true"android:gravity="center"android:src="@drawable/ic_splash" /></item>
</layer-list>
//3. 为启动页/闪屏页Activity设置theme
<activityandroid:name=".splash.SplashActivity"android:screenOrientation="portrait"android:theme="@style/AppTheme.Launcher"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter>
</activity>
//4. 在该Activity.onCreate()中设置AppTheme(设置布局id之前)
//比如我是基类中单独抽取的获取布局id方法,那么在启动页中重写此方法时加入如下配置:@Override
protected int getContentViewId() {setTheme(R.style.AppTheme_Launcher);return R.layout.activity_splash;
}
复制代码
  1. 合并闪屏和主页面的Activity(微信),不过违法单一职责原则,业务逻辑比较复杂;

2. 业务梳理

  • 理清启动过程中的模块,哪些需要,哪些可以砍掉,哪些可以懒加载(懒加载要防止集中化,避免首页可见但用户无法操作的情况);
  • 根据业务场景决定不同的启动模式;
  • 对低端机降级,做功能取舍;
  • 启动优化带来整体留存、转化的正向价值,是大于某个业务取消预加载带来的负面影响的;

3. 业务优化

  • 抓大放小,解决主要耗时问题,如优化解密算法;
  • 异步线程预加载,但过度使用会让代码逻辑更加复杂;
  • 偿还技术债,如有必要,择时对老代码进行重构;

4. 线程优化

  • 减少CPU调度带来的波动,让应用的启动时间更加稳定
  1. 控制线程的数量,避免线程太多互争CPU资源,用统一线程池,根据机器性能来控制数量;
  2. 检查线程间的锁,特别是防止主线程出现长时间的空转(主线程因为锁而干等子线程任务);
  //通过sched查看线程切换数据proc/[pid]/sched:nr_voluntary_switches:     主动上下文切换次数,因为线程无法获取所需资源导致上下文切换,最普遍的是IO。    nr_involuntary_switches:   被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占CPU。
复制代码
  • 现在有很多启动框架,使用Pipeline机制,根据业务优先级规定业务初始化时机,如微信的mmkernel,阿里的alpha, 会为任务建立依赖关系,最终形成一个有向无环图;
  • 下面是自定义的一个可以区分多类型任务的线程池工具类,也可以用于异步初始化
//- 注意区分任务类型:
//    - IO密集型任务:不消耗CPU,核心池可以很大,如文件读写,网络请求等。
//    - CPU密集型任务:核心池大小和CPU核心数相关,如复杂的计算,需要使用大量的CPU计算单元。
//
// 执行的任务是CPU密集型
DispatcherExecutor.getCPUExecutor().execute(YourRunable());// 执行的任务是IO密集型
DispatcherExecutor.getIOExecutor().execute(YourRunable());/*** @Author: LiuJinYang* @CreateDate: 2020/12/16* * 实现用于执行多类型任务的基础线程池*/
public class DispatcherExecutor {/*** CPU 密集型任务的线程池*/private static ThreadPoolExecutor sCPUThreadPoolExecutor;/*** IO 密集型任务的线程池*/private static ExecutorService sIOThreadPoolExecutor;/*** 当前设备可以使用的 CPU 核数*/private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();/*** 线程池核心线程数,其数量在2 ~ 5这个区域内*/private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 5));/*** 线程池线程数的最大值:这里指定为了核心线程数的大小*/private static final int MAXIMUM_POOL_SIZE = CORE_POOL_SIZE;/*** 线程池中空闲线程等待工作的超时时间,当线程池中* 线程数量大于corePoolSize(核心线程数量)或* 设置了allowCoreThreadTimeOut(是否允许空闲核心线程超时)时,* 线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。* 否则,线程会永远等待新的工作。*/private static final int KEEP_ALIVE_SECONDS = 5;/*** 创建一个基于链表节点的阻塞队列*/private static final BlockingQueue<Runnable> S_POOL_WORK_QUEUE = new LinkedBlockingQueue<>();/*** 用于创建线程的线程工厂*/private static final DefaultThreadFactory S_THREAD_FACTORY = new DefaultThreadFactory();/*** 线程池执行耗时任务时发生异常所需要做的拒绝执行处理* 注意:一般不会执行到这里*/private static final RejectedExecutionHandler S_HANDLER = new RejectedExecutionHandler() {@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {Executors.newCachedThreadPool().execute(r);}};/*** 获取CPU线程池** @return CPU线程池*/public static ThreadPoolExecutor getCPUExecutor() {return sCPUThreadPoolExecutor;}/*** 获取IO线程池** @return IO线程池*/public static ExecutorService getIOExecutor() {return sIOThreadPoolExecutor;}/*** 实现一个默认的线程工厂*/private static class DefaultThreadFactory implements ThreadFactory {private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);private final ThreadGroup group;private final AtomicInteger threadNumber = new AtomicInteger(1);private final String namePrefix;DefaultThreadFactory() {SecurityManager s = System.getSecurityManager();group = (s != null) ? s.getThreadGroup() :Thread.currentThread().getThreadGroup();namePrefix = "TaskDispatcherPool-" +POOL_NUMBER.getAndIncrement() +"-Thread-";}@Overridepublic Thread newThread(Runnable r) {// 每一个新创建的线程都会分配到线程组group当中Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);if (t.isDaemon()) {// 非守护线程t.setDaemon(false);}// 设置线程优先级if (t.getPriority() != Thread.NORM_PRIORITY) {t.setPriority(Thread.NORM_PRIORITY);}return t;}}static {sCPUThreadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,S_POOL_WORK_QUEUE, S_THREAD_FACTORY, S_HANDLER);// 设置是否允许空闲核心线程超时时,线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。否则,线程会永远等待新的工作。sCPUThreadPoolExecutor.allowCoreThreadTimeOut(true);// IO密集型任务线程池直接采用CachedThreadPool来实现,// 它最多可以分配Integer.MAX_VALUE个非核心线程用来执行任务sIOThreadPoolExecutor = Executors.newCachedThreadPool(S_THREAD_FACTORY);}}
复制代码

5. GC优化

  • 启动过程,要尽量减少GC次数,避免造成主线程长时间的卡顿
  //1. 通过 systrace 单独查看整个启动过程 GC 的时间python systrace.py dalvik -b 90960 -a com.sample.gc//2. 通过Debug.startAllocCounting监控启动过程总GC的耗时情况// GC使用的总耗时,单位是毫秒Debug.getRuntimeStat("art.gc.gc-time");// 阻塞式GC的总耗时Debug.getRuntimeStat("art.gc.blocking-gc-time");//如果发现主线程出现比较多的 GC 同步等待,就需要通过 Allocation 工具做进一步的分析
复制代码
  • 启动过程避免大量字符串操作,序列化和反序列化,减少对象创建(提高服用或移到Native实现);
  • java对象逃逸也很容易引起GC,应保证对象生命周期尽量短,在栈上就进行销毁;

6. 系统调用优化

  • 通过systrace的System Service类型,可以看到启动过程System Server的 CPU 工作情况
  • 启动过程尽量不要做系统调用,如PackageManagerService操作,Binder调用
  • 启动过程也不要过早的拉起应用的其他进程,System Server和新的进程都会竞争CPU资源,内存不足时可能触发系统的low memory killer 机制,导致系统杀死和拉起(保活)大量进程,进而影响前台进程

启动进阶方法

1. IO优化

  • 负载过高时,IO性能下降的会比较快,特别是对低端机;
  • 启动过程不建议出现网络IO
  • 磁盘IO要清楚启动过程读取了什么文件,多少字节,buffer大小,耗时多少,在什么线程等
  • 重度用户是启动优化一定要覆盖的群体,如本地缓存,数据库,SP文件非常多时的耗时
  • 数据结构的选择,如启动时可能只需要sp文件中的几个字段,SharedPreference就需要分开存储,避免解析全部sp数据耗时过长;
  • 启动过程适合使用随机读写的数据结构,可以将ArrayMap改造成支持随机读写、延时解析的数据存储方式。

2. 数据重排

  • Linux 文件 I/O 流程
Linux 文件系统从磁盘读文件的时候,会以 block 为单位去磁盘读取,一般 block
大小是 4KB。也就是说一次磁盘读写大小至少是 4KB,然后会把 4KB 数据放到页缓存Page Cache 中。如果下次读取文件数据已经在页缓存中,那就不会发生真实的磁盘 I/O,而是直接从页缓存中读取,大大提升了读的速度。例如读取文件中1KB数据,因为Buffer不小心写成了 1 byte,总共要读取 1000 次。
那系统是不是真的会读1000次磁盘呢?事实上1000次读操作只是我们发起的次数,
并不是真正的磁盘 I/O 次数,我们虽然读了 1000 次,但事实上只会发生一次磁盘
I/O,其他的数据都会在页缓存中得到。
复制代码

Dex文件用的到的类和安装包APK里面各种资源文件一般都比较小,但是读取非常频繁。 我们可以利用系统这个机制将它们按照读取顺序重新排列,减少真实的磁盘 I/O 次数;

类重排

// 启动过程类加载顺序可以通过复写 ClassLoader 得到
class MyClassLoader extends PathClassLoader {public Class<?> findClass(String name) {//将name记录到文件writeToFile(name,"coldstart_classes.txt");return super.findClass(name);}
}
//然后通过ReDex的Interdex调整类在Dex中的排列顺序,最后可以利用 010 Editor 查看修改后的效果。
复制代码

资源文件重排

  • Facebook 在比较早的时候就使用“资源热图”来实现资源文件的重排
  • 支付宝在《通过安装包重排布优化 Android 端启动性能》中详细讲述了资源重排的原理和落地方法;
  • 实现上都是通过修改 Kernel 源码,单独编译了一个特殊的 ROM,为了便于资源文件统计,重排后实现效果的度量,流程自动化
  • 如果仅仅为了统计,也可以用hook方式
    //Hook,利用 Frida 实现获得 Android 资源加载顺序的方法
    resourceImpl.loadXmlResourceParser.implementation=function(a,b,c,d){send('file:'+a)return this.loadXmlResourceParser(a,b,c,d)
    }
    resourceImpl.loadDrawableForCookie.implementation=function(a,b,c,d,e){send("file:"+a)return this.loadDrawableForCookie(a,b,c,d,e)
    }
    //Frida相对小众,后面会替换其他更加成熟的 Hook 框架
    //调整安装包文件排列需要修改 7zip 源码实现支持传入文件列表顺序,同样最后可以利用 010 Editor 查看修改后的效果;
    复制代码
    • 所谓创新,不一定是创造前所未有的东西。我们将已有的方案移植到新的平台,并且很好地结合该平台的特性将其落地,就是一个很大的创新

3. 类的加载

  • 在加载类的过程有一个 verify class 的步骤,它需要校验方法的每一个指令,是一个比较耗时的操作,可以通过 Hook 来去掉 verify 这个步骤
  • 最大的优化场景在于首次和覆盖安装时
//Dalvik 平台: 将 classVerifyMode 设为 VERIFY_MODE_NONE
// Dalvik Globals.h
gDvm.classVerifyMode = VERIFY_MODE_NONE;
// Art runtime.cc
verify_ = verifier::VerifyMode::kNone;//ART 平台要复杂很多,Hook 需要兼容几个版本
//在安装时大部分 Dex 已经优化好了,去掉 ART 平台的 verify 只会对动态加载的 Dex 带来一些好处
//Atlas 中的dalvik_hack-3.0.0.5.jar可以通过下面的方法去掉 verify
AndroidRuntime runtime = AndroidRuntime.getInstance();
runtime.init(context);
runtime.setVerificationEnabled(false);
//这个黑科技可以大大降低首次启动的速度,代价是对后续运行会产生轻微的影响。同时也要考虑兼容性问题,暂时不建议在 ART 平台使用
复制代码

4. 黑科技

保活:

  • 保活可以减少Application创建跟初始化的时间,让冷启动变成温启动。不过在Target 26之后,保活的确变得越来越难;(大厂一般是厂商合作,例如微信的 Hardcoder 方案和 OPPO 推出的Hyper Boost方案,当应用体量足够大,就可以倒逼厂商去专门为它们做优化)

插件化和熱修復:

  • 事实上大部分的框架在设计上都存在大量的 Hook 和私有 API 调用,带来的缺点主要有两个:
  1. 稳定性/兼容性: 厂商的兼容性、安装失败、dex2oat 失败等,Android P推出的non-sdk-interface调用限制
  2. 性能:Android Runtime 每个版本都有很多的优化,黑科技会导致失效

应用加固:

  • 对启动速度来说简直是灾难,有时候我们需要做一些权衡和选择

GC 抑制:

  • 参考支付宝客户端架构解析-Android 客户端启动速度优化之「垃圾回收」;
  • 允许堆一直增长,直到手动或OOM停止GC抑制

5. MultiDex 优化

apk编译流程/Android Studio 按下编译按钮后发生了什么?

1. 打包资源文件,生成R.java文件(使用工具AAPT)
2. 处理AIDL文件,生成java代码(没有AIDL则忽略)
3. 编译 java 文件,生成对应.class文件(java compiler)
4. .class 文件转换成dex文件(dex)
5. 打包成没有签名的apk(使用工具apkbuilder)
6. 使用签名工具给apk签名(使用工具Jarsigner)
7. 对签名后的.apk文件进行对齐处理,不进行对齐处理不能发布到Google Market(使用工具zipalign)
复制代码

其中第4步,单个dex文件中的方法数不能超过65536,不然编译会报错:Unable to execute dex: method ID not in [0, 0xffff]: 65536, 所以我们项目中一般都会用到multidex:

1. gradle中配置defaultConfig {...multiDexEnabled true}implementation 'androidx.multidex:multidex:2.0.1'2. Application中初始化
@Override
protected void attachBaseContext(Context base) {super.attachBaseContext(base);MultiDex.install(this);
}
复制代码

然鹅,这个multidex过程是比较耗时的,那么能否针对这个问题进行优化呢?

MultiDex优化的两种方案

1. 子线程install(不推荐):

  • 闪屏页开一个子线程去执行MultiDex.install,然后加载完才跳转到主页,
需要注意的是闪屏页的Activity,包括闪屏页中引用到的其它类必须在主dex中,
不然在MultiDex.install之前加载这些不在主dex中的类会报错Class Not Found。
这个可以通过gradle配置,如下:
defaultConfig {//分包,指定某个类在main dexmultiDexEnabled truemultiDexKeepProguard file('multiDexKeep.pro') // 打包到main dex的这些类的混淆规制,没特殊需求就给个空文件multiDexKeepFile file('maindexlist.txt') // 指定哪些类要放到main dex
}
复制代码

2. 今日头条方案

  1. 在主进程Application 的 attachBaseContext 方法中判断如果需要使用MultiDex,则创建一个临时文件,然后开一个进程(LoadDexActivity),显示Loading,异步执行MultiDex.install 逻辑,执行完就删除临时文件并finish自己。
  2. 主进程Application 的 attachBaseContext 进入while代码块,定时轮循临时文件是否被删除,如果被删除,说明MultiDex已经执行完,则跳出循环,继续正常的应用启动流程。
  3. 注意LoadDexActivity 必须要配置在main dex中。
  4. 具体实现参考项目MultiDexTest

6. 预加载优化

1. 类预加载:

  • 在Application中提前异步加载初始化耗时较长的类

2. 页面数据预加载:

  • 在主页空闲时,将其它页面的数据加载好保存到内存或数据库

3. WebView预加载:

  1. WebView第一次创建比较耗时,可以预先创建WebView,提前将其内核初始化;
  2. 使用WebView缓存池,用到WebView的地方都从缓存池取,缓存池中没有缓存再创建,注意内存泄漏问题。
  3. 本地预置html和css,WebView创建的时候先预加载本地html,之后通过js脚本填充内容部分。

4. Activity预创建: (今日头条)

  • Activity对象是在子线程预先new出来,例如在闪屏页等待广告时调用下面代码
DispatcherExecutor.getCPUExecutor().execute(new Runnable() {@Overridepublic void run() {long startTime = System.currentTimeMillis();MainActivity mainActivity = new MainActivity();LjyLogUtil.d( "preNewActivity 耗时: " + (System.currentTimeMillis() - startTime));}
});
复制代码

对象第一次创建的时候,java虚拟机首先检查类对应的Class 对象是否已经加载。如果没有加载,jvm会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。

7. 启动阶段不启动子进程

  • 子进程会共享CPU资源,导致主进程CPU紧张

8. CPU锁频

  • 当下移动设备cpu性能暴增,但一般利用率并不高,我们可以在启动时暴力拉伸CPU频率,来增加启动速度
  • 但是会导致耗电量增加
  • Android系统中,CPU相关的信息存储在/sys/devices/system/cpu目录的文件中,通过对该目录下的特定文件进行写值,实现对CPU频率等状态信息的更改。
- CPU工作模式
performance:最高性能模式,即使系统负载非常低,cpu也在最高频率下运行。
powersave:省电模式,与performance模式相反,cpu始终在最低频率下运行。
ondemand:CPU频率跟随系统负载进行变化。
userspace:可以简单理解为自定义模式,在该模式下可以对频率进行设定。
复制代码

启动监控/耗时检测

logcat

  • Android Studio的logcat中过滤关键字Displayed

adb shell

adb shell am start -W com.ljy.publicdemo.lite/com.ljy.publicdemo.activity.MainActivity
执行结果:
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.ljy.publicdemo.lite/com.ljy.publicdemo.activity.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.ljy.publicdemo.lite/com.ljy.publicdemo.activity.MainActivity
TotalTime: 2065
WaitTime: 2069
Complete
//LaunchState表示冷热温启动
//TotalTime:表示所有Activity启动耗时。(主要数据,包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程)
//WaitTime:表示AMS启动Activity的总耗时。
复制代码

实验室监控

  • 通过定期自动录屏并分析,也适合做竞品的对比测试
  • 如何找到启动结束的点
    • 80%绘制
    • 图像识别
  • 门槛高,适合大厂

线上监控

启动耗时计算的细节:

  • 启动结束的统计时机:使用用户真正可以操作的时间
  • 启动时间的扣除逻辑:闪屏,广告,新手引导的时间都应扣除
  • 启动排除逻辑:Broadcast、Server 拉起,启动过程进入后台等都需排除掉

衡量启动速度快慢的标准

  • 平均启动时间(体验差的用户可能被平均)
  • 快开慢开比,如2秒快开比、5秒慢开比
  • 90%用户的启动时间

区分启动类型:

  • 首次安装启动、覆盖安装启动、冷启动,温启动,热启动
  • 热启动的占比也可以反映出我们程序的活跃或保活能力
除了指标的监控,启动的线上堆栈监控更加困难。Facebook 会利用 Profilo 工具对启动的
整个流程耗时做监控,并且在后台直接对不同的版本做自动化对比,监控新版本是否有新增耗时的函数。
复制代码

对于启动优化要警惕 KPI 化,要解决的不是一个数字,而是用户真正的体验问题。

代码打点(函数插桩),缺点是代码有侵入性较强

/*** @Author: LiuJinYang* @CreateDate: 2020/12/14** 在项目中需要统计时间的地方加入打点, 比如* 应用程序的生命周期节点。* 启动时需要初始化的重要方法,例如数据库初始化,读取本地的一些数据。* 其他耗时的一些算法。*/
public class TimeMonitor {private int mMonitorId = -1;/*** 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间*/private HashMap<String, Long> mTimeTag = new HashMap<>();private long mStartTime = 0;public TimeMonitor(int mMonitorId) {LjyLogUtil.d("init TimeMonitor id: " + mMonitorId);this.mMonitorId = mMonitorId;}public int getMonitorId() {return mMonitorId;}public void startMonitor() {// 每次重新启动都把前面的数据清除,避免统计错误的数据if (mTimeTag.size() > 0) {mTimeTag.clear();}mStartTime = System.currentTimeMillis();}/*** 每打一次点,记录某个tag的耗时*/public void recordingTimeTag(String tag) {// 若保存过相同的tag,先清除if (mTimeTag.get(tag) != null) {mTimeTag.remove(tag);}long time = System.currentTimeMillis() - mStartTime;LjyLogUtil.d(tag + ": " + time);mTimeTag.put(tag, time);}public void end(String tag, boolean writeLog) {recordingTimeTag(tag);end(writeLog);}public void end(boolean writeLog) {if (writeLog) {//写入到本地文件}}public HashMap<String, Long> getTimeTags() {return mTimeTag;}
}
复制代码
  • 耗时统计可能会在多个模块和类中需要打点,所以需要一个单例类来管理各个耗时统计的数据:
/*** @Author: LiuJinYang* @CreateDate: 2020/12/14*/
public class TimeMonitorManager {private HashMap<Integer, TimeMonitor> mTimeMonitorMap;private TimeMonitorManager() {this.mTimeMonitorMap = new HashMap<>();}private static class TimeMonitorManagerHolder {private static TimeMonitorManager mTimeMonitorManager = new TimeMonitorManager();}public static TimeMonitorManager getInstance() {return TimeMonitorManagerHolder.mTimeMonitorManager;}/*** 初始化打点模块*/public void resetTimeMonitor(int id) {if (mTimeMonitorMap.get(id) != null) {mTimeMonitorMap.remove(id);}getTimeMonitor(id).startMonitor();}/*** 获取打点器*/public TimeMonitor getTimeMonitor(int id) {TimeMonitor monitor = mTimeMonitorMap.get(id);if (monitor == null) {monitor = new TimeMonitor(id);mTimeMonitorMap.put(id, monitor);}return monitor;}
}
复制代码

AOP打点,例如统计Application中的所有方法耗

1. 通过AspectJ

//1. 集成aspectj//根目录build.gradle中classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'//app module的build.gradle中apply plugin: 'android-aspectjx'//如果遇到报错Cause: zip file is empty,可添加如下配置android{aspectjx {include 'com.ljy.publicdemo'}}
//2. 创建注解类
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetTime {String tag() default "";
}//3. 使用aspectj解析注解并实现耗时记录
@Aspect
public class AspectHelper {@Around("execution(@GetTime * *(..))")public void getTime(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature joinPointObject = (MethodSignature) joinPoint.getSignature();Method method = joinPointObject.getMethod();boolean flag = method.isAnnotationPresent(GetTime.class);LjyLogUtil.d("flag:"+flag);String tag = null;if (flag) {GetTime getTime = method.getAnnotation(GetTime.class);tag = getTime.tag();}if (TextUtils.isEmpty(tag)) {Signature signature = joinPoint.getSignature();tag = signature.toShortString();}long time = System.currentTimeMillis();try {joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}LjyLogUtil.d( tag+" get time: " + (System.currentTimeMillis() - time));}
}
复制代码

2. 通过Epic三方库

//目前 Epic 支持 Android 5.0 ~ 11 的 Thumb-2/ARM64 指令集,arm32/x86/x86_64/mips/mips64 不支持。
//1. 添加epic依赖
implementation 'me.weishu:epic:0.11.0'
//2. 使用epic
public static class ActivityMethodHook extends XC_MethodHook{private long startTime;@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);startTime = System.currentTimeMillis();}@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);Activity act = (Activity) param.thisObject;String methodName=param.method.getName();LjyLogUtil.d( act.getLocalClassName()+"."+methodName+" get time: " + (System.currentTimeMillis() - startTime));}
}
private void initEpic() {//对所有activity的onCreate执行耗时进行打印DexposedBridge.hookAllMethods(Activity.class, "onCreate", new ActivityMethodHook());
}
//也可以用于锁定线程创建者
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);Thread thread = (Thread) param.thisObject;LjyLogUtil.i("stack " + Log.getStackTraceString(new Throwable()));}
});复制代码

课后作业

  • systrace + 函数插桩
  • 如何在 Dalvik 去掉 verify

参考

  • Android开发高手课-启动优化(上):从启动过程看启动速度优化
  • 初识pipeline
  • 微信Android模块化架构重构实践(mmkernel)
  • Alpha启动框架
  • Android开发高手课-启动优化(下):优化启动速度的进阶方法
  • ReDex: An Android Bytecode Optimizer
  • 010Editor的Template安装与使用
  • 支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能
  • 微信Android热补丁实践演进之路(verify class)
  • Atlas动态组件化(Dynamic Bundle)框架
  • 历时三年研发,OPPO 的 Hyper Boost 引擎如何对系统、游戏和应用实现加速
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • Android Vitals
  • 深入探索Android启动速度优化(上)
  • Epic:一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架
  • 面试官:今日头条启动很快,你觉得可能是做了哪些优化?
  • 开源 | BoostMultiDex:挽救 Android Dalvik 机型APP升级安装体验
  • 今日头条App 页面秒开方案详解
  • 深入探索Android启动速度优化(下)

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

Android高手笔记 - 启动优化相关推荐

  1. Android高手笔记 - IO优化

    IO 优化不就是不在主线程读写大文件吗,真的只有这么简单吗? IO 基础 IO流程:应用程序 发送逻辑IO命令给文件系统,文件系统发送物理IO命令给存储设备/磁盘 文件系统 文件读(read)过程:应 ...

  2. Android高手笔记 - 耗电优化

    耗电的背景知识 电池技术:电池容量,充电时间,寿命,安全性: 电量和硬件:应用程序不会直接去消耗电池,而是通过使用硬件模块消耗相应的电能:CPU.屏幕.WiFi 和数据网络.GPS 以及音视频通话都是 ...

  3. Android高手笔记-D8, R8编译优化

    在之前的文章Android高手笔记-包体积优化中提到过通过编译优化包体积,涉及到了ProGuard,D8,R8,其中关于ProGuard及包体积优化方案已经进行了详细介绍,那么今天我们来说说D8和R8 ...

  4. Android高手笔记-屏幕适配 UI优化

    Android高手笔记-屏幕适配 & UI优化 屏幕与适配 由于Android碎片化严重,屏幕分辨率千奇百怪,而想要在各种分辨率的设备上显示基本一致的效果,适配成本越来越高: 屏幕适配究其根本 ...

  5. Android高手笔记 - 开篇 崩溃优化

    开篇-焦虑的移动开发者如何破局 移动互联网的发展不知不觉已经十多年了,Mobile First 也已经变成了 AI First.换句话说,我们已经不再是"风口上的猪". 可以说,国 ...

  6. android+闪屏启动优化,Android分享笔记(2) APP启动时闪屏

    App在启动时,即在欢迎界面.老是出现白屏或黑屏,闪一下然后才出现欢迎界面. 我欢迎界面原先是这样的:<?xml  version="1.0" encoding=" ...

  7. Android高手笔记 - 网络优化

    很难找到一款完全不需要网络的应用,即使是单机应用,也会存在数据上报.广告等各种各样的网络请求 网络基础 Http & Https 定义: https:Hypertext Transfer Pr ...

  8. Android安装包体积优化

    APK瘦身经验小结_crazy_jack-CSDN博客 最近看滴滴开源的Dokit框架中有一个大图监控的功能,可以对图片的文件大小和所占用的内存大小设置一个阈值,当图片超过该值的时候进行提示. And ...

  9. Linux 启动优化实战-2.41 秒启动应用!

    系统启动是一个大问题,前段时间有同学也问了我这个问题,不仅仅是Linux,Android 下面的启动优化也可以借助bootchar来分析.下面正文是老吴的实操过程. 哦,对了,上篇文章有同学问文章的封 ...

最新文章

  1. 上三角矩阵的特征值分解
  2. Python打包工具Pyintealler打包py文件为windows exe文件过程及踩坑记录+实战例子
  3. dev 点击子控件触发panelcontrol事件_LINUX IIO子系统分析之二 IIO子系统数据结构分析...
  4. 关于IR21的自举电路
  5. 你了解欧拉回路吗?(附Java实现代码)
  6. java中文乱码解决方案
  7. python urllib安装_Python Urllib库
  8. 最短路径 - 迪杰斯特拉(Dijkstra)算法
  9. 3.4 svm人脸识别
  10. 又一个“众所周知”的DAL层设计BUG
  11. h5如何实现贪吃蛇小游戏
  12. 监视浏览器是否打开控制台
  13. VISTA下载全集(下)
  14. 三维地理信息系统空间的可视分析
  15. Solidwork仿真总结(Motion,Xpress、Simulation,Flow Simulaiton)
  16. Windows11 Docker-Compose 因为挂载问题报错
  17. php 微博获取粉丝,新浪API,提取微博账号的信息,粉丝数、微博数等
  18. Linux shell 的ss
  19. 程序员跳槽找工作避坑指南(2019最新新版)
  20. android 实现微信分享添加缩略图的一个大坑

热门文章

  1. canvas画矩形方法
  2. Android中级第十一讲之MotionEvent的分发、拦截机制分析
  3. java中持久化是什么意思?
  4. ASEMI电磁炉整流桥KBPC2510怎么测量好坏
  5. Oracle约数,oracle 约束
  6. DataGridView合并单元格(横向合并)
  7. EXCEL导出单元格出现换行的空格符,去除所有空格符的处理方法
  8. 2022-2028年全球及中国旋转门行业发展现状调研及投资前景分析
  9. Vue-hooks(钩子)
  10. 刚做了一个梦,梦见自己错过了黄金十年