背景
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 Coroutine和XTask去實現這一流程。
案例分析
高仿網紅產品的流程
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 Coroutine和XTask去實現這一流程。
案例分析
展示商品詳細信息的流程
- 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中執行並行任務,一般使用flatMapMerge和zip的組合方式,對任務流進行合併。不過説實在話,與上面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開源之旅】