在性能优化的整个知识体系中,最重要的就是稳定性优化,在上一篇文章
《深入探索Android稳定性优化》
我们已经深入探索了Android稳定性优化的疆域。那么,除了稳定性以外,对于性能纬度来说,哪个方面的性能是最重要的呢?毫无疑问,就是 应用的启动速度。下面,就让我们扬起航帆,一起来逐步深入探索Android启动速度优化的奥秘。
如果我们去一家餐厅吃饭,在点餐的时候等了半天都没有服务人员过来,可能就没有耐心等待直接走了。
对于App来说,也是同样如此,如果用户点击App后,App半天都打不开,用户就可能失去耐心卸载应用。
启动速度是用户对我们App的第一体验,打开应用后才能去使用其中提供的强大功能,就算我们应用的内部界面设计的再精美,功能再强大,如果启动速度过慢,用户第一印象就会很差。
因此,拯救App的启动速度,迫在眉睫。
应用启动的类型总共分为如下三种:
下面,我们来详细分析下各个启动类型的特点及流程。
从点击应用图标到UI界面完全显示且用户可操作的全部过程。
耗时最多,衡量标准。
Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl
首先,用户进行了一个点击操作,这个点击事件它会触发一个IPC的操作,之后便会执行到Process的start方法中,这个方法是用于进程创建的,接着,便会执行到ActivityThread的main方法,这个方法可以看做是我们单个App进程的入口,相当于Java进程的main方法,在其中会执行消息循环的创建与主线程Handler的创建,创建完成之后,就会执行到 bindApplication 方法,在这里使用了反射去创建 Application以及调用了 Application相关的生命周期,Application结束之后,便会执行Activity的生命周期,在Activity生命周期结束之后,最后,就会执行到 ViewRootImpl,这时才会进行真正的一个页面的绘制。
直接从后台切换到前台。
启动速度最快。
只会重走Activity的生命周期,而不会重走进程的创建,Application的创建与生命周期等。
较快,介于冷启动和热启动之间的一个速度。
LifeCycle -> ViewRootImpl
它是GUI管理系统与GUI呈现系统之间的桥梁。每一个ViewRootImpl关联一个Window, ViewRootImpl 最终会通过它的setView方法绑定Window所对应的View,并通过其performTraversals方法对View进行布局、测量和绘制。
需要注意的是,这些都是系统的行为,一般情况下我们是无法直接干预的。
通常到了界面首帧绘制完成后,我们就可以认为启动已经结束了。
我们的优化方向就是 Application和Activity的生命周期 这个阶段,因为这个阶段的时机对于我们来说是可控的。
在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。
使用adb shell获取应用的启动时间
// 其中的AppstartActivity全路径可以省略前面的packageName
adb shell am start -W [packageName]/[AppstartActivity全路径]
执行后会得到三个时间:ThisTime、TotalTime和WaitTime,详情如下:
表示最后一个Activity启动耗时。
表示所有Activity启动耗时。
表示AMS启动Activity的总耗时。
一般来说,只需查看得到的TotalTime,即应用的启动时间,其包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程。
可以写一个统计耗时的工具类来记录整个过程的耗时情况。其中需要注意的有:
其代码如下所示:
/**
* 耗时监视器对象,记录整个过程的耗时情况,可以用在很多需要统计的地方,比如Activity的启动耗时和Fragment的启动耗时。
*/
public class TimeMonitor {
private final String TAG = TimeMonitor.class.getSimpleName();
private int mMonitord = -1;
// 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间
private HashMap<String, Long> mTimeTag = new HashMap<>();
private long mStartTime = 0;
public TimeMonitor(int mMonitorId) {
Log.d(TAG, "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;
Log.d(TAG, 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;
}
}
为了使代码更好管理,我们需要定义一个打点配置类,如下所示:
/**
* 打点配置类,用于统计各阶段的耗时,便于代码的维护和管理。
*/
public final class TimeMonitorConfig {
// 应用启动耗时
public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
}
此外,耗时统计可能会在多个模块和类中需要打点,所以需要一个单例类来管理各个耗时统计的数据:
/**
* 采用单例管理各个耗时统计的数据。
*/
public class TimeMonitorManager {
private static TimeMonitorManager mTimeMonitorManager = null;
private HashMap<Integer, TimeMonitor> mTimeMonitorMap = null;
public synchronized static TimeMonitorManager getInstance() {
if (mTimeMonitorManager == null) {
mTimeMonitorManager = new TimeMonitorManager();
}
return mTimeMonitorManager;
}
public TimeMonitorManager() {
this.mTimeMonitorMap = new HashMap<Integer, TimeMonitor>();
}
/**
* 初始化打点模块
*/
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;
}
}
主要在以下几个方面需要打点:
例如,启动时在Application和第一个Activity加入打点统计:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START);
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("Application-onCreate");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate");
super.onCreate(savedInstanceState);
initData();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate-Over");
}
@Override
protected void onStart() {
super.onStart();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("SplashActivity-onStart", false);
}
精确,可带到线上,但是代码有侵入性,修改成本高。
1、在上传数据到服务器时建议根据用户ID的尾号来抽样上报。
2、onWindowFocusChanged只是首帧时间,App启动完成的结束点应该是真实数据展示出来的时候(通常来说都是首帧数据),如列表第一条数据展示,记得使用getViewTreeObserver().addOnPreDrawListener()(在API 16以上可以使用addOnDrawListener),它会把任务延迟到列表显示后再执行,例如,在 Awesome-WanAndroid 项目的主页就有一个RecyclerView实现的列表,启动结束的时间就是列表的首帧时间,也即列表第一条数据展示的时候。这里,我们直接在RecyclerView的适配器ArticleListAdapter的convert(onBindViewHolder)方法中加上如下代码即可:
if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
mHasRecorded = true;
helper.getView(R.id.item_search_pager_group).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
helper.getView(R.id.item_search_pager_group).getViewTreeObserver().removeOnPreDrawListener(this);
LogHelper.i("FeedShow");
return true;
}
});
}
具体的实例代码可在 这里查看。
因为用户看到真实的界面是需要有网络请求返回真实数据的,但是onWindowFocusChanged只是界面绘制的首帧时机,但是列表中的数据是需要从网络中下载得到的,所以应该以列表的首帧数据作为启动结束点。
面向切面编程,通过预编译和运行期动态代理实现程序功能统一维护的一种技术。
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时大大提高了开发效率。
对哪些方法进行拦截,拦截后怎么处理。
类是对物体特征的抽象,切面就是对横切关注点的抽象。
被拦截到的点(方法、字段、构造器)。
对JoinPoint进行拦截的定义。
拦截到JoinPoint后要执行的代码,分为前置、后置、环绕三种类型。
首先,为了在Android使用AOP埋点需要引入AspectJ,在项目根目录的build.gradle下加入:
classpath 'com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0'
然后,在app目录下的build.gradle下加入:
apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'
JoinPoint一般定位在如下位置
使用PointCut对我们指定的连接点进行拦截,通过Advice,就可以拦截到JoinPoint后要执行的代码。Advice通常有以下几种类型:
首先,我们举一个小栗子:
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
...
}
在 execution 中的是一个匹配规则,第一个 * 代表匹配任意的方法返回值,后面的语法代码匹配所有Activity中on开头的方法。
其中execution是处理Join Point的类型,在AspectJx中共有两种类型,如下所示:
@Aspect
public class ApplicationAop {
@Around("call (* com.json.chao.application.BaseApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(TAG, name + " cost" + (System.currentTimeMillis() - time));
}
}
在上述代码中,我们需要注意 不同的Action类型其对应的方法入参是不同的,具体的差异如下所示:
ProceedingPoint不同于JoinPoint,其提供了proceed方法执行目标方法。
使用 Profile 的 CPU 模块可以帮我们快速找到耗时的热点方法,下面,我们来详细来分析一下这个模块。
Trace types 有四种,如下所示。
会记录每个方法的时间、CPU信息。对运行时性能影响较大。
相比于Trace Java Methods会记录每个方法的时间、CPU信息,它会在应用的Java代码执行期间频繁捕获应用的调用堆栈,对运行时性能的影响比较小,能够记录更大的数据区域。
需部署到Android 8.0及以上设备,内部使用simpleperf跟踪应用的native代码,也可以命令行使用simpleperf。
用于显示应用程序在其生命周期中转换不同状态的活动,如用户交互、屏幕旋转事件等。
用于显示应用程序 实时CPU使用率、其它进程实时CPU使用率、应用程序使用的线程总数。
列出应用程序进程中的每个线程,并使用了不同的颜色在其时间轴上指示其活动。
Profile提供的检查跟踪数据窗口有四种,如下所示:
提供函数跟踪数据的图形表示形式。
右键点击 Jump to source 跳转至指定函数。
将具有相同调用方顺序的完全相同的方法收集起来。
看顶层的哪个函数占据的宽度最大(表现为平顶),可能存在性能问题。
我们在查看上面4个跟踪数据的区域时,应该注意右侧的两个时间,如下所示:
主要做热点分析,用来得到以下两种数据:
首先,我们可以定义一个Trace静态工厂类,将Trace.begainSection(),Trace.endSection()封装成i、o方法,然后再在想要分析的方法前后进行插桩即可。
然后,在命令行下执行systrace.py脚本,命令如下所示:
python /Users/quchao/Library/Android/sdk/platform-tools/systrace/systrace.py -t 20 sched gfx view wm am app webview -a "com.wanandroid.json.chao" -o ~/Documents/open-project/systrace_data/wanandroid_start_1.html
具体参数含义如下:
在UIThread一栏可以看到核心的系统方法时间区域和我们自己使用代码插桩捕获的方法时间区域。
其中,Android Framework 里面一些重要的模块都插入了label信息,用户App中也可以添加自定义的Lable。
覆盖高中低端机型不同的场景。
需要准确地统计启动耗时。
是否是使用界面显示且用户真正可以操作的时间作为启动结束时间。
闪屏、广告和新手引导这些时间都应该从启动时间里扣除。
Broadcast、Server拉起,启动过程进入后台都需要排除统计。
一些体验很差的用户很可能被平均了。
如2s快开比,5s慢开比,可以看到有多少比例的用户体验好,多少比例的用户比较糟糕。
如果90%用户的启动时间都小于5s,那么90%区间的启动耗时就是5s。
借鉴Facebook的 profilo 工具原理,对启动整个流程进行耗时监控,在后台对不同的版本做自动化对比,监控新版本是否有新增耗时的函数。
Application、Activity创建以及回调等过程。
使用Activity的windowBackground主题属性预先设置一个启动图片(layer-list实现),在启动后,在Activity的onCreate()方法中的super.onCreate()前再setTheme(R.style.AppTheme)。
按需初始化,特别是针对于一些应用启动时不需要初始化的库,可以等到用时才进行加载。
轮流获取、均分CPU。
优先级高的获取。
设置线程优先级。
它是一种更严格的群组调度策略,主要分为如下两种类型:
由强大的调度器Scheduler集合提供。
不同类型的Scheduler:
特别适合Hook手段,找Hook点:构造函数或者特定方法,如Thread的构造函数。
这里我们直接使用维数的 epic 对Thread进行Hook。在attachBaseContext中调用DexposedBridge.hookAllConstructors方法即可,如下所示:
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
@Override protected void afterHookedMethod(MethodHookParam param)throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
LogUtils.i("stack " + Log.getStackTraceString(new Throwable());
}
);
从log找到线程创建信息,根据堆栈信息跟相关业务方沟通解决方案。
直接依赖线程库,但问题在于线程库更新可能会导致基础库更新。
目前基础线程池组件位于启动器sdk之中,使用非常简单,示例代码如下所示:
// 如果当前执行的任务是CPU密集型任务,则从基础线程池组件
// DispatcherExecutor中获取到用于执行 CPU 密集型任务的线程池
DispatcherExecutor.getCPUExecutor().execute(YourRunable());
// 如果当前执行的任务是IO密集型任务,则从基础线程池组件
// DispatcherExecutor中获取到用于执行 IO 密集型任务的线程池
DispatcherExecutor.getIOExecutor().execute(YourRunable());
具体的实现源码也比较简单,并且我对每一处代码都进行了详细的解释,就不一一具体分析了。代码如下所示:
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);
}
}
项目发展阶段忽视基础设施建设,没有采用统一的线程池,导致线程数量过多。
异步任务执行太耗时,导致主线程卡顿。
子线程分担主线程任务,并行减少时间。
异步启动器源码及使用demo地址
充分利用CPU多核,自动梳理任务顺序。
启动器的流程图如下所示:
启动器的主题流程为上图中的中间区域,即主线程与并发两个区域块。需要注意的是,在上图中的 head task与tail task 并不包含在启动器的主题流程中,它仅仅是用于处理启动前/启动后的一些通用任务,例如我们可以在head task中做一些获取通用信息的操作,在tail task可以做一些log输出、数据上报等操作。
那么,这里我们总结一下启动的核心流程,如下所示:
下面,我们就来使用异步启动器来在Application的onCreate方法中进行异步优化,代码如下所示:
// 1、启动器初始化
TaskDispatcher.init(this);
// 2、创建启动器实例,这里每次获取的都是新对象
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
// 3、给启动器配置一系列的(异步/非异步)初始化任务并启动启动器
dispatcher
.addTask(new InitAMapTask())
.addTask(new InitStethoTask())
.addTask(new InitWeexTask())
.addTask(new InitBuglyTask())
.addTask(new InitFrescoTask())
.addTask(new InitJPushTask())
.addTask(new InitUmengTask())
.addTask(new GetDeviceIdTask())
.start();
// 4、需要等待微信SDK初始化完成,程序才能往下执行
dispatcher.await();
这里的 TaskDispatcher 就是我们的启动器调用类。首先,在注释1处,我们需要先调用TaskDispatcher的init方法进行启动器的初始化,其源码如下所示:
public static void init(Context context) {
if (context != null) {
sContext = context;
sHasInit = true;
sIsMainProcess = Utils.isMainProcess(sContext);
}
}
可以看到,仅仅是初始化了几个基础字段。接着,在注释2处,我们创建了启动器实例,其源码如下所示:
/**
* 注意:这里我们每次获取的都是新对象
*/
public static TaskDispatcher createInstance() {
if (!sHasInit) {
throw new RuntimeException("must call TaskDispatcher.init first");
}
return new TaskDispatcher();
}
在createInstance方法的中我们每次都会创建一个新的TaskDispatcher实例。然后,在注释3处,我们给启动器配置了一系列的初始化任务并启动启动器,需要注意的是,这里的Task既可以是用于执行异步任务(子线程)的也可以是用于执行非异步任务(主线程)。下面,我们来分析下这两种Task的用法,比如InitStethoTask这个异步任务的初始化,代码如下所示:
/**
* 异步的Task
*/
public class InitStethoTask extends Task {
@Override
public void run() {
Stetho.initializeWithDefaults(mContext);
}
}
这里的InitStethoTask直接继承自Task,Task中的runOnMainThread方法返回为false,说明 task 是用于处理异步任务的task,其中的run方法就是Runnable的run方法。下面,我们再看看另一个用于初始化非异步任务的例子,例如用于微信SDK初始化的InitWeexTask,代码如下所示:
/**
* 主线程执行的task
*/
public class InitWeexTask extends MainTask {
@Override
public boolean needWait() {
return true;
}
@Override
public void run() {
InitConfig config = new InitConfig.Builder().build();
WXSDKEngine.initialize((Application) mContext, config);
}
}
可以看到,它直接继承了MainTask,MainTask的源码如下所示:
public abstract class MainTask extends Task {
@Override
public boolean runOnMainThread() {
return true;
}
}
MainTask 直接继承了Task,并仅仅是重写了runOnMainThread方法返回了true,说明它就是用来初始化主线程中的非异步任务的。
此外,我们注意到InitWeexTask中还重写了一个needWait方法并返回了true,其目的是为了在某个时刻之前必须等待InitWeexTask初始化完成程序才能继续往下执行,这里的某个时刻指的就是我们在Application的onCreate方法中的注释4处的代码所执行的地方:dispatcher.await(),其实现源码如下所示:
/**
* 需要等待的任务数
*/
private AtomicInteger mNeedWaitCount = new AtomicInteger();
/**
* 调用了 await 还没结束且需要等待的任务列表
*/
private List<Task> mNeedWaitTasks = new ArrayList<>();
private CountDownLatch mCountDownLatch;
private static final int WAITTIME = 10000;
@UiThread
public void await() {
try {
// 1、仅仅在测试阶段才输出需等待的任务列表数与任务名称
if (DispatcherLog.isDebug()) {
DispatcherLog.i("still has " + mNeedWaitCount.get());
for (Task task : mNeedWaitTasks) {
DispatcherLog.i("needWait: " + task.getClass().getSimpleName());
}
}
// 2、只要还有需要等待的任务没有执行完成,就调用mCountDownLatch的await方法进行等待,这里我们设定超时时间为10s
if (mNeedWaitCount.get() > 0) {
if (mCountDownLatch == null) {
throw new RuntimeException("You have to call start() before call await()");
}
mCountDownLatch.await(WAITTIME, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException e) {
}
}
首先,在注释1处,我们仅仅只会在测试阶段才会输出需等待的任务列表数与任务名称。然后,在注释2处,只要需要等待的任务数mNeedWaitCount大于0,即只要还有需要等待的任务没有执行完成,就调用mCountDownLatch的await方法进行等待,注意我们这里设定了超时时间为10s。当一个task执行完成后,无论它是异步还是非异步的,最终都会执行到mTaskDispatcher的markTaskDone(mTask)方法,我们看看它的实现源码,如下所示:
/**
* 已经结束的Task
*/
private volatile List<Class<? extends Task>> mFinishedTasks = new ArrayList<>(100);
public void markTaskDone(Task task) {
if (ifNeedWait(task)) {
mFinishedTasks.add(task.getClass());
mNeedWaitTasks.remove(task);
mCountDownLatch.countDown();
mNeedWaitCount.getAndDecrement();
}
}
可以看到,这里每执行完成一个task,就会将mCountDownLatch的锁计数减1,与此同时,也会将我们的mNeedWaitCount这个原子整数包装类的数量减1。
此外,我们在前面说到了启动器将各个任务之间的依赖关系抽象成了一个有向无环图,在上面一系列的初始化代码中,InitJPushTask是需要依赖于GetDeviceIdTask的,那么,我们怎么告诉启动器它们两者之间的依赖关系呢?
这里只需要在InitJPushTask中重写dependsOn()方法,并返回包含GetDeviceIdTask的task列表即可,代码如下所示:
/**
* InitJPushTask 需要在 getDeviceId 之后执行
*/
public class InitJPushTask extends Task {
@Override
public List<Class<? extends Task>> dependsOn() {
List<Class<? extends Task>> task = new ArrayList<>();
task.add(GetDeviceIdTask.class);
return task;
}
@Override
public void run() {
JPushInterface.init(mContext);
MyApplication app = (MyApplication) mContext;
JPushInterface.setAlias(mContext, 0, app.getDeviceId());
}
}
至此,我们的异步启动器就分析完毕了。下面我们来看看如何高效地进行延迟初始化。
延迟启动器源码及使用demo地址
利用IdleHandler特性,在CPU空闲时执行,对延迟任务进行分批初始化。
延迟初始化启动器的代码很简单,如下所示:
/**
* 延迟初始化分发器
*/
public class DelayInitDispatcher {
private Queue<Task> mDelayTasks = new LinkedList<>();
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 分批执行的好处在于每一个task占用主线程的时间相对
// 来说很短暂,并且此时CPU是空闲的,这些能更有效地避免UI卡顿
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll();
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty();
}
};
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
在DelayInitDispatcher中,我们提供了mDelayTasks队列用于将每一个task添加进来,使用者只需调用addTask方法即可。当CPU空闲时,mIdleHandler便会回调自身的queueIdle方法,这个时候我们可以将task一个一个地拿出来并执行。这种分批执行的好处在于每一个task占用主线程的时间相对来说很短暂,并且此时CPU是空闲的,这样能更有效地避免UI卡顿,真正地提升用户的体验。
至于使用就非常简单了,我们可以直接利用SplashActivity的广告页停留时间去进行延迟初始化,代码如下所示:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
GlobalHandler.getInstance().getHandler().post((Runnable) () -> {
if (hasFocus) {
DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
delayInitDispatcher.addTask(new InitOtherTask())
.start();
}
});
}
需要注意的是,能异步的task我们会优先使用异步启动器在Application的onCreate方法中加载(或者是必须在Application的onCreate方法完成前必须执行完的非异task务),对于不能异步的task,我们可以利用延迟启动器进行加载。如果任务可以到用时再加载,可以使用懒加载的方式。
我们都知道,安装或者升级后首次 MultiDex 花费的时间过于漫长,我们需要进行Multidex的预加载优化。
Multidex优化Demo地址
5.0以上默认使用ART,在安装时已将Class.dex转换为oat文件了,无需优化,所以应判断只有在主进程及SDK 5.0以下才进行Multidex的预加载。
主要包括inline以及quick指令的优化。
使编译器在函数调用处用函数体代码代替函数调用指令。
函数调用的转移操作有一定的时间和空间方面的开销,特别是对于一些函数体不大且频繁调用的函数,解决其效率问题更为重要,引入inline函数就是为了解决这一问题。
inline函数至少在三个方面提升了程序的时间性能:
为了彻底解决MutiDex加载时间慢的问题,抖音团队深入挖掘了 Dalvik 虚拟机的底层系统机制,对 DEX 相关的处理逻辑进行了重新设计与优化,并推出了 BoostMultiDex 方案,它能够减少 80% 以上的黑屏等待时间,挽救低版本 Android 用户的升级安装体验。
具体的实现原理为:在第一次启动的时候,直接加载没有经过 OPT 优化的原始 DEX,先使得 APP 能够正常启动。然后在后台启动一个单独进程,慢慢地做完 DEX 的 OPT 工作,尽可能避免影响到前台 APP 的正常使用。绕过 ODEX 直接加载 DEX 的方案如下:
补充:getDex 会抛出异常,原因是 memMap 需要被赋值,但是 Dalvik_dalvik_system_DexFile_openDexFile_bytearray 这个函数没有这个操作。分析代码后,我们发现,只要 dex_object 对象不为空,就会直接返回,不会再往下执行到取 memMap 的地方。因此,我们在加载完 DEX 数组之后,可以自己生成一个dex_object对象,并注入pDvmDex里面。
如有兴趣的同学可以看看这篇文章:抖音BoostMultiDex优化实践:Android低版本上APP首次启动时间减少80%
在Application中提前异步加载初始化耗时较长的类。
替换系统的ClassLoader,打印类加载的时间,按需选取需要异步加载的类。
在主页空闲时,将其它页面的数据加载好保存到内存或数据库,等到打开该页面时,判断已经预加载过,就直接从内存或数据库取数据并显示。
子进程会共享CPU资源,导致主进程CPU紧张。此外,在多进程情况下一定要可以在onCreate中去区分进程做一些初始化工作。
App onCreate之前是ContentProvider初始化。
关于布局与绘制优化可以参考Android性能优化之绘制优化。
1、Android开发高手课之启动优化
2、支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
3、支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能
4、Facebook Redex字节码优化工具
5、微信Android热补丁实践演进之路
6、安卓App热补丁动态修复技术介绍
7、Dalvik Optimization and Verification With dexopt
8、微信在Github开源了Hardcoder,对Android开发者有什么影响?
9、历时三年研发,OPPO 的 Hyper Boost 引擎如何对系统、游戏和应用实现加速?
10、抱歉,Xposed真的可以为所欲为
11、墙上时钟时间 ,用户cpu时间 ,系统cpu时间的理解
12、《Android应用性能优化最佳实践》
13、必知必会 | Android 性能优化的方面方面都在这儿
14、极客时间之Top团队大牛带你玩转Android性能分析与优化
15、启动器源码
16、MultiDex优化源码
17、使用gradle自动化增加Trace Tag
转载:https://juejin.cn/post/6844904093786308622