日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

/ 前言 /

我收回標題上的話,從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 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的模板就被敲定了,接下來我們需要建立任務之間的關系:

 

  • Set儲存所有的任務,通過addDAGTask添加任務

     

  • Map儲存所有的任務與其前置依賴關系,通過addDAGEdge添加任務依賴關系

     

  • 整體采用建造者模式構建DAGProject類

     

 

于是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 == null) {
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后,我們先不著急丟進線程池,執行對應邏輯前先檢測是否有環,這樣我們可以在任務加載前拋出相互依賴的錯誤,大可不必等到執行至有環那一步才拋出。雖然有環可以靠輸入者去保障,但是在一些小細節方面,我們要求輸入者保證過于苛刻也過于差體驗。

 

  • 將度為0的任務儲存在tempTaskQueue

     

  • while循環將任務取出,存在依賴關系則對應的任務度-1,如果度為0,添加到resultTaskQueue

     

  • 判斷最后的resultTaskQueue與之前儲存任務的set個數是否相等,false則有環拋出異常

     

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) == null) {
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 != null && tempDAGSet.contains(tempDAGTask)) {
tempDAGSet.remove(tempDAGTask);
if (tempDAGSet.size() == 0) {
tempTaskQueue.add(DAGTask);
}
}
}
}
if (resultTaskQueue.size() != tempTaskSet.size()) {
throw new IllegalArgumentException("相互依賴,玩屁啊,我不跑了!");
}
}
}

檢測完環后,開始調度這些依賴任務,將度為0的任務加入阻塞隊列,通過newSingleThreadExecutor開啟一個線程不斷去阻塞隊列拿任務。

 

  • 判斷拿出的任務屬于主線程執行還是異步執行,主線程執行通過handler.post發送出去,異步執行通過線程池execute

     

  • 所有任務執行完畢,關閉線程池,結束遍歷

     

  • 不斷將度為0的任務加入阻塞隊列

     

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

分享到:
標簽:框架
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定