前言
本章继续上一章的启动优化讲解,主要基于手淘全链路性能优化分析 Android StartUp 启动框架;
有向无环图(DAG)算法
我们首先去 LeetCode 上看一道算法题:课程表
看题目,可能好多算法薄弱的人有点懵逼,转换一下思路,例如:如果你想学习 OKHttp,那么你需要一些其他的知识来辅助你学习 OKhttp
那么你需要学习 java 只是,学习 Socket、Https 和设计模式,然后学习了这些知识之后,来辅助你学习 OKHttp,这样的一个依赖关系;
转换到我们进行启动优化上,我们在 Application 中初始化三方 SDK 的时候,就要考虑相互之间的依赖关系,而不是一股脑的所有 SDK 都放到子线程或者延迟初始化;
这种有依赖,有方向,没有形成回环的结构就是『有向无环图』简称 DAG;
在上面那张依赖图中,剪头我们通常叫做『边』,Socket 有一个箭头指向它,那么它的入度就是 1,OKHttp 有两个箭头指向它,那么它的入度就是 2,倒过来讲就是出度,Java 的出度是 2, OKHttp 的出度是 0;
那么,我们接下来如何通过代码来实现这个『有向无环图』我们可以借助 BFS 或者 DFS,对这两个感兴趣的可以看下这个链接:图文详解 BFS DFS
拓扑排序
拓扑排序是对一个有向图构造拓扑序列的过程;图的拓扑排序不是唯一的;实现拓扑排序的整体思路是:
- 找出图中 0 入度的顶点;
- 依次在图中删除这些顶点,删除后再找出图中 0 入度的顶点;
- 然后在删除…再找出…;
- 直至删除所有顶点,即完成拓扑排序;
Android StartUp 具体实现
首先我们来定一个 StartUp 接口,这个接口用来创建当前任务,获取当前任务依赖了哪些任务,以及当前任务依赖了多少个任务
任务的排序
public interface Startup<T> {
/**
* 创建当前任务
* @param context context
* @return T 任务执行结果,返回值类型
*/
T create(Context context);
/**
* 当前任务依赖了哪些任务,也就是入度数
*
* @return List<Class<? extends StartUp<?>>>
*/
List<Class<? extends Startup<?>>> dependencies();
/**
* 当前任务依赖了多少个任务
* @return int
*/
int getDependenciesCount();
}
然后定义一个抽象类 AndroidStartup 实现 Startup 接口
public abstract class AndroidStartup<T> implements Startup<T> {
/**
* 获取依赖的任务
* @return 依赖的任务
*/
@Override
public List<Class<? extends Startup<?>>> dependencies() {
return null;
}
/**
* 获取依赖的任务数
* @return int
*/
@Override
public int getDependenciesCount() {
List<Class<? extends Startup<?>>> dependencies = dependencies();
return dependencies == null ? 0 : dependencies.size();
}
}
这个抽象类中只实现了 dependencies 和 getDependenciesCount 接口;
然后定义了 5 个 Task;
CommonParamsTaskStartup 依赖 PushTaskStartup;
CuidTaskStartup 依赖 PushTaskStartup;
DeviceIdTaskStartup 依赖 CommonParamsTaskStartup
ImTaskStartup 依赖 DeviceIdTaskStartup、CuidTaskStartup
PushTaskStartup 没有任何依赖;
所以最终的有向无环图如下:
那么,具体是怎么实现这个拓扑排序的呢?我们来看下排序规则:
/**
* 拓扑排序
* @param startupList startupList
* @return StartupSortStore
*/
public static StartupSortStore sort(List<? extends Startup<?>> startupList) {
// 将所有的任务存入到一个map中
Map<Class<? extends Startup>, Startup<?>> startupMap = new HashMap<>();
// 入读表,记录每个任务的依赖数
Map<Class<? extends Startup>, Integer> inDegreeMap = new HashMap<>();
// 记录每个任务的依赖任务
Map<Class<? extends Startup>, List<Class<? extends Startup>>> startupChildrenMap = new HashMap<>();
// 记录入度为 0 的任务
Deque<Class<? extends Startup>> zeroDegree = new ArrayDeque<>();
// 遍历所有的任务
for (Startup<?> startup : startupList) {
startupMap.put(startup.getClass(), startup);
// 获取每个任务的依赖数
int dependenciesCount = startup.getDependenciesCount();
// 存入入度表中
inDegreeMap.put(startup.getClass(), dependenciesCount);
if (dependenciesCount == 0) {
// 如果依赖的任务数为0,则存入0入度表
zeroDegree.offer(startup.getClass());
} else {
// 获取每个任务依赖的任务,一个任务可以依赖多个任务,所以是一个集合
// 例如获取 ImTaskStartup 的 dependencies,包含 DeviceIdTaskStartup CuidTaskStartup
List<Class<? extends Startup<?>>> dependencies = startup.dependencies();
// 遍历依赖的任务
for (Class<? extends Startup<?>> parent : dependencies) {
// parent = DeviceIdTaskStartup | parent = CuidTaskStartup
List<Class<? extends Startup>> children = startupChildrenMap.get(parent);
if (children == null) {
children = new ArrayList<>();
startupChildrenMap.put(parent, children);
}
// 也就是说 DeviceIdTaskStartup 的 child 是 ImTaskStartup
children.add(startup.getClass());
}
}
}
// 依次
List<Startup<?>> result = new ArrayList<>();
// 如果 0 入度表不为空
while (!zeroDegree.isEmpty()) {
Class<? extends Startup> cls = zeroDegree.poll();
Startup<?> startup = startupMap.get(cls);
result.add(startup)
//
if (startupChildrenMap.containsKey(cls)) {
List<Class<? extends Startup>> childStartup = startupChildrenMap.get(cls);
for (Class<? extends Startup> childCls : childStartup) {
Integer integer = inDegreeMap.get(childCls);
inDegreeMap.put(cls, integer - 1);
if (integer - 1 == 0) {
zeroDegree.offer(childCls);
}
}
}
}
StartupSortStore startupSortStore = new StartupSortStore();
startupSortStore.setResult(result);
startupSortStore.setStartupMap(startupMap);
startupSortStore.setStartupChildrenMap(startupChildrenMap);
return startupSortStore;
}
任务的执行
任务排序排好之后,如何按顺序的执行这些任务呢?我们来定义两个任务执行管理类;
public class StartupCacheManager {
/**
* 执行结束的任务集合
*/
private final ConcurrentHashMap<Class<? extends Startup>, Result> mInitializedComponents = new ConcurrentHashMap<>();
/**
* 单例
*/
private static StartupCacheManager mInstance;
/**
* 私有构造
*/
private StartupCacheManager() {}
/**
* 对外暴漏 StartupCacheManager
* @return StartupCacheManager
*/
public static StartupCacheManager getInstance() {
if (mInstance == null) {
synchronized (StartupCacheManager.class) {
if (mInstance == null) {
mInstance = new StartupCacheManager();
}
}
}
return mInstance;
}
/**
* save result of initialized component.
* @param zClass zClass
* @param result result
*/
public void saveInitializedComponent(Class<? extends Startup> zClass, Result result) {
mInitializedComponents.put(zClass, result);
}
/**
* check initialized.
* @param zClass zClass
* @return true
*/
public boolean hadInitialized(Class<? extends Startup> zClass) {
return mInitializedComponents.containsKey(zClass);
}
/**
* 获取 result
* @param zClass zClass
* @param <T> T
* @return Result<T>
*/
public <T> Result<T> obtainInitializedResult(Class<? extends Startup<T>> zClass) {
return mInitializedComponents.get(zClass);
}
/**
* 移除 result
* @param zClass zClass
*/
public void remove(Class<? extends Startup> zClass) {
mInitializedComponents.remove(zClass);
}
/**
* 清空 result
*/
public void clear() {
mInitializedComponents.clear();
}
}
定义一个单例类 StartupCacheManager 用来缓存每一个任务的执行结果;例如,任务2 依赖任务1 的执行结果才能执行自己的任务,那么就需要缓存当前任务的执行结果;
public class StartupManager {
/**
* context
*/
private final Context mContext;
/**
* 任务结合
*/
private final List<AndroidStartup<?>> mStartupList;
/**
* 任务执行结果
*/
private StartupSortStore mStartupSortStore;
/**
* 构造
* @param mContext mContext
* @param mStartupList mStartupList
*/
public StartupManager(Context mContext, List<AndroidStartup<?>> mStartupList) {
this.mContext = mContext;
this.mStartupList = mStartupList;
}
/**
* 启动任务,主线程调用
* @return StartupManager
*/
public StartupManager start() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new RuntimeException("please start task on main thread");
}
// 执行拓扑排序,核心
mStartupSortStore = TopologySort.sort(mStartupList);
for (Startup<?> startup : mStartupSortStore.getResult()) {
Object result = startup.create(mContext);
StartupCacheManager.getInstance().saveInitializedComponent(startup.getClass(), new Result(result));
}
return this;
}
public static class Builder {
private final List<AndroidStartup<?>> startupList = new ArrayList<>();
public Builder addTask(AndroidStartup<?> task) {
startupList.add(task);
return this;
}
public Builder addAllStartup(List<Startup<?>> startups) {
startupList.addAll(startups);
return this;
}
public StartupManager build(Context context) {
return new StartupManager(context, startupList);
}
}
}
线程管理
我们要想完成任务的启动,本质上还是要合理的运用 CPU,那么就有必要把一些任务的启动放到子线程中并行执行;
例如:我们需要把前面定义的 5 个 Task 都要放到子线程中去执行,那么应该怎么实现?
又或者:我们在主线程中启动了 100 个子线程,这些子线程执行完毕之后,主线程才能打印完成,那么应该怎么实现呢?
我们可以借助 join、wait/notify、juc 工具包下的类等等;
但是,假设:A、B 两个线程,A 线程分别执行第一步、第二步、第三步,当 A 线程执行到第二步的时候,执行 B 线程,如何实现?
答案:join 不可以实现,使用 wait 和 notify,但是要 B 线程先 start;
假设:A、B、C 三个线程,A、B 线程执行三步,当 A、B 线程执行完第二步的时候,执行 C 线程,如何实现?
答案:A 、B 两个线程,在执行完第二步之后,调用 CountDownLatch 的 countdown 方法,C 线程调用CountDownLatch 的 await 方法等待被唤醒;
那么我们如何处理任务之间的同步?
那么 CountDowmLatch 如何应用到任务中呢?
public interface Dispatcher {
/**
* 当前任务是否在主线程执行
* @return true 主线程调用
*/
boolean callCreateOnMainThread();
/**
* 主线程是否需要等待该任务执行结束
* @return true 等待主线程结束
*/
boolean waitOnMainThread();
/**
* 等待
*/
void toWait();
/**
* 唤醒
*/
void toNotify();
/**
* 获取当前任务被执行的时候 需要的线程池
* @return Executor
*/
Executor executor();
/**
* 线程优先级
* @return int
*/
int getThreadPriority();
}
然后我们的 Startup 继承这个接口,这样我们的任务就默认是 Dispatch 的实现类了;callCreateOnMainThread 表示当前任务是否在主线程执行,false 则在子线程中执行;定义了 toWait 和 toNotify 方法,这两个方法我们进入 AndroidStartup 中看下:
public abstract class AndroidStartup<T> implements Startup<T> {
/**
* 执行线程调度,给每个任务都创建一个闭锁,对应的状态码是依赖的任务数
*
* 如果状态码是0,那么调用 await 的时候,并不会被阻塞
*/
private final CountDownLatch mWaitDownLatch = new CountDownLatch(getDependenciesCount());
/**
* 唤醒
*/
@Override
public void toNotify() {
mWaitDownLatch.countDown();
}
/**
* 等待
*/
@Override
public void toWait() {
try {
mWaitDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过 CountDownLatch 来控制任务的 wait 和 notify;
然后,我们在任务启动的地方,改造如下:
public class StartupManager {
/**
* 启动任务,主线程调用
* @return StartupManager
*/
public StartupManager start() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new RuntimeException("please start task on main thread");
}
// 执行拓扑排序,核心
mStartupSortStore = TopologySort.sort(mStartupList);
for (Startup<?> startup : mStartupSortStore.getResult()) {
StartupRunnable startupRunnable = new StartupRunnable(this, startup, mContext);
// 定义了一个 StartupRunnable 将所有的任务都放到 runnable 中,如果是在主线程执行,则直接 run,否则加入到线程池中进行分发;
if (startup.callCreateOnMainThread()) {
startupRunnable.run();
} else {
startup.executor().execute(startupRunnable);
}
}
return this;
}
/**
* 通知依赖任务 notify
* @param mStartup mStartup
*/
public void notifyChildren(Startup<?> mStartup) {
if (!mStartup.callCreateOnMainThread() && mStartup.waitOnMainThread()) {
mCountDownLatch.countDown();
}
if (mStartupSortStore.getStartupChildrenMap().containsKey(mStartup.getClass())) {
// 获取到依赖的任务
List<Class<? extends Startup>> childStartupCls = mStartupSortStore.getStartupChildrenMap().get(mStartup.getClass());
for (Class<? extends Startup> childStartupCl : childStartupCls) {
// 通知子任务,父任务已经完成
Startup<?> startup = mStartupSortStore.getStartupMap().get(childStartupCl);
startup.toNotify();
}
}
}
}
StartupRunnable 定义如下:
public class StartupRunnable implements Runnable {
/**
* StartupManager
*/
private final StartupManager mStartupManager;
/**
* Startup
*/
private final Startup<?> mStartup;
/**
* Context
*/
private final Context mContext;
/**
* 构造任务启动器
* @param mStartupManager mStartupManager
* @param mStartup mStartup
* @param mContext mContext
*/
public StartupRunnable(StartupManager mStartupManager, Startup<?> mStartup, Context mContext) {
this.mStartupManager = mStartupManager;
this.mStartup = mStartup;
this.mContext = mContext;
}
/**
* 执行任务
*/
@Override
public void run() {
Process.setThreadPriority(mStartup.getThreadPriority());
// 所有任务执行 run 的时候都先 toWait 下,等待被唤醒,countDown 为 0 的则不会被 wait 而是直接执行;
mStartup.toWait();
Object result = mStartup.create(mContext);
StartupCacheManager.getInstance().saveInitializedComponent(mStartup.getClass(), new Result(result));
mStartupManager.notifyChildren(mStartup);
}
}
如何处理主线程需要等待子线程执行完毕之后才能执行
如果任务5是在子线程,主线程需要等待任务5执行完成,主线程才能继续执行,应该怎么处理?
我们依然是借助于 juc 包下的工具类,StartupManager 中的 Builder 的 build 方法改造如下;
public StartupManager build(Context context) {
AtomicInteger atomicInteger = new AtomicInteger();
for (AndroidStartup<?> startup : startupList) {
// 当前任务在线程调用,并且需要主线程等待其执行结束
if (!startup.callCreateOnMainThread() && startup.waitOnMainThread()) {
atomicInteger.decrementAndGet();
}
}
CountDownLatch countDownLatch = new CountDownLatch(atomicInteger.get());
return new StartupManager(context, startupList, countDownLatch);
}
这里我们就用到了 waitOnMainThread 方法,需要主线程等待当前任务执行完成才能继续往下执行;也就说这个方法只有在 callCreateOnMainThread 返回 false(也就是当前任务是在子线程执行)的时候,才有意义;
然后 StartupManager 中新增一个 await 方法;
/**
* 等待任务 await
*/
public void await() {
try {
mCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
也就说,如果需要等待子线程任务初始化完成,那么就调用一下这个方法;
new StartupManager.Builder()
.addStartup(CommonParamsTaskStartup)
.addStartup(CuidTaskStartup)
.addStartup(DeviceIdTaskStartup)
.addStartup(ImTaskStartup)
.addStartup(PushTaskStartup)
.build(getContext())
.start()
.await();
然后 notifyChild 方法中,countDown 一下
public void notifyChildren(Startup<?> mStartup) {
if (!mStartup.callCreateOnMainThread() && mStartup.waitOnMainThread()) {
mCountDownLatch.countDown();
}
....
}
大批量任务注册实现
当我们的程序中有很多很多的任务需要 addStartup 的时候,我们如果在 Application 中新增一个就要添加一个的话就比较繁琐,那么如何简化它的任务注册呢?能不能实现自动注册呢?
ContentProvider
这里我们可以借助 ContentProvider 来实现(思想来源于:LeakCanary),这也就是为什么 LeakCanary 不需要在 Application 中初始化依然可以使用的原因;
我们来增加一个 ContentProvider
public class StartupProvider extends ContentProvider {
@Override
public boolean onCreate() {
List<Startup<?>> startups = StartupInitializer.discoverAndInitializer(this.getContext(), getClass().getName());
new StartupManager.Builder()
.addAllStartup(startups)
.build(getContext())
.start()
.await();
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
以及 AndroidManifest 中进行注册(按需注册)
<provider
android:authorities="${applicationId}.android_startup"
android:name="com.example.startup.provider.StartupProvider"
android:exported="false">
<!-- 强制需要指定 meta-data 用来指定一个任务-->
<meta-data
android:name="com.example.startup.task.CommonParamsTaskStartup"
android:value="android.startup" />
</provider>
然后解析 meta-data 获取指定的任务,然后就可以获取它依赖的任务,以及它依赖的任务依赖的其他任务;
public class StartupInitializer {
public static String META_VALUE = "android.startup";
public static List<Startup<?>> discoverAndInitializer(Context context,
String providerName) {
try {
Map<Class<? extends Startup>, Startup<?>> startups = new HashMap<>();
//获得manifest contentProvider中的meta-data
ComponentName provider = new ComponentName(context, providerName);
ProviderInfo providerInfo = context.getPackageManager().getProviderInfo(provider, PackageManager.GET_META_DATA);
for (String key : providerInfo.metaData.keySet()) {
String value = providerInfo.metaData.getString(key);
if (TextUtils.equals(META_VALUE, value)) {
Class<?> clazz = Class.forName(key);
if (Startup.class.isAssignableFrom(clazz)) {
doInitialize((Startup<?>) clazz.newInstance(), startups);
}
}
}
List<Startup<?>> result = new ArrayList<>(startups.values());
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static void doInitialize(Startup<?> startup,
Map<Class<? extends Startup>, Startup<?>> startups) throws Exception {
//避免重复 不能使用List
startups.put(startup.getClass(), startup);
if (startup.getDependenciesCount() != 0) {
//遍历父任务
for (Class<? extends Startup<?>> dependency : startup.dependencies()) {
doInitialize(dependency.newInstance(), startups);
}
}
}
}
AndroidManifest 的 provider 中的 meta-data 必须指定最后执行的那个任务,这样才能向前找到所要依赖的任务;
拓扑优化
思考:同步任务阻塞异步任务怎么办?
我们来优化下排序算法:
public static StartupSortStore sort(List<? extends Startup<?>> startupList) {
```
// 依次
List<Startup<?>> result = new ArrayList<>();
// 增加 主线程集合
List<Startup<?>> main = new ArrayList<>();
// 增加 子线程集合
List<Startup<?>> threads = new ArrayList<>();
// 如果 0 入度表不为空
while (!zeroDegree.isEmpty()) {
Class<? extends Startup> cls = zeroDegree.poll();
Startup<?> startup = startupMap.get(cls);
// 分别加入不到不同的集合中
if (startup.callCreateOnMainThread()) {
main.add(startup);
} else {
threads.add(startup);
}
//
if (startupChildrenMap.containsKey(cls)) {
List<Class<? extends Startup>> childStartup = startupChildrenMap.get(cls);
for (Class<? extends Startup> childCls : childStartup) {
Integer integer = inDegreeMap.get(childCls);
inDegreeMap.put(cls, integer - 1);
if (integer - 1 == 0) {
zeroDegree.offer(childCls);
}
}
}
}
// 先添加子线程
result.addAll(threads);
// 再添加主线程
result.addAll(main);
StartupSortStore startupSortStore = new StartupSortStore();
startupSortStore.setResult(result);
startupSortStore.setStartupMap(startupMap);
startupSortStore.setStartupChildrenMap(startupChildrenMap);
return startupSortStore;
}
这样我们最终的排序就是 1、3、4、5 2
1 -> 子线程执行,直接执行
3 -> 子线程执行,等待 1 任务
4 -> 子线程执行,等待 2 任务
5 -> 子线程执行,等待 3、4 任务
2 -> 主线程执行,等待 1 任务
因为 result 先添加的 threads 任务,再添加的 main 任务
好了,启动优化就写到这里吧;
下一章预告
继续性能优化~
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~