在之前的文章中,我介紹了自研的 Android 啟動(dòng)任務(wù)調(diào)度工具 AndroidStartup思杯。近期郁妈,因?yàn)樵诮M件化項(xiàng)目中運(yùn)用該項(xiàng)目的需要带斑,我對(duì)這個(gè)庫(kù)做了一番升級(jí)。在最新的 2.2 版本中,我新增了一些特性匈睁。相比于目前市面上其他的啟動(dòng)任務(wù)調(diào)度庫(kù)卷哩,使其具備了更多的優(yōu)勢(shì)蛋辈。這里我只介紹下經(jīng)過新的版本迭代之后該項(xiàng)目與其他項(xiàng)目的不同點(diǎn)。對(duì)于其基礎(chǔ)的實(shí)現(xiàn)原理将谊,可以參考我之前的文章 《異步冷溶、非阻塞式 Android 啟動(dòng)任務(wù)調(diào)度庫(kù)》。
1尊浓、支持多種線程模型
這是相對(duì)于 Jetpack 的啟動(dòng)任務(wù)庫(kù)的優(yōu)勢(shì)逞频,在指定任務(wù)的時(shí)候,你可以通過 ISchedulerJob
的 threadMode()
方法指定該任務(wù)執(zhí)行的線程栋齿,當(dāng)前支持主線程(ThreadMode.MAIN
)和非主線程(ThreadMode.BACKGROUND
)兩種情況苗胀。前者在主線程當(dāng)中執(zhí)行,后者在線程池當(dāng)中執(zhí)行瓦堵,同時(shí)基协,該庫(kù)還允許你自定義自己的線程池。關(guān)于這塊的實(shí)現(xiàn)原理可以參考之前的文章或者項(xiàng)目源碼菇用。
2澜驮、非阻塞的任務(wù)調(diào)度方式
在之前的文章中也提到了,如果說采用 CountDownLatch 等阻塞的方式來實(shí)現(xiàn)任務(wù)調(diào)度惋鸥,雖然不會(huì)占用主線程的 CPU杂穷,但是子線程會(huì)被阻塞鹅龄,一樣會(huì)導(dǎo)致 CPU 空轉(zhuǎn),影響程序執(zhí)行的性能亭畜,尤其啟動(dòng)的時(shí)候大量任務(wù)執(zhí)行時(shí)的情況扮休。所以,在這個(gè)庫(kù)的設(shè)計(jì)中拴鸵,我們使用了通知喚醒的方式進(jìn)行任務(wù)調(diào)度玷坠。也就是,
首先劲藐,它會(huì)將所有的需要執(zhí)行的任務(wù)收集起來八堡;然后,它會(huì)根據(jù)任務(wù)的依賴關(guān)系指定分發(fā)和調(diào)度任務(wù)的子任務(wù)聘芜;最后兄渺,當(dāng)當(dāng)前任務(wù)執(zhí)行完畢,該任務(wù)會(huì)通知所有的子任務(wù)按照順序執(zhí)行汰现。大致實(shí)現(xiàn)邏輯如下挂谍,
override fun execute() {
val realJob = {
// 1. Run the task if match given process.
if (matcher.match(job.targetProcesses())) {
job.run(context)
}
// 2. Then sort children task.
children.sortBy { child -> -child.order() }
// 3. No matter the task invoked in current process or not,
// its children will be notified after that.
children.forEach { it.notifyJobFinished(this) }
}
try {
if (job.threadMode() == ThreadMode.MAIN) {
// Cases for main thread.
if (Thread.currentThread() == Looper.getMainLooper().thread) {
realJob.invoke()
} else {
mainThreadHandler.post { realJob.invoke() }
}
} else {
// Cases for background thread.
executor.execute { realJob.invoke() }
}
} catch (e: Throwable) {
throw SchedulerException(e)
}
}
3、非 Class 的依賴方式
之前在本項(xiàng)目中瞎饲,以及其他的項(xiàng)目中可能采用了基于 Class 的形式進(jìn)行任務(wù)依賴口叙。這種使用方式存在一些問題,即在組件化開發(fā)的時(shí)候嗅战,Class 之間需要直接進(jìn)行引用妄田。這導(dǎo)致各個(gè)組件之間的強(qiáng)耦合。這顯然不是我們希望的驮捍。
所以疟呐,為了更好地支持組件化,在該庫(kù)的新版本中东且,我們?cè)试S通過 name()
方法執(zhí)行任務(wù)的名稱启具,以及通過 dependencies()
方法指定該任務(wù)依賴的其他任務(wù)的名稱。name()
默認(rèn)使用任務(wù) Class 的全限定名苇倡。這樣富纸,當(dāng)多個(gè)組件之間進(jìn)行相互依賴的時(shí)候囤踩,只需要通過字符串指定名稱而無需引用具體的類旨椒。
比如,一個(gè)任務(wù)在一個(gè)組件中定義如下堵漱,
@StartupJob class BlockingBackgroundJob : ISchedulerJob {
override fun name(): String = "blocking"
override fun threadMode(): ThreadMode = ThreadMode.BACKGROUND
override fun dependencies(): List<String> = emptyList()
override fun run(context: Context) {
Thread.sleep(5_000L) // 5 seconds
L.d("BlockingBackgroundJob done! ${Thread.currentThread()}")
toast("BlockingBackgroundJob done!")
}
}
在另一個(gè)組件中的另一個(gè)任務(wù)需要依賴上述任務(wù)的時(shí)候综慎,定義如下,
@StartupJob class SubModuleTask : ISchedulerJob {
override fun dependencies(): List<String> = listOf("blocking")
override fun run(context: Context) {
Log.d("SubModuleTask", "runed ")
}
}
這樣我們就實(shí)現(xiàn)組件化場(chǎng)景中的依賴關(guān)系了勤庐。
4示惊、支持任務(wù)的優(yōu)先級(jí)
在實(shí)際開發(fā)中好港,我們可能會(huì)遇到需要為所有的根任務(wù)或者一個(gè)任務(wù)的所有的子任務(wù)指定執(zhí)行的先后順序的場(chǎng)景∶追#或者在組件化中钧汹,存在依賴關(guān)系,但是我們希望某個(gè)根任務(wù)優(yōu)先執(zhí)行录择,但是不想為每個(gè)子任務(wù)都執(zhí)行依賴關(guān)系的時(shí)候拔莱,我們可以通過指定這個(gè)任務(wù)的優(yōu)先級(jí)為最高來使其最先被執(zhí)行。你可以通過 priority()
方法傳遞一個(gè) 0 到 100 的整數(shù)來指定任務(wù)的優(yōu)先級(jí)隘竭。
@StartupJob class TopPriorityJob : ISchedulerJob{
override fun priority(): Int = 100
override fun run(context: Context) {
L.d("Top level job done!")
}
}
優(yōu)先級(jí)局限于依賴關(guān)系相同的任務(wù)塘秦,所以是依賴關(guān)系的補(bǔ)充,不會(huì)造成歧義动看。
5尊剔、支持指定任務(wù)執(zhí)行的進(jìn)程,可自定義進(jìn)程匹配策略
如果我們的項(xiàng)目支持多進(jìn)程菱皆,而我們希望某些啟動(dòng)任務(wù)只在某個(gè)進(jìn)程中執(zhí)行而其他進(jìn)程不需要執(zhí)行须误,以此避免沒必要的任務(wù)來提升任務(wù)執(zhí)行的性能的時(shí)候,我們可以通過指定任務(wù)執(zhí)行的進(jìn)程來進(jìn)行優(yōu)化仇轻。你可以通過 targetProcesses()
傳遞一個(gè)進(jìn)程的列表來指定該任務(wù)執(zhí)行的所有進(jìn)程霹期。默認(rèn)列表為空,表示運(yùn)行在所有的進(jìn)程拯田。
對(duì)于進(jìn)程的匹配历造,我們提供了 IProcessMatcher
這個(gè)接口,
interface IProcessMatcher {
fun match(target: List<String>): Boolean
}
你可以通過指定這個(gè)接口來自定義線程的匹配策略船庇。
6吭产、支持注解形式的組件化調(diào)用
在之前的版本中,通過 ContentProvider 的形式我們一樣可以實(shí)現(xiàn)所有組件內(nèi)任務(wù)的收集和調(diào)用鸭轮。但是使用 ContentProvider 存在一些不便之處臣淤,比如 ContentProvider 的初始化實(shí)際在 Application 的 attachBaseContext()
,如果我們的任務(wù)中一些操作需要放到 Application 的 onCreate()
中執(zhí)行的時(shí)候窃爷,通過 ContentProvider 默認(rèn)裝載任務(wù)的調(diào)度方式就存在問題邑蒋。而通過基于注解 + APT的形式,我們可以隨意指定任務(wù)收集按厘、整理和執(zhí)行的時(shí)機(jī)医吊,靈活性更好。
為了支持組件化逮京,我們?cè)谥暗捻?xiàng)目上做了一些拓展卿堂。之前的項(xiàng)目雖然也是基于注解發(fā)現(xiàn)機(jī)制,但是在組件化的應(yīng)用中存在問題。在新的版本中草描,我們只是處理了組件化應(yīng)用場(chǎng)景中的問題览绿,但是使用方式上面完全兼容,只不過你需要為每個(gè)組件在 gradle.build
中增加一個(gè)行信息來指定組件的名稱(就像 ARouter 一樣)穗慕,
javaCompileOptions {
annotationProcessorOptions {
arguments = [STARTUP_MODULE_NAME: project.getName()]
}
}
也就是說你還是通過 @StartupJob
注解將任務(wù)標(biāo)記為啟動(dòng)任務(wù)饿敲,然后通過
launchStartup(this) {
scanAnnotations()
}
這行代碼啟動(dòng)掃描并執(zhí)行任務(wù)。
在新的版本中逛绵,所有生產(chǎn)的代碼會(huì)被統(tǒng)一放到包 me.shouheng.startup.hunter
下面诀蓉,然后通過 JobHunter$$組件名
的形式為每個(gè)組件生成自己的類,然后在掃描任務(wù)的時(shí)候通過加載這個(gè)包名之下的所有的代碼來找到所有要執(zhí)行的任務(wù)暑脆。如果你對(duì)組件化感興趣可以直接閱讀這塊的源碼實(shí)現(xiàn)渠啤。
總結(jié)
啟動(dòng)任務(wù)調(diào)度庫(kù)的設(shè)計(jì)不算復(fù)雜,但是我卻在之前的面試中兩次被問到如何設(shè)計(jì)添吗。這種類型的問題能很好地考察代碼設(shè)計(jì)能力沥曹。相信閱讀這個(gè)庫(kù)的代碼之后,此類的問題再也難不倒你碟联。如果你對(duì) APT+注解 的組件化實(shí)現(xiàn)方式等感興趣一樣可以閱讀這個(gè)庫(kù)的代碼妓美。
以上介紹了這個(gè)庫(kù)的一些特性和優(yōu)勢(shì),沒用過多地介紹其源碼實(shí)現(xiàn)鲤孵,感興趣的同學(xué)可以直接閱讀項(xiàng)目的源碼壶栋,相信你能夠從代碼中學(xué)到一些東西。對(duì)于示例項(xiàng)目普监,除了閱讀這個(gè)項(xiàng)目的示例贵试,還可以參考 Android-VMLib 這個(gè)項(xiàng)目。該項(xiàng)目地址:https://github.com/Shouheng88/AndroidStartup凯正。