背景
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開源之旅】