1傻寂、背景
節(jié)前面試的時候被問到 Android 啟動任務依賴怎么做調度。當時隨口給了一個方案携兵,后來想想覺得有意思就自己花了一天的時間寫了一個崎逃。這個庫已經(jīng)開源到 Github 上面:
https://github.com/Shouheng88/AndroidStartup
在寫這個庫之前只是看了下 Jetpack 的 Startup. 畢竟,如果這個庫已經(jīng)非常完善了眉孩,那么我就沒必要自己再搞一個了。截止目前勒葱,在我看來浪汪,這個庫最大的缺點是,這個庫所有的任務都在主線程中觸發(fā)并執(zhí)行凛虽,而我們?yōu)榱藘?yōu)化啟動的性能通常會將任務放到異步線程中執(zhí)行死遭。所以,Jetpack 的庫充其量只能解決你的任務的依賴關系凯旋。
如果要支持異步任務執(zhí)行呀潭,首先要解決的是如何保證任務的先后順序。最初我也想到了使用并發(fā)包里的閉鎖的方案至非,但是這種方案有個問題钠署。即,閉鎖執(zhí)行的時候使用 CAS 以阻塞的方式進行等待荒椭,這樣會白白浪費線程資源谐鼎。如果因此占用了 CPU,將會影響到我們其他線程的執(zhí)行趣惠。所以狸棍,在我的庫中身害,我使用了非阻塞的事件通知機制。這樣在某個任務結束之后會通知所有依賴于它的任務草戈。當一個任務的所有依賴都執(zhí)行完畢塌鸯,再執(zhí)行自己的任務。當然唐片,對于不同參數(shù)的線程池丙猬,異步任務執(zhí)行的表現(xiàn)也是不一樣的,所以牵触,我也提供了方法用來自定義線程池淮悼。
此外,我在開發(fā)這個庫的時候還用到了注解處理器揽思。你可以通過注解聲明自己的任務袜腥,然后編譯期間會自動發(fā)現(xiàn)并拼接你的任務。相對于其他的框架钉汗,又多了一個初始化的選擇羹令。
2、結構
在開發(fā)的時候我將任務調度和啟動工具分成了兩個獨立的模塊损痰,這樣任務調度工具也可以單獨拿出來使用福侈。后來,增加了注解驅動相關的邏輯卢未,就又增加了兩個模塊肪凛。所以,現(xiàn)在各模塊及功能如下:
scheduler: 任務調度工具
startup: 啟動工具辽社,任務調度工具包裝
startup-annotation: 注解定義
startup-compiler: 注解編譯器
3伟墙、調度器
先來看任務調度器的工作原理。
3.1 任務封裝
首先是任務的定義滴铅。在我的項目中我使用 ISchedulerJob 定義任務戳葵。
interface ISchedulerJob {
fun threadMode(): ThreadMode
fun dependencies(): List<Class<out ISchedulerJob>>
fun run(context: Context)
}
它定義了三個方法,分別是:
-
threadMode()
用來指定執(zhí)行任務的線程 -
dependencies()
用來指定當前任務依賴的任務 -
run()
你的初始化的任務執(zhí)行的方法
其次汉匙,真實的任務分發(fā)的邏輯是通過 Dispatcher 來完成的拱烁。根據(jù)任務之間的依賴關系,我們可以構建成一拓撲結構噩翠。執(zhí)行任務的時候先執(zhí)行的任務就是這個拓撲結構的根結點戏自。放在這里就是 dependencies()
方法為空的結點。所以伤锚,這里浦妄,我們首先要監(jiān)測拓撲結構是否存在環(huán)。然后,只需要找到根結點并從根結點執(zhí)行任務即可剂娄。
3.2 環(huán)檢測
對于環(huán)監(jiān)測蠢涝,如果不考慮空間復雜度,我們可以使用 Set 來發(fā)現(xiàn)循環(huán)依賴:
private fun checkDependencies() {
val checking = mutableSetOf<Class<out ISchedulerJob>>()
val checked = mutableSetOf<Class<out ISchedulerJob>>()
val schedulerMap = mutableMapOf<Class<ISchedulerJob>, ISchedulerJob>()
schedulerJobs.forEach { schedulerMap[it.javaClass] = it }
schedulerJobs.forEach { schedulerJob ->
checkDependenciesReal(schedulerJob, schedulerMap, checking, checked)
}
}
private fun checkDependenciesReal(
schedulerJob: ISchedulerJob,
map: Map<Class<ISchedulerJob>, ISchedulerJob>,
checking: MutableSet<Class<out ISchedulerJob>>,
checked: MutableSet<Class<out ISchedulerJob>>
) {
if (checking.contains(schedulerJob.javaClass)) {
// Cycle detected.
throw SchedulerException("Cycle detected for ${schedulerJob.javaClass.name}.")
}
if (!checked.contains(schedulerJob.javaClass)) {
checking.add(schedulerJob.javaClass)
if (schedulerJob.dependencies().isNotEmpty()) {
schedulerJob.dependencies().forEach {
if (!checked.contains(it)) {
val job = map[it]
?: throw SchedulerException(String.format("dependency [%s] not found", it.name))
checkDependenciesReal(job, map, checking, checked)
}
}
}
checking.remove(schedulerJob.javaClass)
checked.add(schedulerJob.javaClass)
}
}
這里的邏輯和 Jetpack 中的環(huán)監(jiān)測的邏輯差不多阅懦。這里用了兩個多余的數(shù)據(jù)結構和二,分別記錄已經(jīng)檢測的和檢測中的任務結點,如果發(fā)現(xiàn)了一個需要檢測的任務正在檢測中耳胎,則說明存在環(huán)惯吕。
3.3 任務啟動
任務啟動之前需要先根據(jù)任務間的依賴關系建立數(shù)據(jù)結構,簡單說就是需要知道當前任務有哪些依賴任務和哪些依賴于它的任務怕午。
private fun buildDispatcherJobs() {
roots.clear()
// Build the map from scheduler class type to dispatcher job.
val map = mutableMapOf<Class<ISchedulerJob>, DispatcherJob>()
schedulerJobs.forEach {
val dispatcherJob = DispatcherJob(this.globalContext, executor, it)
map[it.javaClass] = dispatcherJob
}
// Fill the parent field for dispatcher job.
schedulerJobs.forEach { schedulerJob ->
val dispatcherJob = map[schedulerJob.javaClass]!!
schedulerJob.dependencies().forEach {
dispatcherJob.addParent(map[it]!!)
}
}
// Fill the children field for dispatcher job.
schedulerJobs.forEach { schedulerJob ->
val dispatcherJob = map[schedulerJob.javaClass]!!
dispatcherJob.parents().forEach {
it.addChild(dispatcherJob)
}
}
// Find roots.
schedulerJobs.filter {
it.dependencies().isEmpty()
}.forEach {
val dispatcherJob = map[it.javaClass]!!
roots.add(dispatcherJob)
}
}
這里先對任務做一個封裝废登,將所有的任務包裝成 DispatcherJob,然后根據(jù)任務的依賴關系找到各任務的父任務郁惜,并調用其 addParent()
方法堡距,這里在 DispatcherJob 中會使用一個 AtomicInteger 進行計數(shù),統(tǒng)計其父任務的數(shù)量兆蕉。然后羽戒,再通過各任務的父任務維護子任務關系。最后虎韵,再根據(jù)任務的依賴找到拓撲的根結點易稠。這樣,我們就可以從根結點開始執(zhí)行整個拓撲結構包蓝。
3.4 任務通知機制
上面我們提到了 DispatcherJob驶社,啟動一個 DispatcherJob 只需要調用它的 execute()
方法,該方法中會根據(jù)線程模型做判斷测萎,從而選擇執(zhí)行的線程執(zhí)行任務:
override fun execute() {
val realJob = {
// Run the task.
job.run(context)
// Handle for children.
children.forEach { it.notifyJobFinished(this) }
}
try {
if (job.threadMode() == ThreadMode.MAIN) {
// Cases for main thread.
if (Thread.currentThread() == Looper.getMainLooper().thread) {
realJob()
} else {
Handler(Looper.getMainLooper()).post { realJob() }
}
} else {
// Cases for background thread.
executor.execute { realJob() }
}
} catch (e: Throwable) {
throw SchedulerException(e)
}
}
這里任務的執(zhí)行邏輯被包裝到了一個 lambda 方法中亡电。如果是主線程,可以根據(jù)當前線程狀態(tài)執(zhí)行執(zhí)行或者 post 到主線程中執(zhí)行绳泉。如果是異步任務,則將其丟到線程池當中執(zhí)行姆泻。
當一個任務的工作結束之后會獲取所有的子任務進行通知零酪,這里用到了 notifyJobFinished()
方法。這個方法也很簡單拇勃,就是沒當一個任務執(zhí)行完畢四苇,則計數(shù)器減 1,當所有的依賴任務都執(zhí)行完畢的時候方咆,它才開始執(zhí)行自己的任務月腋,以此來通過事件而不是阻塞的方式進行任務調度:
override fun notifyJobFinished(job: IDispatcherJob) {
if (waiting.decrementAndGet() == 0) {
// All dependencies finished, commit the job.
execute()
}
}
4、啟動器
對于啟動器,你有三種選擇榆骚。使用類似于 Jetpack 的 ContentProvider片拍、自己聲明任務或者使用注解 @StartupJob
進行任務聲明。
對于內容提供器妓肢,原理比較簡單捌省,就是再自定義 ContentProvider 的 onCreate()
方法中掃描自定義的 meta-data. ContentProvider 的聲明方式有幾個問題,第一碉钠,ContentProvider 作為四大組件之一纲缓,建立過程需要消耗一定性能。此外喊废,默認 ContentProvider 運行在主進程當中祝高,所以,如果你的應用中如果用到了多進程污筷,那么默認的 ContentProvider 不會為你的子進程做初始化工闺,除非你明確指定它的進程。
所以颓屑,除了 ContentProvider斤寂,你還可以使用手動聲明的方式,
AndroidStartup.newInstance(this).jobs(
CrashHelperInitializeJob(),
ThirdPartLibrariesInitializeJob(),
DependentBlockingBackgroundJob(),
BlockingBackgroundJob()
).launch()
此外揪惦,我還特意增加了注解的方式聲明任務遍搞。使用起來很簡單,你只需要在自己的任務上面使用注解聲明即可器腋,如:
@StartupJob
class BlockingBackgroundJob : ISchedulerJob {
override fun threadMode(): ThreadMode = ThreadMode.BACKGROUND
override fun dependencies(): List<Class<out ISchedulerJob>> = emptyList()
override fun run(context: Context) {
Thread.sleep(5_000L) // 5 seconds
L.d("BlockingBackgroundJob done! ${Thread.currentThread()}")
}
}
它的工作原理也比較簡單溪猿,就是當你調用 AndroidStartup 的 scanAnnotations()
方法的時候,它會通過反射調用 JobHunter 的方法獲取所有的任務纫塌。在編譯期間诊县,我們會為這個接口提供實現(xiàn),并會對所有掃描到的任務進行初始化并在該實現(xiàn)中返回措左。
總結
以上就是這個庫的實現(xiàn)原理依痊。