正文
在优化APP启动之前,我们首先需要知道,APP启动时究竟发生了什么,才能有的放矢的优化。
APP的启动过程
APP的启动过程就是指,在手机屏幕上点击某个APP的图标,到APP的首页显示在用户面前的过程。
一般APP的启动过程分为一下几步:
- Launcher通知AMS(ActivityManagerService)启动APP
- AMS检查APP是否已经启动,如果已经启动,则直接唤醒即可。否则创建一个新的进程,AMS在新进程中创建一个ActivityThread对象,启动其中的main函数。
- ActivityThread启动bindApplication,bindApplication通过反射创建APP中的Application
- 启动之后通知AMS,AMS再通知APP启动xml中配置的Activity。
- 首页Activity启动后调用onCreate方法,然后加载页面布局,布置屏幕,进行首桢绘制。
从APP的启动过程中看,程序员能优化的地方实际上非常少,主要就是Application的创建过程和首页Activity的绘制过程。
不过在优化之前,我们需要精确的知道APP的启动时间,以及各个方法执行的耗时
精确测量启动耗时
1.使用adb 命令获得启动耗时
例如,测量手机中默认浏览器的启动时长,在Android Studio的控制台运行如下指令即可,
adb shell am start -W com.android.browser/com.android.browser.BrowserActivity
其中com.android.browser和com.android.browser.BrowserActivity分别是浏览器的包名和指定浏览器Activity,实际开发中换成我们自己的包名和首页Activity即可。
运行结果如下
其中
ThisTime:最后一个activity的启动耗时
TotalTime:所有Activity的总启动耗时
WaitTime:AMS启动Activity的总耗时
使用adb命令获取的启动耗时,并不够绝对精确,不能为我们提供优化方向,只能大致反应一个APP的启动耗时。
2.使用埋点的方式获得方法的耗时
埋点的方式有很多中方式,因为埋点不是本文重点,这里只介绍一种最简单也最low的方式。例如,如果需要知道某个方法的执行时间,最简单的方式就是在方法的执行前记录一个时间点,然后在方法执行完毕后,再记录一个时间点,两个时间相减,就可以得出该方法的耗时。
long time;
time = SystemClock.currentThreadTimeMillis();
initAMap();
time = SystemClock.currentThreadTimeMillis() - time;
上述代码就可以得出 initAMap()的耗时了。
实际上,获取一个方法的精确执行耗时,还可以通过Android Studio中各种辅助工具,因为不是本文的重点,同时这些工具也有一定的学习成本,所以这里不再介绍,后续有时间再介绍。
启动优化
1.首页Activity设定不同的Theme
在APP开发中我们都会遇到启动页面白屏的问题,这主要就是,APP启动时会先创建一个空白的window导致的,这会让用户觉得app启动很慢,通过在AndroidManifest.xml中给首页Activity设定一个带有首页图片的主题,即可解决。具体代码,百度即可。需要注意的是,此种方式只能解决肉眼上的延迟,实际上APP的启动速度,并没有任何优化。
2.异步任务
我们公司的APP启动慢,主要是Application中大量的第三方和第一方框架在onCreate中初始化造成的,onCreate中的方法默认是在主线程中执行。既然如此,那就简单了,将这些初始化框架的方法放到子线程运行就可以。
实际执行后发现,app的启动速度确实奇迹般的快了不少,但是小范围上线后,就直接翻车了。
上线之后,后台监控显示部分手机的开屏页会出现空指针异常,调查之后发现,我们的APP开屏也会请求后台的热修复接口,并执行下载任务,在一部分手机,进入开屏页时,网络框架尚未初始化完毕,导致了网络操作出现空指针异常。
处理方式很简单,使用CountDownLatch。Application的onCreate方法是运行在主线程,如果子线程中初始化方法尚未执行完,主线程暂时阻塞不向后执行就可以了。
完整的代码如下:
//cpu 核心数
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));
//最大非核心线程数
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
//空闲时间
private static final int KEEP_ALIVE_SECONDS = 30;
//线程池
public static final Executor THREAD_POOL_EXECUTOR;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "LaunchTask #" + mCount.getAndIncrement());
}
};
//非核心任务线程队列
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
private CountDownLatch countDownLatch = new CountDownLatch(1);
@Override
public void onCreate() {
super.onCreate();
THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
try {
//模拟耗时的初始化操作
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
initPush();
}
});
THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
initHttp();
//initHttp执行完毕
countDownLatch.countDown();
}
});
THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
initX5WebView();
}
});
try {
//在此处等待 initHttp执行完毕
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
3.其他优化技巧
上面已经说到了,APP启动慢的原因主要在于大量的框架需要初始化,处了异步初始化以外,还有一些其他的注意要点:
- 一些重量级且不重要的框架的初始化尽量将其移出Application,可以在用到它的时候再做初始化,具体需要根据业务作出判断。
- SharedPreferences的初始化可以提前到attachBaseContext中。
- 延迟子进程的开启时机,因为子进程会共享主进程的cpu资源。
结尾
总得来说,APP的启动优化是一个优先级并不高的优化方向,相对于内存优化、CPU优化、电量优化、网络优化等等,它对用户的影响并不大,从我个人接触过的APP来看,《掘金》就是一个启动比较慢的APP,从点击图标到看到开屏图片,平均比其他APP要慢上0.5秒左右。所以,一般来说,如果没有特殊要求,APP优化要以其他方向的优化为主。