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

/ 前言 /

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

/ 依赖任务加载 /

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

private void init {

mmkv.init(context);

glide.init(context);

leakcanary.init(context);

......

}

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

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

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

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

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

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

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

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

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

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

不考虑各个任务之间的耗时情况,依赖任务关系被拓扑排序成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,另一方面抽象类的方式涉及到了另一个问题:

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

public class IDAGTask implements Runnable {

private final boolean mIsSyn;

private final AtomicInteger mAtomicInteger;

private IDAGCallBack mDAGCallBack;

private final Set mNextTaskSet;

public IDAGTask {

this("");

}

public IDAGTask(boolean isSyn) {

this("", isSyn);

}

public IDAGTask(String alias) {

this(alias, false);

}

public IDAGTask(String alias, boolean IsSyn) {

mIsSyn = IsSyn;

mAtomicInteger = new AtomicInteger;

mDAGCallBack = new DAGCallBack(alias);

mNextTaskSet = new HashSet<>;

}

boolean getIsAsync {

return mIsSyn;

}

void addRely {

mAtomicInteger.incrementAndGet;

}

void deleteRely {

mAtomicInteger.decrementAndGet;

}

int getRely {

return mAtomicInteger.get;

}

void addNextDAGTask(IDAGTask DAGTask) {

mNextTaskSet.add(DAGTask);

}

public void setDAGCallBack(IDAGCallBack DAGCallBack) {

this.mDAGCallBack = DAGCallBack;

}

public void completeDAGTask {

for (IDAGTask DAGTask : mNextTaskSet) {

DAGTask.deleteRely;

}

mDAGCallBack.onCompleteDAGTask;

}

@Override

public void run {

mDAGCallBack.onStartDAGTask;

}

}

定义DAGProject类

IDAGTask的模板就被敲定了,接下来我们需要建立任务之间的关系:

于是DAGProject实现如下:

public class DAGProject {

private final Set mTaskSet;

private final Map> mTaskMap;

public DAGProject(Builder builder) {

mTaskSet = builder.mTaskSet;

mTaskMap = builder.mTaskMap;

}

Set getDAGTaskSet {

return mTaskSet;

}

Map> getDAGTaskMap {

return mTaskMap;

}

public static class Builder {

private final Set mTaskSet = new HashSet<>;

private final Map> mTaskMap = new HashMap<>;

public Builder addDAGTask(IDAGTask DAGTask) {

if (this.mTaskSet.contains(DAGTask)) {

throw new IllegalArgumentException;

}

this.mTaskSet.add(DAGTask);

return this;

}

public Builder addDAGEdge(IDAGTask DAGTask, IDAGTask preDAGTask) {

if (!this.mTaskSet.contains(DAGTask) || !this.mTaskSet.contains(preDAGTask)) {

throw new IllegalArgumentException;

}

Set preDAGTaskSet = this.mTaskMap.get(DAGTask);

if (preDAGTaskSet == ) {

preDAGTaskSet = new HashSet<>;

this.mTaskMap.put(DAGTask, preDAGTaskSet);

}

if (preDAGTaskSet.contains(preDAGTask)) {

throw new IllegalArgumentException;

}

DAGTask.addRely;

preDAGTaskSet.add(preDAGTask);

preDAGTask.addNextDAGTask(DAGTask);

return this;

}

public DAGProject builder {

return new DAGProject(this);

}

}

}

使用时,我们需要创建对应的IDAGTask,通过addDAGTask、addDAGEdge方法构建出对应有向无环图:

ATask a = new ATask;

BTask b = new BTask;

CTask c = new CTask;

DTask d = new DTask;

ETask e = new ETask;

DAGProject dagProject = new DAGProject.Builder

.addDAGTask(a)

.addDAGTask(b)

.addDAGTask(c)

.addDAGTask(e)

.addDAGTask(d)

.addDAGEdge(b, a)

.addDAGEdge(c, a)

.addDAGEdge(d, b)

.addDAGEdge(d, c)

.addDAGEdge(e, b)

.builder;

表达任务依赖关系的DAGProject对象就通过建造者模式构建成功了。

依赖任务加载的调度

当多个任务构建成有向无环图的DAGProject后,我们先不着急丢进线程池,执行对应逻辑前先检测是否有环,这样我们可以在任务加载前抛出相互依赖的错误,大可不必等到执行至有环那一步才抛出。虽然有环可以靠输入者去保障,但是在一些小细节方面,我们要求输入者保证过于苛刻也过于差体验。

public class DAGScheduler {

private void checkCircle(Set TaskSet, Map> TaskMap) {

LinkedList resultTaskQueue = new LinkedList<>;

LinkedList tempTaskQueue = new LinkedList<>;

for (IDAGTask DAGTask : tempTaskSet) {

if (tempTaskMap.get(DAGTask) == ) {

tempTaskQueue.add(DAGTask);

}

}

while (!tempTaskQueue.isEmpty) {

IDAGTask tempDAGTask = tempTaskQueue.pop;

resultTaskQueue.add(tempDAGTask);

for (IDAGTask DAGTask : tempTaskMap.keySet) {

Set tempDAGSet = tempTaskMap.get(DAGTask);

if (tempDAGSet != && tempDAGSet.contains(tempDAGTask)) {

tempDAGSet.remove(tempDAGTask);

if (tempDAGSet.size == 0) {

tempTaskQueue.add(DAGTask);

}

}

}

}

if (resultTaskQueue.size != tempTaskSet.size) {

throw new IllegalArgumentException("相互依赖,玩屁啊,我不跑了!");

}

}

}

检测完环后,开始调度这些依赖任务,将度为0的任务加入阻塞队列,通过newSingleThreadExecutor开启一个线程不断去阻塞队列拿任务。

public class DAGScheduler {

private void loop {

for (IDAGTask DAGTask : mTaskSet) {

if (DAGTask.getRely == 0) {

mTaskBlockingDeque.add(DAGTask);

}

}

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor;

singleThreadExecutor.execute( -> {

for (; ; ) {

try {

while (!mTaskBlockingDeque.isEmpty) {

IDAGTask executedDAGTsk = (IDAGTask) mTaskBlockingDeque.take;

if (executedDAGTsk.getIsAsync) {

Handler handler = new Handler(getMainLooper);

handler.post(executedDAGTsk);

} else {

mTaskThreadPool.execute(executedDAGTsk);

}

mTaskSet.remove(executedDAGTsk);

}

if (mTaskSet.isEmpty) {

singleThreadExecutor.shutdown;

mTaskThreadPool.shutdown;

return;

}

Iterator iterator = mTaskSet.iterator;

while (iterator.hasNext) {

IDAGTask DAGTask = iterator.next;

if (DAGTask.getRely == 0) {

mTaskBlockingDeque.put(DAGTask);

iterator.remove;

}

}

} catch (InterruptedException e) {

e.printStackTrace;

}

}

});

}

}

至此依赖任务的调度器搭建完毕,配合之前构建好的DAGProject,使用方法如下:

DAGScheduler dagScheduler = new DAGScheduler;dagScheduler.start(dagProject);

/ 使用方式 /

第一步,对应build.gradle配置远程依赖,已经发布到maven central,不用担心jcenter弃用。

implementation 'work.lingling.dagtask:dagtsk:1.0.0'

第二步,继承IDAGTask类,在run方法中实现对应的初始化逻辑。

public class ATask extends IDAGTask {

public ATask(String alias) {

super(alias);

}

@Override

public void run {

super.run;

try {

// 模拟随机时间

Random random = new Random;

Thread.sleep(random.nextInt(1000));

} catch (InterruptedException e) {

e.printStackTrace;

}

// 第三方框架内部使用同步加载

// completeDAGTask方法写在run方法末尾即可

completeDAGTask;

}

// 第三方框架内部使用异步加载

// completeDAGTask方法需要写进成功回调

/*onLibrarySuccess{

completeDAGTask;

}*/

}

tips:加载任务内部未开线程,completeDAGTask方法写在run方法的末尾,感知初始化结束;加载任务内部使用多线程,需要将completeDAGTask方法写进加载成功回调。

第三步,根据任务的依赖关系构建DAGProject并执行。

回首一开始出现的复杂依赖关系:

我们模拟对应的任务,任务A、B、C、D、E,构建DAGProject如下:

ATask a = new ATask("ATask");

BTask b = new BTask("BTask");

CTask c = new CTask("CTask");

DTask d = new DTask("DTask");

ETask e = new ETask("ETask");

DAGProject dagProject = new DAGProject.Builder

.addDAGTask(b)

.addDAGTask(c)

.addDAGTask(a)

.addDAGTask(d)

.addDAGTask(e)

.addDAGEdge(b, a)

.addDAGEdge(c, a)

.addDAGEdge(d, b)

.addDAGEdge(d, c)

.addDAGEdge(e, b)

.builder;

DAGScheduler dagScheduler = new DAGScheduler;

dagScheduler.start(dagProject);

依赖任务执行结果如下:

可以看到依赖任务被拆开成A、C、B、E、D的顺序进行执行。

/ 结语 /

行文至此,总算凑到了结尾,1202年了,居然还有人在用java写客户端。

框架实现整体很简单,但还是踩了很多坑,大到框架整体应该如何实现,小到设计模式应该如何使用、对外应该暴露什么方法、maven central如何上传等等各种细节问题,综上,这是一篇很青涩的文章。中途参考了很多大佬的文章思路和美好意见,但还是很不足,欢迎大佬们下场one one指导。

最后贴一下github链接:

https://github.com/LING-0001/DAGTask

展开阅读全文

页面更新:2024-04-02

标签:框架   加载   拓扑   末尾   队列   初始化   线程   源码   国庆   关系   方式   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top