Android性能优化----执行时间优化
在了解了启动优化后,Application会做一些初始化的工作,但不要在Application中做耗时操作,然而有些初始化工作可能是很耗时的,那怎么办?初始化操作可以开启子线程来完成。
计算执行时间
- 常规方案(手动埋点标记)
- AOP方式获取
1、常规方案
常规方案就是在执行前埋点标记开始时间,在执行后埋点标记结束时间,然后计算开始时间和结束时间的差值,时间差值就是耗时时间。
具体的耗时计算实现如下代码所示,在 Application 中 onCreate 方法中调用了很多初始化的方法,我们通过手动埋点标记的方式在每一个调用的方法内部都去计算其方法的耗时时间。
//Application.java
@Override
public void onCreate() {
initSharedPreference();
initImageLoader();
initSQLite();
//.....
}
private void initSharedPreference() {
long startTime = System.currentTimeMillis();
//init SharedPreference
Log.d(TAG, "initSharedPreference cost :" + (System.currentTimeMillis() - startTime));
}
private void initImageLoader() {
long startTime = System.currentTimeMillis();
Fresco.initialize(this);
Log.d(TAG, "initImageLoader cost :" + (System.currentTimeMillis() - startTime));
}
private void initSQLite() {
long startTime = System.currentTimeMillis();
//init bugly
Log.d(TAG, "initSQLite cost :" + (System.currentTimeMillis() - startTime));
}
上面的计算方式,是容易想到的实现方式,但缺点也很明显:
- 每个方法都是标记并计算耗时时间,代码也不够优雅。
- 对项目的入侵性很大,工作量大。
2、AOP方式获取
AOP 就是我们常说的
面向切面编程
,它可以针对同一类问题
进行统一处理
。
下面我们就来通过 AOP 的方式来优雅的获取Application每一个方法的执行时间。
2.1引入 AspectJ
在 Android 中通过 AspectJ 这个库来使用 AOP ,接下来来引入这个库:
- 在工程根 build.gradle 中引入 AspectJ 插件
- classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
- 在Module中 build.gradle 应用插件
- apply plugin: 'android-aspectjx'
- 在Module中build.gradle 中引入 aspectj 库
- implementation 'org.aspectj:aspectjrt:1.8.9'
2.2AOP的具体使用
- 定义一个类PerformanceAop使用@Aspect注解
- @Around("execution(* com.lu.aop.MyApplication.**(..))")表示需要对 MyApplication中的每一个方法都做 hook 处理。
- 记录方法执行前后的时间戳,并计算对应的时间差。
@Aspect
public class PerformanceAop {
private static final String TAG = PerformanceAop.class.getSimpleName();
@Around("execution(* com.lu.aop.MyApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
//得到方法的名字,例如:MyApplication.attachBaseContext(..)
String name = signature.toShortString();
//记录方法执行前的时间戳
long startTime = System.currentTimeMillis();
try {
//执行该方法
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
//记录执行该方法的时间
long costTime = System.currentTimeMillis() - startTime;
Log.e(TAG, "method " + name + " cost:" + costTime);
}
}
运行结果
2019-08-18 17:09:12.946 10094-10094/com.lu.aop E/PerformanceAop: method MyApplication.attachBaseContext(..) cost:1
2019-08-18 17:09:12.979 10094-10094/com.lu.aop E/PerformanceAop: method MyApplication.initSQLite() cost:11
2019-08-18 17:09:13.002 10094-10094/com.lu.aop E/PerformanceAop: method MyApplication.initImageLoader() cost:17
2019-08-18 17:09:13.002 10094-10094/com.lu.aop E/PerformanceAop: method MyApplication.onCreate() cost:28
AOP 这种方式是一个比较优雅的方式实现的,它对已有代码是零侵入性的,修改也方便。
用异步执行耗时任务
在 Application 去执行这些第三方库的初始化,是会拖慢整个应用的启动过程的,因此用子线程与主线程并行的方式来分担主线程的工作,从而减少主线程的执行时间。
在子线程中执行任务
我们可能会容易想到以下这两种方式:
- 方式一
public void onCreate(){
new Thread() {
public run() {
//执行任务1
//执行任务2
//执行任务3
}
}.start();
}
- 方式二
public void onCreate(){
new Thread() {
public run() {
//执行任务1
}
}.start();
new Thread() {
public run() {
//执行任务2
}
}.start();
new Thread() {
public run() {
//执行任务3
}
}.start();
}
方式二更加充分地利用 CPU资源,但是直接创建线程还是不够优雅,所以使用线程池来管理这些线程会更好一些。
线程池管理
获取到对应的线程池,但是这个线程个数不能随意填,我们要能充分利用到 CPU 资源
,因此我们可以参考 AsyncTask
它是如何去设置核心线程数
的。
Executors service = Executors.newFixedThreadPool(核心线程个数);
看到AsyncTask源码中了解核心线程数设置
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//CORE_POOL_SIZE 就是核心线程数
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
这样我们就可以设置核心线程数了
//参考AsyncTask来设置线程的个数。
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
在MyApplication中实现异步执行任务:
@Override
public void onCreate() {
super.onCreate();
//参考AsyncTask来设置线程的个数。
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(new Runnable() {
@Override
public void run() {
initSQLite();
}
});
service.submit(new Runnable() {
@Override
public void run() {
initImageLoader();
}
});
}
异步加载的代码执行结果
2019-08-18 19:09:38.022 13948-13948/com.lu.aop E/PerformanceAop: method MyApplication.attachBaseContext(..) cost:1
2019-08-18 19:09:38.062 13948-13948/com.lu.aop E/PerformanceAop: method MyApplication.onCreate() cost:4
2019-08-18 19:09:38.078 13948-13967/com.lu.aop E/PerformanceAop: method MyApplication.initSQLite() cost:9
2019-08-18 19:09:38.094 13948-13968/com.lu.aop E/PerformanceAop: method MyApplication.initImageLoader() cost:15
从Log日志数据对比,可以看出主线程执行的 onCreate 方法的执行时间从原来的 28ms 减到到了 4ms 。所以用子线程执行耗时任务还是相当好的。但是到这里又遇到一个问题,就是有一些方法是必须在 Application onCreate 执行完成之前完成初始化的,因为在 MainActivity 中就需要使用到,那我们上面的异步就会有问题了,那如何解决这个问题呢?
异步任务必须在某一个阶段执行完成
以initImageLoader()为例,不知道Application中的子线程什么时候才完成初始化任务,但是这个时候已经进入了MainActivity了,用到ImageLoader,ImageLoader在Application中还没有完成初始化就用不了,所以得控制ImageLoader在 Application onCreate 执行完成之前完成初始化。这时就需要使用到 CountDownLatch 了。
CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。
- 在Application中定义CountDownLatch
//Application
private CountDownLatch countDownLatch = new CountDownLatch(1);
- 在initImageLoader方法中执行完毕时,执行 countDownLatch.countDown()
private void initImageLoader() {
Fresco.initialize(this);
//try {
//模拟耗时
//Thread.sleep(3000);
//} catch (Exception e) {
// e.printStackTrace();
//}
//Log.e(TAG, "初始化initImageLoader完毕");
//数量减一
countDownLatch.countDown();
}
- 等待 countDownLatch.await()
在 onCreate 方法结束点等待,如果在此处之前之前调用了countDownLatch.countDown(),那么就直接跳过,否则就在此等待。
public void onCreate() {
super.onCreate();
//参考AsyncTask来设置线程的个数。
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(new Runnable() {
@Override
public void run() {
initSQLite();
}
});
service.submit(new Runnable() {
@Override
public void run() {
initImageLoader();
}
});
//在 onCreate 方法中等待,如果在此处之前之前调用了countDownLatch.countDown(),那么就直接跳过,否则就在此等待。
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "Application onCreate 执行完毕");
}
这样,我们的 Application onCreate 方法就会等待异步任务 initImageLoader 执行完毕之后才会结束 onCreate 这个方法的生命周期。
总结
- 了解计算执行任务时间
- 了解AOP面向切面编程知识
- 了解AsyncTask的核心线程数及运用
- 学习了初始化数据时异步优化方案