XTask與Kotlin Coroutine的使用對比

xuexiangjys發表於2022-04-24

背景

XTask是我基於RxJava的設計思想,並結合實際專案中使用的經驗所創造出來的一個開源專案,其目的就是要代替RxJava在Android中的部分使用場景,提升開發的體驗和可維護性。

前段時間寫過一篇《XTask與RxJava的使用對比》文章,本來只是從對比這兩者使用的不同,來讓大家更直觀全面地瞭解XTask,然而有些槓精們就開始在下面評論或者私信說“用Kotlin的協程它不香嘛”、“和kotlin的協程相比如何”等。

首先我想說的是,協程並沒某些人吹得那麼神乎其神,說到底它就是個應用框架而已,主要解決的就是在開發過程中的非同步執行問題,這點它和RxJava是類似的;其次,協程並不是kotlin最先提出的,協程概念的提出最早可追溯至20世紀50年代,目前主流的語言如python、C++和go語言對於協程都有支援和實現;最後,這世上從來就沒有一本萬利的框架,任何不談使用場景的技術吹捧,都是在耍流氓。

不過既然你們想要對比,那我這就安排上!

不過在對比之前,我還是先來簡單介紹這兩個框架。

簡介

XTask

XTask是一個擴充性極強的Android任務執行框架。通過它,你可以自由定義和組合任務來實現你想要的功能,尤其適用於處理複雜的業務流程,可靈活新增前置任務或者調整執行順序。

專案的地址:
https://github.com/xuexiangjys/XTask

使用文件:
https://github.com/xuexiangjys/XTask/wiki

Kotlin Coroutine

kotlinx.coroutines 是由 JetBrains 開發的功能豐富的協程庫。它包含本指南中涵蓋的很多啟用高階協程的原語,包括 launch、async 等等。

協程不是系統級執行緒,很多時候協程被稱為“輕量級執行緒”、“微執行緒”。在Java中就類似Runnable。

專案地址:
https://github.com/Kotlin/kotlinx.coroutines

中文文件:
https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html

使用對比

還是和上次一樣,這次我還是從下面兩個小且常用的場景來給大家呈現它們的不同。

  • 複雜序列任務處理
  • 複雜併發任務處理

複雜序列任務

相信我們在平時的開發過程中一定會遇到很多複雜的業務流程,而這些流程很多都是一環套著一環,需要一步一步走下去才行,中間有任何錯誤都將停止執行。

下面我就以 [高仿網紅產品] 的案例流程為例,簡單講解如何通過Kotlin CoroutineXTask去實現這一流程。

案例分析

高仿網紅產品的流程

1.獲取產品資訊 -> 2.查詢可生產的工廠 -> 3.聯絡工廠生產產品 -> 4.送去市場部門評估售價 -> 5.產品上市

實體類設計

這裡主要涉及3個實體類: Product、ProductInfo和ProductFactory。

/**
 * 產品
 */
class Product {
    /**
     * 產品資訊
     */
    var info: ProductInfo
    /**
     * 產品生產地址
     */
    var address: String
    /**
     * 產品價格
     */
    var price: String? = null
    /**
     * 產品釋出時間
     */
    var publicTime: String? = null
}
/**
 * 產品資訊
 */
class ProductInfo {
    /**
     * 編號
     */
    var id: String
    /**
     * 品牌
     */
    var brand: String? = null
    /**
     * 質量
     */
    var quality: String? = null
}
/**
 * 產品工廠
 */
class ProductFactory {
    /**
     * 工廠id
     */
    var id: String
    /**
     * 工廠地址
     */
    var address: String
}

案例實現

業務流程處理

上述共有5個業務流程,我們將其簡化分為以下4個處理器進行處理。

  • 1.獲取產品資訊: GetProductInfoProcessor (productId -> ProductInfo)
  • 2.查詢相關的工廠: SearchFactoryProcessor (ProductInfo -> ProductFactory)
  • 3.評估產品,給出價格: GivePriceProcessor (Product -> Product)
  • 4.產品釋出: PublicProductProcessor (Product -> Product)
業務流程串聯
  • 普通寫法

普通寫法我們直接使用介面回撥的方式,一層層執行。

AppExecutors.get().singleIO().execute {
    // 1.獲取產品資訊
    GetProductInfoProcessor(binding?.logger, productId).setProcessorCallback(object :
        ProcessorCallbackAdapter<ProductInfo?>() {
        override fun onSuccess(productInfo: ProductInfo?) {
            // 2.查詢可生產的工廠
            SearchFactoryProcessor(binding?.logger, productInfo!!).setProcessorCallback(
                object : ProcessorCallbackAdapter<ProductFactory?>() {
                    override fun onSuccess(factory: ProductFactory?) {
                        // 3.聯絡工廠生產產品
                        log("開始生產產品...")
                        val product = factory?.produce(productInfo)
                        // 4.送去市場部門評估售價
                        GivePriceProcessor(binding?.logger, product!!).setProcessorCallback(
                            object : ProcessorCallbackAdapter<Product?>() {
                                override fun onSuccess(product: Product?) {
                                    // 5.產品上市
                                    PublicProductProcessor(
                                        binding?.logger,
                                        product
                                    ).setProcessorCallback(object :
                                        ProcessorCallbackAdapter<Product?>() {
                                        override fun onSuccess(product: Product?) {
                                            log("總共耗時:" + (System.currentTimeMillis() - startTime) + "ms")
                                            log("仿冒生產網紅產品完成, $product")
                                        }
                                    }).process()
                                }
                            }).process()
                    }
                }).process()
        }
    }).process()
  • Kotlin Coroutine寫法

Kotlin Coroutine最大的優勢就是可以讓非同步程式碼同步化,只需要使用withContext即可完成。其實這也不是什麼新鮮玩意,這就和js、dart語言裡的await類似。

mainScope.launch {
    val productInfo = withContext(Dispatchers.IO) {
        // 1.獲取產品資訊
        GetProductInfoProcessor(binding?.logger, productId).process()
    }
    val factory = withContext(Dispatchers.IO) {
        // 2.查詢可生產的工廠
        SearchFactoryProcessor(binding?.logger, productInfo).process()
    }
    // 3.聯絡工廠生產產品
    log("開始生產產品...")
    var product = factory.produce(productInfo)
    product = withContext(Dispatchers.IO) {
        // 4.送去市場部門評估售價
        GivePriceProcessor(binding?.logger, product).process()
        // 5.產品上市
        PublicProductProcessor(binding?.logger, product).process()
    }
    log("總共耗時:" + (System.currentTimeMillis() - startTime) + "ms")
    log("仿冒生產網紅產品完成, $product")
}
  • Kotlin Flow寫法

Kotlin Flow是Kotlin Coroutine生態的一部分,必須依託其才能使用。它是對標RxJava設計出來的,所有的API和RxJava基本相同,在絕大多數場景下可以做到等價替換。

如下程式碼所示,flowOf就類比just,map更是連名字都一樣的,flowIn類比subscribeOn,collect類比subscribe。

mainScope.launch {
    flowOf(productId)
        .map { id ->
            // 1.獲取產品資訊
            GetProductInfoProcessor(binding?.logger, id).process()
        }
        .map { productInfo ->
            // 2.查詢可生產的工廠
            SearchFactoryProcessor(binding?.logger, productInfo).process() to productInfo
        }
        .map { pair ->
            // 3.聯絡工廠生產產品
            log("開始生產產品...")
            val product = pair.first.produce(pair.second)
            // 4.送去市場部門評估售價
            GivePriceProcessor(binding?.logger, product).process()
        }.map { product ->
            // 5.產品上市
            PublicProductProcessor(binding?.logger, product).process()
        }.flowOn(Dispatchers.IO)
        .collect { product ->
            log("總共耗時:" + (System.currentTimeMillis() - startTime) + "ms")
            log("仿冒生產網紅產品完成, $product")
        }
}
  • XTask寫法

與普通寫法和RxJava寫法不同的是,XTask是把所有的業務處理器都封裝在了一個一個的Task中,然後按任務的執行順序依次新增對應的Task即可完成。

XTask.getTaskChain()
    .setTaskParam(TaskParam.get(ProductTaskConstants.KEY_PRODUCT_ID, productId)) // 1.獲取產品資訊
    .addTask(GetProductInfoTask(binding?.logger)) // 2.查詢可生產的工廠, 3.聯絡工廠生產產品
    .addTask(SearchFactoryTask(binding?.logger)) // 4.送去市場部門評估售價
    .addTask(GivePriceTask(binding?.logger)) // 5.產品上市
    .addTask(PublicProductTask(binding?.logger))
    .setTaskChainCallback(object : TaskChainCallbackAdapter() {
        override fun onTaskChainCompleted(engine: ITaskChainEngine, result: ITaskResult) {
            log("總共耗時:" + (System.currentTimeMillis() - startTime) + "ms")
            val product = result.dataStore.getObject(
                ProductTaskConstants.KEY_PRODUCT,
                Product::class.java
            )
            log("仿冒生產網紅產品完成, $product")
        }
    }).start()

案例執行結果

  • 程式執行結果

  • XTask執行日誌一覽


複雜並行任務

除了上面我們討論到的常見序列任務,我們在平時的開發過程中也會遇到一些複雜的並行流程。這些流程往往是單獨可執行的,雖說前後關聯不大,但是又是同時為了某個目標去執行的流程。

下面我就以常見的 [展示商品詳細資訊] 的案例流程為例,簡單講解如何通過Kotlin CoroutineXTask去實現這一流程。

案例分析

展示商品詳細資訊的流程
  • 1.根據商品的唯一號ID獲取商品簡要資訊
  • 2.獲取商品的詳細資訊:

    • 2.1 獲取商品的生產資訊
    • 2.2 獲取商品的價格資訊
    • 2.3 獲取商品的促銷資訊
    • 2.4 獲取商品的富文字資訊
  • 3.進行商品資訊的展示

其中步驟2中的4個子步驟是可以同時進行,互不影響的併發流程。

實體類設計

這裡主要涉及6個實體類: BriefInfo、Product、FactoryInfo、PriceInfo、PromotionInfo 和 RichInfo。

/**
 * 產品簡要資訊
 */
open class BriefInfo {
    var id: String
    var name: String? = null
    var factoryId: String? = null
    var priceId: String? = null
    var promotionId: String? = null
    var richId: String? = null
}

/**
 * 產品
 */
class Product(briefInfo: BriefInfo) : BriefInfo(briefInfo) {
    /**
     * 生產資訊
     */
    var factory: FactoryInfo? = null
    /**
     * 價格資訊
     */
    var price: PriceInfo? = null
    /**
     * 促銷資訊
     */
    var promotion: PromotionInfo? = null
    /**
     * 富文字資訊
     */
    var rich: RichInfo? = null
}

/**
 * 工廠生產資訊
 */
class FactoryInfo(var id: String) {
    /**
     * 生產地址
     */
    var address: String? = null
    /**
     * 生產日期
     */
    var productDate: String? = null
    /**
     * 過期日期
     */
    var expirationDate: String? = null
}

/**
 * 價格資訊
 */
class PriceInfo(var id: String) {
    /**
     * 出廠價
     */
    var factoryPrice = 0f
    /**
     * 批發價
     */
    var wholesalePrice = 0f
    /**
     * 零售價
     */
    var retailPrice = 0f
}

/**
 * 產品促銷資訊
 */
class PromotionInfo(var id: String) {
    /**
     * 促銷型別
     */
    var type = 0
    /**
     * 促銷內容
     */
    var content: String? = null
    /**
     * 生效日期
     */
    var effectiveDate: String? = null
    /**
     * 失效日期
     */
    var expirationDate: String? = null
}

/**
 * 富文字資訊
 */
class RichInfo(var id: String) {
    /**
     * 描述資訊
     */
    var description: String? = null
    /**
     * 圖片連結
     */
    var imgUrl: String? = null
    /**
     * 視訊連結
     */
    var videoUrl: String? = null
}

案例實現

業務流程處理

上述共有3個大業務流程,4個子業務流程,我們將其簡化分為以下5個處理器進行處理。

  • 1.獲取商品簡要資訊: GetBriefInfoProcessor (productId -> BriefInfo)
  • 2.獲取商品的生產資訊: GetFactoryInfoProcessor (factoryId -> FactoryInfo)
  • 3.獲取商品的價格資訊: GetPriceInfoProcessor (priceId -> PriceInfo)
  • 4.獲取商品的促銷資訊: GetPromotionInfoProcessor (promotionId -> PromotionInfo)
  • 5.獲取商品的富文字資訊: GetRichInfoProcessor (richId -> RichInfo)
業務流程串聯
  • 普通寫法

普通寫法我們需要通過介面回撥+同步鎖的方式, 實現任務的併發和協同。

AppExecutors.get().singleIO().execute {
    GetBriefInfoProcessor(binding?.logger, productId).setProcessorCallback(object :
        AbstractProcessor.ProcessorCallbackAdapter<BriefInfo?>() {
        override fun onSuccess(briefInfo: BriefInfo?) {
            val product = Product(briefInfo!!)
            val latch = CountDownLatch(4)

            // 2.1 獲取商品的生產資訊
            AppExecutors.get().networkIO().execute {
                GetFactoryInfoProcessor(
                    binding?.logger,
                    product.factoryId!!
                ).setProcessorCallback(object :
                    AbstractProcessor.ProcessorCallbackAdapter<FactoryInfo?>() {
                    override fun onSuccess(result: FactoryInfo?) {
                        product.factory = result
                        latch.countDown()
                    }
                }).process()
            }
            // 2.2 獲取商品的價格資訊
            AppExecutors.get().networkIO().execute {
                GetPriceInfoProcessor(
                    binding?.logger,
                    product.priceId!!
                ).setProcessorCallback(
                    object : AbstractProcessor.ProcessorCallbackAdapter<PriceInfo?>() {
                        override fun onSuccess(result: PriceInfo?) {
                            product.price = result
                            latch.countDown()
                        }
                    }).process()
            }
            // 2.3 獲取商品的促銷資訊
            AppExecutors.get().networkIO().execute {
                GetPromotionInfoProcessor(
                    binding?.logger,
                    product.promotionId!!
                ).setProcessorCallback(object :
                    AbstractProcessor.ProcessorCallbackAdapter<PromotionInfo?>() {
                    override fun onSuccess(result: PromotionInfo?) {
                        product.promotion = result
                        latch.countDown()
                    }
                }).process()
            }
            // 2.4 獲取商品的富文字資訊
            AppExecutors.get().networkIO().execute {
                GetRichInfoProcessor(
                    binding?.logger,
                    product.richId!!
                ).setProcessorCallback(
                    object : AbstractProcessor.ProcessorCallbackAdapter<RichInfo?>() {
                        override fun onSuccess(result: RichInfo?) {
                            product.rich = result
                            latch.countDown()
                        }
                    }).process()
            }
            try {
                latch.await()
                log("總共耗時:" + (System.currentTimeMillis() - startTime) + "ms")
                log("查詢商品資訊完成, $product")
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
        }
    }).process()
}
  • Kotlin Coroutine寫法

Kotlin Coroutine實現並行任務非常簡單,只需要使用async+await的方式即可完成,而且寫出來也非常簡潔明瞭。

mainScope.launch {
    // 1.獲取商品簡要資訊
    val briefInfo = withContext(Dispatchers.IO) {
        GetBriefInfoProcessor(binding?.logger, productId).process()
    }

    val product = Product(briefInfo)
    // 2.1 獲取商品的生產資訊
    val factory = async(Dispatchers.IO) {
        GetFactoryInfoProcessor(binding?.logger, product.factoryId!!).process()
    }
    // 2.2 獲取商品的價格資訊
    val price = async(Dispatchers.IO) {
        GetPriceInfoProcessor(binding?.logger, product.factoryId!!).process()
    }
    // 2.3 獲取商品的促銷資訊
    val promotion = async(Dispatchers.IO) {
        GetPromotionInfoProcessor(binding?.logger, product.factoryId!!).process()
    }
    // 2.4 獲取商品的富文字資訊
    val rich = async(Dispatchers.IO) {
        GetRichInfoProcessor(binding?.logger, product.factoryId!!).process()
    }
    product.factory = factory.await()
    product.price = price.await()
    product.promotion = promotion.await()
    product.rich = rich.await()

    log("總共耗時:" + (System.currentTimeMillis() - startTime) + "ms")
    log("查詢商品資訊完成, $product")
}
  • Kotlin Flow寫法

和RxJava類似,在Kotlin Flow中執行並行任務,一般使用flatMapMergezip的組合方式,對任務流進行合併。不過說實在話,與上面Kotlin Coroutine實現的方式還是相對繁瑣一些的。

mainScope.launch {
    flowOf(productId)
        .map { id ->
            // 1.獲取商品簡要資訊
            GetBriefInfoProcessor(binding?.logger, id).process()
        }
        .map { briefInfo -> Product(briefInfo) }
        .flatMapMerge { product ->
            // 2.1 獲取商品的生產資訊
            flowFactory(product)
                // 2.2 獲取商品的價格資訊
                .zip(flowPrice(product)) { factoryInfo, priceInfo ->
                    product.apply {
                        factory = factoryInfo
                        price = priceInfo
                    }
                    // 2.3 獲取商品的促銷資訊
                }.zip(flowPromotion(product)) { _, promotionInfo ->
                    product.apply {
                        promotion = promotionInfo
                    }
                    // 2.4 獲取商品的富文字資訊
                }.zip(flowRich(product)) { _, richInfo ->
                    product.apply {
                        rich = richInfo
                    }
                }
        }.flowOn(Dispatchers.IO)
        .collect { product ->
            log("總共耗時:" + (System.currentTimeMillis() - startTime) + "ms")
            log("查詢商品資訊完成, $product")
        }
}
  • XTask寫法

XTask是把所有的業務處理器都封裝在了一個一個的Task中,然後並行的任務需要通過一個ConcurrentGroupTask(同步組任務)進行包裹,其他按正常執行順序新增Task即可。

XTask.getTaskChain()
    .setTaskParam(
        TaskParam.get(
            ProductTaskConstants.KEY_PRODUCT_ID,
            productId
        )
    ) // 1.獲取商品簡要資訊
    .addTask(GetBriefInfoTask(binding?.logger))
    .addTask(
        XTask.getConcurrentGroupTask(ThreadType.SYNC) // 2.1 獲取商品的生產資訊
            .addTask(GetFactoryInfoTask(binding?.logger)) // 2.2 獲取商品的價格資訊
            .addTask(GetPriceInfoTask(binding?.logger)) // 2.3 獲取商品的促銷資訊
            .addTask(GetPromotionInfoTask(binding?.logger)) // 2.4 獲取商品的富文字資訊
            .addTask(GetRichInfoTask(binding?.logger))
    )
    .setTaskChainCallback(object : TaskChainCallbackAdapter() {
        override fun onTaskChainCompleted(engine: ITaskChainEngine, result: ITaskResult) {
            log("總共耗時:" + (System.currentTimeMillis() - startTime) + "ms")
            val product: Product = result.dataStore.getObject(
                ProductTaskConstants.KEY_PRODUCT,
                Product::class.java
            )
            log("查詢商品資訊完成, $product")
        }
    }).start()

案例執行結果

  • 程式執行結果

  • XTask執行日誌一覽


使用對比總結

從上面的使用對比來看,我們可以簡單歸納總結以下幾點:

程式設計方式

1.Kotlin Coroutine遵循的是函數語言程式設計的原則,可以使用阻塞的方式寫出非阻塞式的程式碼,解決併發中常見的回撥地獄。消除了併發任務之間的協作的難度,協程可以讓我們輕鬆地寫出複雜的併發程式碼。從這一點來看,Kotlin Coroutine無疑是非常優秀的,因為它可以大大降低非同步程式的設計複雜度。

2.XTask遵循的是物件導向的程式設計原則,每個處理過程都對應了一個具體或者抽象的Task。這樣的好處就是,減少了業務和資料結構之間的耦合,同時也減少了各個業務之間的耦合。這樣即使你的資料結構或者業務流程出現大的變動,功能實現的主體也不會產生大的改動,更多的只是每個子業務Task內部的改動和調整,真正實現了高複用低耦合。

總結: 如果從程式設計的簡潔性角度而言,無疑Kotlin Coroutine是完勝的,畢竟這是函數語言程式設計的優勢。但是如果從程式設計的耦合性角度而言,那XTask還是有點優勢的。所以兩種不同的程式設計方式,遵循兩種不同的程式設計原則,無法對比孰優孰劣。

上手難度

1.如果拋開kotlin Flow不談的話,Kotlin Coroutine上手還是相對比較容易的。相比於RXJava而言,可能更適合我們Android開發。

2.XTask作為專為Android設計的任務執行框架,功能相對單一。沒有複雜的操作符,有的只是“任務鏈、任務、組任務、任務引數和執行結果”這五個組成要素,使用起來相對簡單容易上手。

總結: 整體比較下來,兩者基本相同,但是Kotlin Coroutine相關的資料比較多一些,所以可能更容易上手,也更加通用。

開發效率

1.函數語言程式設計最大的優勢就是程式碼簡潔寫得快。在這點上Kotlin Coroutine無疑是非常優秀的,基本吊打一眾非同步執行框架。

2.XTask由於每個業務子步驟都需要寫一個Task類,相比較而言效率是明顯會低一些的。

總結: 整體比較下來,Kotlin Coroutine完勝XTask。

可維護性

1.Kotlin Coroutine遵循的是函數語言程式設計的原則,本質上還是程式導向式的程式設計。所有的業務流程都和資料有著比較強的耦合,當業務流程發生變動的時候,必然會導致主幹程式碼的變動。而且對於初入專案的開發人員接手專案的時候,過多地暴露了內部實現的細節,很難從全域性的視角去理解專案主體業務,很容易產生區域性修改影響全域性的結果。

2.XTask遵循的是物件導向的程式設計原則,設計之初就嚴格遵循物件導向的設計模式原則。充分減少業務與業務、業務與資料流之間的耦合,這樣即使你的資料結構或者業務流程出現重大的變化,主幹程式碼也不會有很大的變動。而且XTask擁有較強的日誌記錄系統,能夠非常清晰的記錄你當前任務鏈的執行過程和所線上程的資訊(自動的),當任務執行出現問題的時候,便能很快地定位出問題產生的位置。而對於初入專案的開發人員來說,也能快速從任務執行過程的日誌中去理解專案的主體業務。待主體業務流程有了清楚的認知後再去仔細看子業務,這樣才能全方位理解專案的業務,也更利於專案的維護。

總結: 整體比較下來,XTask是要優於Kotlin Coroutine的。

效能

在效能上,XTask為了實現業務與資料之間的隔離,設計了共享資料的結構,相比較Kotlin Coroutine而言,多了資料拷貝以及資料儲存的過程,所以無論是在時間還是空間上而言,Kotlin Coroutine都是較優於XTask的。


最後

綜合以上的論述,Kotlin Coroutine總體上是要優於XTask的。

  • 如果你是函數語言程式設計的愛好者,那麼一定是選擇Kotlin Coroutine; 如果你是物件導向程式設計的愛好者,那麼XTask一定是個不錯的選擇;
  • 如果追求開發的效率,那麼可以優先考慮Kotlin Coroutine; 如果站在日後專案的穩定性和可維護性角度,選擇XTask一定不會讓你失望;
  • 如果你使用kotlin進行開發,那麼別想了,就選Kotlin Coroutine了; 如果你還是非常鍾愛於用Java開發Android,那麼一定要考慮一下XTask。

本文章所涉及的原始碼都已放在github上,專案主頁:
https://github.com/xuexiangjys/KotlinSample

喜歡的朋友可以關注XTask的專案主頁: https://github.com/xuexiangjys/XTask

我是xuexiangjys,一枚熱愛學習,愛好程式設計,致力於Android架構研究以及開源專案經驗分享的技術up主。獲取更多資訊,歡迎微信搜尋公眾號:【我的Android開源之旅】

相關文章