网络知识 娱乐 国庆在家,从0手撸一个依赖任务加载框架(有源码)

国庆在家,从0手撸一个依赖任务加载框架(有源码)

/ 前言 /

我收回标题上的话,从0手撸一个框架一点也不轻松,需要考虑的地方比较多,一些实现和细节值得商榷,是一个比较大的挑战,有不足的地方欢迎大佬们提供意见

/ 依赖任务加载 /

平时我们常常会使用各种第三方框架,如mmkv、glide、leakcanary等优秀的第三方库,大多数第三方库需要初始化后才能使用,因此会出现下面的代码:

private void init {

mmkv.init(context);

glide.init(context);

leakcanary.init(context);

......

}

如果不想让任务的初始化阻塞主线程太久,我们可以考虑通过异步的方式加载任务,直到最后一个必要任务加载完毕,开始进行对应的操作。

如果部分任务是依赖关系,如下图任务A依赖任务B,单纯异步的方式的方式显然不能满足述求。

我们通常会想到的解决办法有三类:

  • 将任务B写进任务A的末尾

  • 监听任务A加载成功的回调函数执行任务B

  • 通过volatile关键字卡住加载流程

这样确实能够解决依赖任务的加载问题,但如果任务的数量和依赖关系更复杂呢?

那如果是这样,你要怎么去处理?

显然是有一种更通用的方法来解决这种场景,也就是下面会讲到的有向无环图。

/ 有向无环图的拓扑排序 /

上面的依赖关系可以看成一种有向无环图(Directed Acyclic Graph, DAG),有向可以理解,表现的是任务的依赖关系,而无环是必要的,因为如果任务A和任务B相互依赖,都需要等待对方的结束来开始,经典死锁套娃。

我们可以通过拓扑排序将最后的线性执行关系呈现出来,什么是拓扑排序?

将上面复杂依赖任务简单的分析一下,任务A前方没有依赖,因此我们可以将任务A的度记为0,任务B、C、E前方各有一个依赖关系,我们把度记为1,剩下的任务D前方由于有两个依赖关系,我们将度计为2;用一个任务队列储存度为0的任务,每当入列任务加载完毕,它对应依赖任务的度-1,新的度为0的任务进队列。

  • A入队列,A任务完成后,依赖A任务的任务B、C度-1。

  • 这时任务B、C度都为0,都可以入队列,没有既定的顺序,我们选择入任务C,待C任务完成后,依赖C任务的D任务的度-1。

  • 接着是任务B进去,B任务完成后,任务D、E的度-1。

  • 最后是任务D、E其中的一个进去,随意选择,我们选择任务D。

  • 最后一个任务E。

不考虑各个任务之间的耗时情况,依赖任务关系被拓扑排序成A->C->B->D->E,是不是发现依赖关系被解开了,排成了线性关系,这种将有向无环图拓扑成线性关系的方式被称为拓扑排序,拓扑结果根据所使用算法的不同而有所差异,这也是后面实现依赖任务加载框架的中心思想。

/ 手撸依赖任务加载框架 /

定义IDAGTask类

上面提到依赖任务的加载可以通过有向无环图的拓扑排序解决,我们开始用代码实现,先定义一个IDAGTask类:

public class IDAGTask{

}

可能大家会疑问,为什么不用接口或者抽象类的思想去做这个基础类,后面解答这个疑惑。

特殊的任务会存在加载线程限制,比如只能在主线程对这个任务进行加载,因此我们需要考虑这个任务是否可以同步。异步任务显然需要使用到线程池,定义IDAGTask类实现Runnable接口,方便后续丢进线程池。

除此之外,之前讲到拓扑排序中任务有个度的概念,其实就是依赖关系的数量,在并发环境下为了保证依赖关系数量的线程可见性,这里我们使用AtomicInteger变量,通过CAS锁来保证依赖数量的实时正确性,因此IDAGTask类变成了这样:

public class IDAGTask implements Runnable {

private final boolean mIsSyn;

private final AtomicInteger mAtomicInteger;

boolean getIsAsync {

return mIsSyn;

}

void addRely {

mAtomicInteger.incrementAndGet;

}

void deleteRely {

mAtomicInteger.decrementAndGet;

}

int getRely {

return mAtomicInteger.get;

}

@Override

public void run {

}

}

回到之前为什么不用接口或者抽象类的方式来实现这个基础类,一方面为了后续将任务丢进线程池,IDAGTask实现了Runnable接口,接口的方式显然pass,另一方面抽象类的方式涉及到了另一个问题:

  • 抽象run方法,可以将IDAGTask任务的监听封装进去,比如startTask、completeDAGTask,如果我们继承IDATask,只需要将初始化部分单纯写进run方法就好了,非常优雅,但是有一种case,如果这个任务的初始化是用多线程实现的,我们调用完Task.init,马上执行completeDAGTask的监听其实是不对的

  • 基于上面的case,我选择了一种不优雅的实现方式,将startTask的监听写在run方法的开头,completeDAGTask的监听需要调用者自己添加,任务初始化是单线程实现写在run方法的末尾即可,任务初始化采用多线程实现,需要将completeDAGTask监听写进加载成功回调

  • 综上,run方法写进了startTask的回调,因此抽象失败,那么IDAGTask没有抽象方法,自然也不需要作为一个抽象类

经过一些加工,最后IDATask实现如下:

public class IDAGTask implements Runnable {

private final boolean mIsSyn;

private final AtomicInteger mAtomicInteger;

private IDAGCallBack mDAGCallBack;

private final Set<IDAGTask> mNextTaskSet;

public IDAGTask {

this("");

}

public IDAGTask(boolean isSyn) {

this("