启动类型
1. 冷启动
2. 热启动
但是,如果一些内存为响应内存整理事件(如 onTrimMemory())而被完全清除,则需要为了响应热启动事件而重新创建相应的对象;
3. 温启动
1. 点击桌面图标无响应:
//优点:避免启动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
<activity
android: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. 首页显示后无法操作
1. 闪屏优化:
//优点:避免点击桌面图标无响应
//缺点:拉长总的闪屏时长
//(可以配合三方库懒加载,异步初始化等方案使用,减少初始化时长)
//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>
<bitmap
android:antialias="true"
android:gravity="center"
android:src="@drawable/ic_splash" />
</item>
</layer-list>
//3. 为启动页/闪屏页Activity设置theme
<activity
android: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;
}
复制代码
2. 业务梳理
3. 业务优化
4. 线程优化
//通过sched查看线程切换数据
proc/[pid]/sched:
nr_voluntary_switches:
主动上下文切换次数,因为线程无法获取所需资源导致上下文切换,最普遍的是IO。
nr_involuntary_switches:
被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占CPU。
复制代码
//- 注意区分任务类型:
// - 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() {
@Override
public 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-";
}
@Override
public 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优化
//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 工具做进一步的分析
复制代码
6. 系统调用优化
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 查看修改后的效果。
复制代码
资源文件重排
//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 查看修改后的效果;
复制代码
//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 平台使用
复制代码
保活:
插件化和熱修復:
应用加固:
GC 抑制:
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(不推荐):
需要注意的是闪屏页的Activity,包括闪屏页中引用到的其它类必须在主dex中,
不然在MultiDex.install之前加载这些不在主dex中的类会报错Class Not Found。
这个可以通过gradle配置,如下:
defaultConfig {
//分包,指定某个类在main dex
multiDexEnabled true
multiDexKeepProguard file('multiDexKeep.pro') // 打包到main dex的这些类的混淆规制,没特殊需求就给个空文件
multiDexKeepFile file('maindexlist.txt') // 指定哪些类要放到main dex
}
复制代码
2. 今日头条方案
1. 类预加载:
2. 页面数据预加载:
3. WebView预加载:
4. Activity预创建: (今日头条)
DispatcherExecutor.getCPUExecutor().execute(new Runnable() {
@Override
public void run() {
long startTime = System.currentTimeMillis();
MainActivity mainActivity = new MainActivity();
LjyLogUtil.d( "preNewActivity 耗时: " + (System.currentTimeMillis() - startTime));
}
});
复制代码
对象第一次创建的时候,java虚拟机首先检查类对应的Class 对象是否已经加载。如果没有加载,jvm会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。
- CPU工作模式
performance:最高性能模式,即使系统负载非常低,cpu也在最高频率下运行。
powersave:省电模式,与performance模式相反,cpu始终在最低频率下运行。
ondemand:CPU频率跟随系统负载进行变化。
userspace:可以简单理解为自定义模式,在该模式下可以对频率进行设定。
复制代码
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的总耗时。
复制代码
启动耗时计算的细节:
衡量启动速度快慢的标准
区分启动类型:
除了指标的监控,启动的线上堆栈监控更加困难。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;
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
startTime = System.currentTimeMillis();
}
@Override
protected 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() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
LjyLogUtil.i("stack " + Log.getStackTraceString(new Throwable()));
}
});
复制代码
我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章