├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── values-night
│ │ │ │ └── themes.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── layout
│ │ │ │ └── activity_main.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── geelee
│ │ │ │ └── startup
│ │ │ │ └── demo
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── MyApplication.kt
│ │ │ │ ├── BaseLogInitializer.kt
│ │ │ │ └── Initializers.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── geelee
│ │ │ └── startup
│ │ │ └── demo
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── geelee
│ │ └── startup
│ │ └── demo
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── startup
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── geelee
│ │ │ └── startup
│ │ │ ├── Initializer.kt
│ │ │ ├── IStartupLogger.kt
│ │ │ ├── ktx
│ │ │ ├── ComponentInfo.kt
│ │ │ └── Context.kt
│ │ │ └── Startup.kt
│ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── geelee
│ │ │ └── startup
│ │ │ └── ExampleInstrumentedTest.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── geelee
│ │ └── startup
│ │ ├── StartupTest.kt
│ │ └── DependencyChainBuilderTest.kt
├── proguard-rules.pro
└── build.gradle
├── startup-annotation
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── geelee
│ └── startup
│ └── annotation
│ ├── Config.kt
│ ├── IInitializerRegistry.kt
│ └── model
│ ├── DependencyChain.kt
│ └── ComponentInfo.kt
├── startup-processor
├── .gitignore
├── src
│ └── main
│ │ └── java
│ │ └── com
│ │ └── geelee
│ │ └── startup
│ │ └── processor
│ │ ├── ktx
│ │ ├── Iterable.kt
│ │ ├── ComponentInfo.kt
│ │ └── Literal.kt
│ │ ├── ILogger.kt
│ │ ├── exception
│ │ ├── IllegalAnnotationException.kt
│ │ ├── CycleDependencyException.kt
│ │ ├── IllegalThreadException.kt
│ │ └── IllegalProcessException.kt
│ │ ├── InitializerReporter.kt
│ │ ├── DependencyChainBuilder.kt
│ │ └── InitializerProcessor.kt
└── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── gradle.properties
├── README-CN.md
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/startup/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/startup/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/startup-annotation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/startup-processor/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Startup
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeeJoe/Startup/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
--------------------------------------------------------------------------------
/startup/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF000000
4 | #FFFFFFFF
5 |
--------------------------------------------------------------------------------
/startup/src/main/java/com/geelee/startup/Initializer.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup
2 |
3 | import android.content.Context
4 |
5 | interface Initializer {
6 | fun init(context: Context, processName: String)
7 | }
--------------------------------------------------------------------------------
/startup-annotation/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | id 'org.jetbrains.kotlin.jvm'
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_8
8 | targetCompatibility = JavaVersion.VERSION_1_8
9 | }
10 |
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/ktx/Iterable.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor.ktx
2 |
3 | operator fun Iterable.contains(condition: (T) -> Boolean): Boolean {
4 | return find {
5 | condition(it)
6 | } != null
7 | }
8 |
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/ILogger.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor
2 |
3 |
4 | /**
5 | * Created by zhiyueli on 2021/6/30.
6 | */
7 | interface ILogger {
8 | fun i(msg: String)
9 | fun e(msg: String, throwable: Throwable)
10 | }
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/exception/IllegalAnnotationException.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor.exception
2 |
3 |
4 | /**
5 | * Created by zhiyueli on 2021/5/29.
6 | *
7 | * 注解使用错误时抛出异常
8 | */
9 | class IllegalAnnotationException(msg: String) : Exception(msg)
--------------------------------------------------------------------------------
/startup/src/main/java/com/geelee/startup/IStartupLogger.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup
2 |
3 |
4 | /**
5 | * Created by zhiyueli on 2021/6/30.
6 | */
7 | interface IStartupLogger {
8 | fun i(msg: String)
9 | fun e(msg: String, throwable: Throwable)
10 | fun isDebugVersion(): Boolean
11 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | rootProject.name = "Startup"
10 | include ':app'
11 | include ':startup'
12 | include ':startup-annotation'
13 | include ':startup-processor'
14 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Nov 13 16:05:10 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 | kapt.use.worker.api=true
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/geelee/startup/demo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.demo
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import android.os.Bundle
5 |
6 | class MainActivity : AppCompatActivity() {
7 | override fun onCreate(savedInstanceState: Bundle?) {
8 | super.onCreate(savedInstanceState)
9 | setContentView(R.layout.activity_main)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/test/java/com/geelee/startup/demo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.demo
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geelee/startup/demo/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.demo
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import com.geelee.startup.Startup
6 |
7 | /**
8 | * Created by zhiyueli on 11/13/23 17:28.
9 | */
10 | class MyApplication : Application() {
11 |
12 | override fun attachBaseContext(base: Context) {
13 | super.attachBaseContext(base)
14 | Startup.init(base)
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/exception/CycleDependencyException.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor.exception
2 |
3 | /**
4 | * The Runtime Exception thrown by the android.startup library.
5 | *
6 | * 当有循环依赖的时候抛出异常
7 | */
8 | class CycleDependencyException : RuntimeException {
9 | constructor(message: String) : super(message) {}
10 | constructor(throwable: Throwable) : super(throwable) {}
11 | constructor(message: String, throwable: Throwable) : super(message, throwable) {}
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/geelee/startup/demo/BaseLogInitializer.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.demo
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import com.geelee.startup.Initializer
6 |
7 | /**
8 | * Created by zhiyueli on 11/13/23 17:52.
9 | */
10 | open class BaseLogInitializer : Initializer {
11 | override fun init(context: Context, processName: String) {
12 | Log.i(
13 | "Startup",
14 | "${this::class.simpleName} curThread=${Thread.currentThread()} curProcess=${processName}"
15 | )
16 | }
17 | }
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/exception/IllegalThreadException.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor.exception
2 |
3 | /**
4 | * The Runtime Exception thrown by the android.startup library.
5 | *
6 | * 当定义的线程信息不合法的时候抛出异常
7 | *
8 | * A: 在主线程初始化
9 | * B: 在子线程初始化
10 | * A 初始化器不能依赖于 B 初始化器
11 | */
12 | class IllegalThreadException : RuntimeException {
13 | constructor(message: String) : super(message) {}
14 | constructor(throwable: Throwable) : super(throwable) {}
15 | constructor(message: String, throwable: Throwable) : super(message, throwable) {}
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/geelee/startup/demo/Initializers.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.demo
2 |
3 | import com.geelee.startup.annotation.Config
4 | import com.geelee.startup.annotation.model.ComponentInfo
5 |
6 | /**
7 | * Created by zhiyueli on 11/13/23 17:52.
8 | */
9 | @Config(
10 | dependencies = [BInitializer::class],
11 | threadMode = ComponentInfo.ThreadMode.WorkThread,
12 | supportProcess = ["sub"]
13 | )
14 | class AInitializer : BaseLogInitializer()
15 |
16 | @Config(dependencies = [CInitializer::class])
17 | class BInitializer : BaseLogInitializer()
18 |
19 | @Config
20 | class CInitializer : BaseLogInitializer()
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/exception/IllegalProcessException.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor.exception
2 |
3 | /**
4 | * The Runtime Exception thrown by the android.startup library.
5 | *
6 | * 当定义的进程信息不合法的时候抛出异常
7 | *
8 | * A 初始化器依赖于 B 初始化器
9 | * A: 子初始化器
10 | * B: 父初始化器
11 | * 父初始化器的 supportProcess 必须大于等于子初始化器.
12 | */
13 | class IllegalProcessException : RuntimeException {
14 | constructor(message: String) : super(message) {}
15 | constructor(throwable: Throwable) : super(throwable) {}
16 | constructor(message: String, throwable: Throwable) : super(message, throwable) {}
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/startup/src/main/java/com/geelee/startup/ktx/ComponentInfo.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.ktx
2 |
3 | import android.content.Context
4 | import com.geelee.startup.annotation.IInitializerRegistry.Companion.ALL_PROCESS
5 | import com.geelee.startup.annotation.model.ComponentInfo
6 |
7 | /**
8 | * 判断进程是否在支持列表中
9 | */
10 | fun ComponentInfo.isSupportProcess(context: Context, process: String): Boolean {
11 | // 空代表主进程
12 | if (this.supportProcess.isEmpty()) {
13 | return context.mainProcessName() == process
14 | }
15 | // ALL_PROCESS 代表所有进程
16 | if (this.supportProcess.contains(ALL_PROCESS)) {
17 | return true
18 | }
19 | return supportProcess.contains(process)
20 | }
21 |
--------------------------------------------------------------------------------
/startup/src/main/java/com/geelee/startup/ktx/Context.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.ktx
2 |
3 | import android.app.ActivityManager
4 | import android.content.Context
5 | import android.os.Process
6 |
7 |
8 | fun Context.processName() = getProcessName(this, Process.myPid()) ?: ""
9 | internal fun Context.mainProcessName() = this.packageName
10 |
11 | private fun getProcessName(cxt: Context, pid: Int): String? {
12 | val am = cxt.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
13 | val runningApps = am.runningAppProcesses ?: return null
14 | for (processInfo in runningApps) {
15 | if (processInfo.pid == pid) {
16 | return processInfo.processName
17 | }
18 | }
19 | return null
20 | }
21 |
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/ktx/ComponentInfo.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor.ktx
2 |
3 | import com.geelee.startup.annotation.model.ComponentInfo
4 |
5 | internal fun List.toFullString(): String {
6 | val sb = StringBuilder("[")
7 | this.forEach {
8 | sb.append(it.toString())
9 | .append(",")
10 | }
11 | sb.removeSuffix(",")
12 | sb.append("]")
13 | return sb.toString()
14 | }
15 |
16 | internal fun List.toSimpleString(): String {
17 | val sb = StringBuilder("[")
18 | this.forEach {
19 | sb.append(it.toSimpleString())
20 | .append(",")
21 | }
22 | sb.removeSuffix(",")
23 | sb.append("]")
24 | return sb.toString()
25 | }
26 |
--------------------------------------------------------------------------------
/startup/src/androidTest/java/com/geelee/startup/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.geelee.startup.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/geelee/startup/demo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.demo
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.geelee.startup.demo", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/startup-annotation/src/main/java/com/geelee/startup/annotation/Config.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.annotation
2 |
3 | import com.geelee.startup.annotation.model.ComponentInfo
4 | import kotlin.reflect.KClass
5 |
6 |
7 | /**
8 | * Created by zhiyueli on 2021/5/29.
9 | *
10 | * 用于标注一个 Initializer, Startup 库会自动将此 Initializer 注册到 [IInitializerRegistry] 中
11 | *
12 | * @param dependencies 依赖列表
13 | * @param threadMode 在指定线程下初始化, 默认主线程
14 | * @param supportProcess 支持在哪些进程初始化,如果不指定进程,默认只在主进程初始化, "*" 表示所有进程
15 | */
16 | @Target(AnnotationTarget.CLASS)
17 | @Retention(AnnotationRetention.RUNTIME)
18 | @MustBeDocumented
19 | annotation class Config(
20 | val dependencies: Array> = [],
21 | val threadMode: ComponentInfo.ThreadMode = ComponentInfo.ThreadMode.MainThread,
22 | val supportProcess: Array = []
23 | )
24 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/startup/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
--------------------------------------------------------------------------------
/startup-annotation/src/main/java/com/geelee/startup/annotation/IInitializerRegistry.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.annotation
2 |
3 | import com.geelee.startup.annotation.model.ComponentInfo
4 | import com.geelee.startup.annotation.model.DependencyChain
5 |
6 |
7 | /**
8 | * Created by zhiyueli on 2021/5/29.
9 | */
10 | interface IInitializerRegistry {
11 | companion object {
12 | const val GENERATED_CLASS_PACKAGE_NAME = "com.geelee.startup"
13 | const val GENERATED_CLASS_NAME = "InitializerRegistry"
14 | const val ALL_PROCESS = "*"
15 | const val MAIN_PROCESS = "MAIN_PROCESS"
16 | }
17 |
18 | /**
19 | * 获取所有注册的初始化器信息
20 | */
21 | fun getAllInitializer(): Map
22 |
23 | /**
24 | * 获取指定名称的初始化器信息
25 | */
26 | fun getInitializerByName(name: String): ComponentInfo? {
27 | return getAllInitializer()[name]
28 | }
29 |
30 | fun getMainThreadComponentChainList(): List
31 |
32 | fun getWorkThreadComponentChainList(): List
33 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/startup-annotation/src/main/java/com/geelee/startup/annotation/model/DependencyChain.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.annotation.model
2 |
3 |
4 | /**
5 | * Created by zhiyueli on 2021/8/10.
6 | *
7 | * 初始化器依赖链, 单向无循环依赖,用一个有序的 LinkedHashSet 表示
8 | */
9 | data class DependencyChain(val chain: LinkedHashSet) {
10 |
11 | companion object {
12 | private const val CHAIN = " -> "
13 | }
14 |
15 | override fun toString(): String {
16 | return chain.toReadableString()
17 | }
18 |
19 | fun toDetailString(): String {
20 | val sb = StringBuilder()
21 | chain.reversed().forEach {
22 | sb.append(it.toDetailString())
23 | sb.append(CHAIN)
24 | }
25 | return sb.removeSuffix(CHAIN).toString()
26 | }
27 |
28 | private fun Set.toReadableString(): String {
29 | val sb = StringBuilder()
30 | this.reversed().forEach {
31 | sb.append(it.toSimpleString())
32 | sb.append(CHAIN)
33 | }
34 | return sb.removeSuffix(CHAIN).toString()
35 | }
36 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 | namespace 'com.geelee.startup.demo'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | applicationId "com.geelee.startup.demo"
13 | minSdk 24
14 | targetSdk 33
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | }
35 |
36 | dependencies {
37 |
38 | implementation 'androidx.core:core-ktx:1.5.0'
39 | implementation 'androidx.appcompat:appcompat:1.6.1'
40 | implementation 'com.google.android.material:material:1.5.0'
41 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
42 |
43 | implementation "io.github.geejoe:startup:0.0.4"
44 | implementation "io.github.geejoe:startup-annotation:0.0.4"
45 | kapt "io.github.geejoe:startup-processor:0.0.4"
46 |
47 | testImplementation 'junit:junit:4.13.2'
48 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
49 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
50 | }
--------------------------------------------------------------------------------
/startup-processor/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | id 'org.jetbrains.kotlin.jvm'
4 | id 'kotlin-kapt'
5 | id 'maven-publish'
6 | }
7 |
8 | java {
9 | sourceCompatibility = JavaVersion.VERSION_1_8
10 | targetCompatibility = JavaVersion.VERSION_1_8
11 | }
12 |
13 | dependencies {
14 | implementation project(":startup-annotation")
15 | implementation "com.squareup:kotlinpoet:1.7.2"
16 | implementation "com.google.auto.service:auto-service:1.0"
17 | kapt "com.google.auto.service:auto-service:1.0"
18 | }
19 |
20 | ext["ossrhUsername"] = ''
21 | ext["ossrhPassword"] = ''
22 |
23 | File secretPropsFile = project.rootProject.file('local.properties')
24 | if (secretPropsFile.exists()) {
25 | println "Found secret props file, loading props"
26 | Properties p = new Properties()
27 | p.load(new FileInputStream(secretPropsFile))
28 | p.each { name, value ->
29 | ext[name] = value
30 | }
31 | } else {
32 | println "No props file, loading env vars"
33 | }
34 |
35 | publishing {
36 | repositories {
37 | maven {
38 | credentials {
39 | username ossrhUsername
40 | password ossrhPassword
41 | }
42 | url 'https://s01.oss.sonatype.org/content/repositories/releases/'
43 | }
44 | }
45 |
46 | publications {
47 | maven(MavenPublication) {
48 | artifact("$buildDir/libs/${project.getName()}.jar")
49 | groupId 'io.github.geejoe'
50 | artifactId 'startup-processor'
51 | version '0.0.4'
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/InitializerReporter.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor
2 |
3 | import com.geelee.startup.annotation.model.DependencyChain
4 | import java.io.File
5 | import javax.annotation.processing.ProcessingEnvironment
6 | import javax.tools.StandardLocation
7 |
8 |
9 | /**
10 | * Created by zhiyueli on 2021/8/13.
11 | *
12 | * 初始化器报告生成器,将在 doc 目录下生成初始化器注册表
13 | */
14 | class InitializerReporter(
15 | private val mainThreadComponentChainList: List,
16 | private val workThreadComponentChainList: List
17 | ) {
18 |
19 | fun outputReport(processingEnv: ProcessingEnvironment) {
20 | val sb = StringBuilder("*Auto generated by APT. DO NOT EDIT!*").appendln()
21 | sb.appendln(mainThreadComponentChainList.listToString("## 主线程初始化器依赖链:"))
22 | sb.appendln(workThreadComponentChainList.listToString("## 子线程初始化器依赖链:"))
23 | val reportFile = processingEnv.filer.createResource(
24 | StandardLocation.SOURCE_OUTPUT,
25 | "",
26 | "report.md"
27 | )
28 | File(reportFile.toUri()).writeText(sb.toString())
29 | }
30 |
31 | private fun List.listToString(title: String): String {
32 | if (this.isEmpty()) {
33 | return ""
34 | }
35 | val sb = StringBuilder()
36 | sb.appendln(title)
37 | sb.appendln("#### 初始化顺序:从上往下,有依赖关系的,从右往左")
38 | this.forEachIndexed { index, dependencyChain ->
39 | sb.appendln("${index + 1}. ${dependencyChain.toDetailString()}")
40 | }
41 | return sb.toString()
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/README-CN.md:
--------------------------------------------------------------------------------
1 | # 这是什么
2 |
3 | 应用启动初始化器,方便在应用启动阶段初始化各种逻辑
4 |
5 | # 有什么用
6 |
7 | - 指定初始化依赖关系
8 | - 指定初始化执行的进程和线程
9 | - 编译时检测依赖关系
10 | - 快速上手,灵活和简单的配置
11 | - 内部逻辑有充分的单元测试保证
12 |
13 | # 怎么用
14 |
15 | 1. 应用启动的时候执行库的初始化
16 |
17 | ```kotlin
18 | class MyApplication : Application() {
19 |
20 | override fun attachBaseContext(base: Context) {
21 | super.attachBaseContext(base)
22 | Startup.init(base)
23 | }
24 | }
25 | ```
26 |
27 | 2. 定义自己的初始化逻辑
28 |
29 | ```kotlin
30 | @Initializer
31 | class LogInitializer : Initializer {
32 | override fun init(context: Context, processName: String) {
33 | // ...
34 | }
35 | }
36 | ```
37 |
38 |
39 |
40 | 3. 根据需要配置依赖项、执行的进程和线程
41 |
42 | ```kotlin
43 | // 配置依赖项、运行进程、线程
44 | @Initializer(
45 | dependencies = [BInitializer::class, CInitializer::class],
46 | threadMode = ComponentInfo.ThreadMode.WorkThread,
47 | supportProcess = ["sub", "main"]
48 | )
49 | class LogInitializer : Initializer {
50 | override fun init(context: Context, processName: String) {
51 | // ...
52 | }
53 | }
54 | ```
55 |
56 | > - 一些优先级不那么高的耗时初始化逻辑,可以放在子线程中执行来提高启动速度
57 | > - 配置在子线程执行的初始化器会在启动时并行执行
58 | > - 配置在主线程的初始化器会在启动时串行执行
59 | > - 有依赖关系的初始化器会保证执行的先后顺序
60 |
61 | # 编译时检测
62 |
63 | 如果有循环依赖、不合理线程、进程配置关系,会在编译时给出错误信息,快速定位问题
64 |
65 | ```kotlin
66 | // 自己依赖自己,会在编译时报错 CycleDependencyException
67 | @Config(dependencies = [CycleInit::class])
68 | class CycleInit : Initializer {
69 | override fun init(context: Context, processName: String) {
70 | // no-op
71 | }
72 | }
73 | ```
74 |
75 | 除了循环依赖检测,还支持以下编译时检测:
76 |
77 | 1. `IllegalAnnotationException` 注解使用错误时抛出异常。只能注解到实现了 `Initializer` 接口的类上
78 | 2. `IllegalProcessException` 当定义的进程信息不合法的时候抛出异常。比如 A 初始化器依赖于 B 初始化器;B 初始化器的 supportProcess 集合必须大于等于 A 初始化器
79 | 3. `IllegalThreadException` 当定义的线程信息不合法的时候抛出异常。比如 A 在主线程初始化,B 在子线程初始化, A 不能依赖 B
--------------------------------------------------------------------------------
/startup-annotation/src/main/java/com/geelee/startup/annotation/model/ComponentInfo.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.annotation.model
2 |
3 | import com.geelee.startup.annotation.IInitializerRegistry.Companion.ALL_PROCESS
4 | import com.geelee.startup.annotation.model.ComponentInfo.ThreadMode.MainThread
5 | import com.geelee.startup.annotation.model.ComponentInfo.ThreadMode.WorkThread
6 |
7 | /**
8 | * @param name 初始化组件名字
9 | * @param supportProcess 支持在指定进程列表初始化
10 | * @param threadMode 在指定线程下初始化
11 | * @param dependencies 依赖列表
12 | * @param instanceProvider 获取实例的 lambda
13 | */
14 | data class ComponentInfo(
15 | val name: String,
16 | val supportProcess: List = emptyList(),
17 | val threadMode: ThreadMode = MainThread,
18 | val dependencies: List = emptyList(),
19 | val instanceProvider: () -> Any
20 | ): Comparable {
21 | val instance by lazy {
22 | instanceProvider()
23 | }
24 |
25 | val simpleName by lazy {
26 | name.split(".").last().replaceFirstChar { char -> char.lowercaseChar() }
27 | }
28 |
29 | override fun compareTo(other: ComponentInfo): Int {
30 | return name.compareTo(other.name)
31 | }
32 |
33 | override fun toString(): String {
34 | return "ComponentInfo(name='$name', supportProcess=$supportProcess, threadMode=$threadMode)"
35 | }
36 |
37 | fun toSimpleString(): String {
38 | return name.split(".").last()
39 | }
40 |
41 | fun toDetailString(): String {
42 | val processList = when {
43 | supportProcess.isEmpty() -> "[MAIN_PROCESS]"
44 | supportProcess.contains(ALL_PROCESS) -> "[ALL_PROCESS]"
45 | else -> supportProcess.toTypedArray().contentToString()
46 | }
47 | return "${toSimpleString()}$processList"
48 | }
49 |
50 | /**
51 | * 所有在 [MainThread] 模式下的初始化器串行执行初始化
52 | * 所有在 [WorkThread] 模式下的初始化器并行执行初始化
53 | */
54 | enum class ThreadMode {
55 | MainThread,
56 | WorkThread
57 | }
58 | }
--------------------------------------------------------------------------------
/startup/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace 'com.geelee.startup'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | minSdk 24
13 | targetSdk 33
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | }
19 |
20 | compileOptions {
21 | sourceCompatibility JavaVersion.VERSION_1_8
22 | targetCompatibility JavaVersion.VERSION_1_8
23 | }
24 | kotlinOptions {
25 | jvmTarget = '1.8'
26 | }
27 | }
28 |
29 | dependencies {
30 | api project(":startup-annotation")
31 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
32 |
33 | testImplementation project(":startup-processor")
34 | testImplementation 'junit:junit:4.13.2'
35 | testImplementation "org.robolectric:robolectric:4.9"
36 | testImplementation "androidx.test:runner:1.5.2"
37 |
38 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
40 | }
41 |
42 | ext["ossrhUsername"] = ''
43 | ext["ossrhPassword"] = ''
44 |
45 | File secretPropsFile = project.rootProject.file('local.properties')
46 | if (secretPropsFile.exists()) {
47 | println "Found secret props file, loading props"
48 | Properties p = new Properties()
49 | p.load(new FileInputStream(secretPropsFile))
50 | p.each { name, value ->
51 | ext[name] = value
52 | }
53 | } else {
54 | println "No props file, loading env vars"
55 | }
56 |
57 | publishing {
58 | repositories {
59 | maven {
60 | credentials {
61 | username ossrhUsername
62 | password ossrhPassword
63 | }
64 | url 'https://s01.oss.sonatype.org/content/repositories/releases/'
65 | }
66 | }
67 |
68 | publications {
69 | maven(MavenPublication) {
70 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
71 | groupId 'io.github.geejoe'
72 | artifactId 'startup'
73 | version '0.0.4'
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/ktx/Literal.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor.ktx
2 |
3 | import com.geelee.startup.annotation.model.ComponentInfo
4 | import com.geelee.startup.annotation.model.DependencyChain
5 |
6 |
7 | /**
8 | * 根据 ComponentInfo 构造实例化代码
9 | */
10 | internal fun ComponentInfo.toLiteral(): String {
11 | val instanceLambda = "{ ${name}() }"
12 | val threadMode = "${ComponentInfo.ThreadMode::class.qualifiedName}.${threadMode}"
13 | return StringBuilder()
14 | .appendln("${ComponentInfo::class.simpleName}(")
15 | .appendln("\"${name}\",")
16 | .appendln("${supportProcess.toLiteral()},")
17 | .appendln("${threadMode},")
18 | .appendln("${dependencies.toLiteral()},")
19 | .append("${instanceLambda})")
20 | .toString()
21 | }
22 |
23 | /**
24 | * 根据 String 列表构造 listOf 代码块
25 | */
26 | internal fun List.toLiteral(): String {
27 | if (this.isEmpty()) return "emptyList()"
28 | val sb = StringBuilder()
29 | sb.append("listOf(")
30 | this.forEach {
31 | sb.append("\"${it}\", ")
32 | }
33 | sb.replace(sb.length - 2, sb.length, ")")
34 | return sb.toString()
35 | }
36 |
37 | /**
38 | * 根据 Component 列表构造 mapOf 代码块
39 | */
40 | internal fun List.toMapLiteral(): String {
41 | if (this.isEmpty()) return "emptyMap()"
42 | val sb = StringBuilder("mapOf(").appendln()
43 | this.forEach {
44 | sb.append("\"${it.name}\" to ${it.simpleName}").appendln(", ")
45 | }
46 | sb.replace(sb.length - 2, sb.length, ")")
47 | return sb.toString()
48 | }
49 |
50 | /**
51 | * 根据 Component 列表构造 listOf 代码块
52 | */
53 | internal fun List.toListLiteral(): String {
54 | if (this.isEmpty()) return "emptyList()"
55 | val sb = StringBuilder("listOf(").appendln()
56 | this.forEach {
57 | sb.append(it.toLiteral()).appendln(", ")
58 | }
59 | sb.replace(sb.length - 2, sb.length, ")")
60 | return sb.toString()
61 | }
62 |
63 | /**
64 | * 根据 DependencyChain 构造实例化代码
65 | */
66 | internal fun DependencyChain.toLiteral(): String {
67 | val set = StringBuilder("linkedSetOf(")
68 | chain.forEach { componentInfo ->
69 | val propertyName = componentInfo.simpleName
70 | set.append(propertyName).append(", ")
71 | }
72 | set.replace(set.length - 2, set.length, ")")
73 | return "${DependencyChain::class.simpleName}($set)"
74 | }
75 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [中文 Readme.md](README-CN.md)
2 |
3 | # What is it
4 |
5 | Application startup initializer, which makes it easy to initialize various logics during the application startup phase.
6 |
7 | # What can it do
8 |
9 | - Specify initialization dependencies
10 | - Specify the process and thread for initialization execution
11 | - Compile-time dependency detection
12 | - Quick start, flexible and simple configuration
13 | - The internal logic is fully guaranteed by unit tests.
14 |
15 | # How to use
16 |
17 | 1. Execute the library's initialization when the application starts
18 |
19 | ```kotlin
20 | class MyApplication : Application() {
21 |
22 | override fun attachBaseContext(base: Context) {
23 | super.attachBaseContext(base)
24 | Startup.init(base)
25 | }
26 | }
27 | ```
28 |
29 | 2. Define your own initialization logic
30 |
31 | ```kotlin
32 | @Initializer
33 | class LogInitializer : Initializer {
34 | override fun init(context: Context, processName: String) {
35 | // ...
36 | }
37 | }
38 | ```
39 |
40 | 3. Configure dependencies, execution processes, and threads as needed
41 |
42 | ```kotlin
43 | // Configure dependencies, runtime processes, and threads
44 | @Initializer(
45 | dependencies = [BInitializer::class, CInitializer::class],
46 | threadMode = ComponentInfo.ThreadMode.WorkThread,
47 | supportProcess = ["sub", "main"]
48 | )
49 | class LogInitializer : Initializer {
50 | override fun init(context: Context, processName: String) {
51 | // ...
52 | }
53 | }
54 | ```
55 |
56 | > - Some time-consuming initialization logic with lower priority can be executed in sub-threads to improve startup speed
57 | > - Initializers configured to run in sub-threads will be executed in parallel during startup
58 | > - Initializers configured to run in the main thread will be executed serially during startup
59 | > - Initializers with dependencies will ensure the order of execution
60 |
61 | # Compile-time detection
62 |
63 | If there are circular dependencies, unreasonable thread, and process configuration relationships, error messages will be given during compilation to quickly locate the problem
64 |
65 | ```kotlin
66 | // Self-dependency, a CycleDependencyException will be reported at compile time
67 | @Config(dependencies = [CycleInit::class])
68 | class CycleInit : Initializer {
69 | override fun init(context: Context, processName: String) {
70 | // no-op
71 | }
72 | }
73 | ```
74 |
75 | In addition to circular dependency detection, the following compile-time detections are also supported:
76 |
77 | 1. `IllegalAnnotationException` is thrown when the annotation is used incorrectly. It can only be annotated on classes that implement the `Initializer` interface
78 | 2. `IllegalProcessException` is thrown when the defined process information is illegal. For example, initializer A depends on initializer B; the supportProcess collection of initializer B must be greater than or equal to that of initializer A
79 | 3. `IllegalThreadException` is thrown when the defined thread information is illegal. For example, A initializes in the main thread, B initializes in the sub-thread, and A cannot depend on B
--------------------------------------------------------------------------------
/startup/src/main/java/com/geelee/startup/Startup.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import com.geelee.startup.annotation.IInitializerRegistry
6 | import com.geelee.startup.annotation.IInitializerRegistry.Companion.GENERATED_CLASS_NAME
7 | import com.geelee.startup.annotation.IInitializerRegistry.Companion.GENERATED_CLASS_PACKAGE_NAME
8 | import com.geelee.startup.annotation.model.ComponentInfo
9 | import com.geelee.startup.annotation.model.DependencyChain
10 | import com.geelee.startup.ktx.isSupportProcess
11 | import com.geelee.startup.ktx.processName
12 | import kotlinx.coroutines.Deferred
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.MainScope
15 | import kotlinx.coroutines.async
16 | import kotlinx.coroutines.launch
17 | import kotlinx.coroutines.withContext
18 |
19 | /**
20 | * 扩展自官方的 [App Startup](https://developer.android.com/topic/libraries/app-startup)
21 | * - 1. 提供组件初始化能力
22 | * - 2. 支持配置初始化依赖顺序
23 | * - 3. 解决官方使用 `ContentProvider` 无法支持多进程的问题
24 | * - 4. 解决官方需要手动注册初始化器的问题, 支持注解方式自动注册
25 | * - 5. 支持指定进程初始化
26 | *
27 | * 使用方式
28 | * Step 1: 实现 [com.geelee.startup.Initializer] 接口,在实现类中添加自己组件的初始化逻辑
29 | * Step 2: 实现类加上注解 [com.geelee.startup.annotation.Config]
30 | */
31 | class Startup private constructor(
32 | private val appContext: Context,
33 | private val registry: IInitializerRegistry,
34 | private val logger: IStartupLogger
35 | ) {
36 |
37 | private val currentProcess = appContext.processName()
38 |
39 | fun init() {
40 | MainScope().launch {
41 | initInMainThread(currentProcess)
42 | initInWorkThreadAsync(currentProcess)
43 | }
44 | }
45 |
46 | /**
47 | * 子线程的依赖链之间并行执行
48 | */
49 | private suspend fun initInWorkThreadAsync(currentProcess: String) =
50 | withContext(Dispatchers.Default) {
51 | val taskInParallel = mutableListOf>()
52 | registry.getWorkThreadComponentChainList().forEach {
53 | taskInParallel.add(async { it.initOneByOneAsync(currentProcess) })
54 | }
55 | taskInParallel.forEach { it.await() }
56 | }
57 |
58 | /**
59 | * 主线程的依赖链之间串行执行
60 | */
61 | fun initInMainThread(currentProcess: String) {
62 | registry.getMainThreadComponentChainList().forEach {
63 | it.initOneByOne(currentProcess)
64 | }
65 | }
66 |
67 | /**
68 | * 依次执行链表中每个初始化器的初始化逻辑
69 | */
70 | private fun DependencyChain.initOneByOne(currentProcess: String) {
71 | this.chain.forEach { it.init(currentProcess) }
72 | }
73 |
74 | /**
75 | * 依次执行链表中每个初始化器的初始化逻辑(异步方法)
76 | */
77 | private suspend fun DependencyChain.initOneByOneAsync(currentProcess: String) {
78 | val dependencyChain = this
79 | withContext(Dispatchers.Default) {
80 | dependencyChain.initOneByOne(currentProcess)
81 | }
82 | }
83 |
84 | /**
85 | * 执行单个初始化器的初始化操作
86 | */
87 | private fun ComponentInfo.init(currentProcess: String) {
88 | if (!isSupportProcess(appContext, currentProcess)) {
89 | logger.i(
90 | "Skip ${this.name} cause it suppose to init at process${
91 | this.supportProcess.toTypedArray().contentToString()
92 | } but the current process is $currentProcess"
93 | )
94 | return
95 | }
96 | try {
97 | val initializer = this.instance as Initializer
98 | logger.i(String.format("Initializing %s at %s", this.name, Thread.currentThread()))
99 | initializer.init(appContext, currentProcess)
100 | logger.i(String.format("Initialized %s at %s", this.name, Thread.currentThread()))
101 | } catch (e: Throwable) {
102 | if (logger.isDebugVersion()) {
103 | throw e
104 | } else {
105 | logger.e("init failed: ${e.message}", e)
106 | }
107 | }
108 | }
109 |
110 | companion object {
111 | @JvmStatic
112 | fun build(
113 | context: Context,
114 | registry: IInitializerRegistry = Class.forName("${GENERATED_CLASS_PACKAGE_NAME}.${GENERATED_CLASS_NAME}")
115 | .newInstance() as IInitializerRegistry,
116 | logger: IStartupLogger = DefaultLogger()
117 | ): Startup {
118 | val appContext = context.applicationContext ?: context
119 | return Startup(appContext, registry, logger)
120 | }
121 |
122 | @JvmStatic
123 | fun init(context: Context) {
124 | build(context).init()
125 | }
126 | }
127 | }
128 |
129 | private class DefaultLogger : IStartupLogger {
130 | override fun i(msg: String) {
131 | Log.i("Startup-Default-Logger", msg)
132 | }
133 |
134 | override fun e(msg: String, throwable: Throwable) {
135 | Log.e("Startup-Default-Logger", msg, throwable)
136 | }
137 |
138 | override fun isDebugVersion(): Boolean {
139 | return BuildConfig.DEBUG
140 | }
141 |
142 | }
--------------------------------------------------------------------------------
/startup/src/test/java/com/geelee/startup/StartupTest.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup
2 |
3 | import android.content.Context
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import androidx.test.runner.AndroidJUnit4
6 | import com.geelee.startup.annotation.IInitializerRegistry
7 | import com.geelee.startup.annotation.IInitializerRegistry.Companion.ALL_PROCESS
8 | import com.geelee.startup.annotation.model.ComponentInfo
9 | import com.geelee.startup.annotation.model.DependencyChain
10 | import org.hamcrest.MatcherAssert.assertThat
11 | import org.hamcrest.core.IsEqual
12 | import org.hamcrest.core.IsInstanceOf
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 | import org.robolectric.annotation.Config
16 |
17 | /**
18 | * Created by zhiyueli on 2021/8/9.
19 | */
20 | @RunWith(AndroidJUnit4::class)
21 | @Config(manifest = Config.NONE)
22 | class StartupTest {
23 |
24 | private class MockException(msg: String) : RuntimeException(msg)
25 |
26 | private val context = InstrumentationRegistry.getInstrumentation().targetContext
27 | private val logger = object : IStartupLogger {
28 | override fun i(msg: String) {
29 | println(msg)
30 | }
31 |
32 | override fun e(msg: String, throwable: Throwable) {
33 | println(msg)
34 | errorExceptionInLog = throwable
35 | }
36 |
37 | override fun isDebugVersion(): Boolean {
38 | return isDebugVersion
39 | }
40 | }
41 | private var isDebugVersion = false
42 | private var errorExceptionInLog: Throwable? = null
43 |
44 | private val registry = object : IInitializerRegistry {
45 | override fun getAllInitializer(): Map {
46 | return allInitializer
47 | }
48 |
49 | override fun getMainThreadComponentChainList(): List {
50 | return mainThreadComponentChainList
51 | }
52 |
53 | override fun getWorkThreadComponentChainList(): List {
54 | return emptyList()
55 | }
56 | }
57 |
58 | private lateinit var allInitializer: Map
59 | private lateinit var mainThreadComponentChainList: List
60 |
61 | var cInitialized = false
62 | var dInitialized = false
63 |
64 | inner class InitializerC : Initializer {
65 | override fun init(context: Context, processName: String) {
66 | cInitialized = true
67 | }
68 | }
69 |
70 | inner class InitializerD : Initializer {
71 | override fun init(context: Context, processName: String) {
72 | dInitialized = true
73 | }
74 | }
75 |
76 | inner class InitializerWithException : Initializer {
77 | override fun init(context: Context, processName: String) {
78 | throw MockException("mock exception")
79 | }
80 | }
81 |
82 | /**
83 | * C 初始化器在所有进程初始化
84 | * D 初始化器只在 processA 进程初始化
85 | *
86 | * 期望:C 能初始化,D 不能初始化
87 | */
88 | @Test
89 | fun `测试在非指定进程是否能初始化`() {
90 | // given
91 | val c = ComponentInfo(
92 | name = InitializerC::class.java.name,
93 | instanceProvider = { InitializerC() },
94 | supportProcess = listOf(ALL_PROCESS) // 所有进程
95 | )
96 | val d = ComponentInfo(
97 | name = InitializerD::class.java.name,
98 | instanceProvider = { InitializerD() },
99 | supportProcess = listOf("processA") // 只在 processA 进程初始化
100 | )
101 | allInitializer = mapOf(
102 | InitializerC::class.java.name to c,
103 | InitializerD::class.java.name to d
104 | )
105 | mainThreadComponentChainList =
106 | listOf(DependencyChain(linkedSetOf(c)), DependencyChain(linkedSetOf(d)))
107 | // when
108 | Startup.build(context, registry, logger).initInMainThread("MAIN_PROCESS")
109 | // then
110 | assertThat(cInitialized, IsEqual(true))
111 | assertThat(dInitialized, IsEqual(false))
112 | }
113 |
114 |
115 | @Test(expected = MockException::class)
116 | fun `测试 debug 版初始化异常`() {
117 | // given
118 | isDebugVersion = true
119 | val a = ComponentInfo(
120 | name = InitializerWithException::class.java.name,
121 | instanceProvider = { InitializerWithException() },
122 | supportProcess = listOf(ALL_PROCESS) // 所有进程
123 | )
124 | allInitializer = mapOf(
125 | InitializerWithException::class.java.name to a
126 | )
127 | mainThreadComponentChainList = listOf(DependencyChain(linkedSetOf(a)))
128 | // when
129 | Startup.build(context, registry, logger).initInMainThread("MAIN_PROCESS")
130 | }
131 |
132 | @Test
133 | fun `测试非 debug 版初始化异常`() {
134 | // given
135 | isDebugVersion = false
136 | val a = ComponentInfo(
137 | name = InitializerWithException::class.java.name,
138 | instanceProvider = { InitializerWithException() },
139 | supportProcess = listOf(ALL_PROCESS) // 所有进程
140 | )
141 | allInitializer = mapOf(
142 | InitializerWithException::class.java.name to a
143 | )
144 | mainThreadComponentChainList = listOf(DependencyChain(linkedSetOf(a)))
145 | // when
146 | Startup.build(context, registry, logger).initInMainThread("MAIN_PROCESS")
147 | // then
148 | assertThat(errorExceptionInLog, IsInstanceOf(MockException::class.java))
149 | }
150 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/DependencyChainBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor
2 |
3 | import com.geelee.startup.annotation.IInitializerRegistry.Companion.ALL_PROCESS
4 | import com.geelee.startup.annotation.IInitializerRegistry.Companion.MAIN_PROCESS
5 | import com.geelee.startup.annotation.model.ComponentInfo
6 | import com.geelee.startup.annotation.model.DependencyChain
7 | import com.geelee.startup.processor.exception.CycleDependencyException
8 | import com.geelee.startup.processor.exception.IllegalProcessException
9 | import com.geelee.startup.processor.exception.IllegalThreadException
10 | import com.geelee.startup.processor.ktx.contains
11 | import com.geelee.startup.processor.ktx.toSimpleString
12 | import java.util.concurrent.CopyOnWriteArraySet
13 |
14 |
15 | /**
16 | * Created by zhiyueli on 2021/8/12.
17 | */
18 | class DependencyChainBuilder(
19 | private val logger: ILogger,
20 | private val allComponentList: Map
21 | ) {
22 |
23 | private val building = CopyOnWriteArraySet()
24 | private val built = CopyOnWriteArraySet()
25 |
26 | /**
27 | * 构建初始化器依赖链列表
28 | *
29 | * 比如 A、B、C、D、E、F、G 可能构建出以下依赖链列表
30 | *
31 | * A -> B -> C
32 | * D -> E
33 | * F
34 | * G
35 | *
36 | * @param components 待构造依赖链列表的 Component 列表
37 | */
38 | fun buildComponentChainList(components: List): List {
39 | val result = mutableListOf()
40 | components.forEach {
41 | val dependencyChain = DependencyChain(linkedSetOf())
42 | dfsBuildDirectedAcyclicGraph(
43 | it,
44 | null,
45 | building = building,
46 | built = built,
47 | dependencyChain = dependencyChain
48 | )
49 | if (dependencyChain.chain.isNotEmpty()) {
50 | result.add(dependencyChain)
51 | }
52 | }
53 | logger.i(
54 | String.format(
55 | "buildComponentChainList(${Thread.currentThread()}) ->\n- input=%s\n- output=%s",
56 | components.toSimpleString(),
57 | result.toTypedArray().contentToString()
58 | )
59 | )
60 | return result
61 | }
62 |
63 | /**
64 | * dfs 构建依赖链(有向无环图)
65 | * @param component 当前 Component 节点
66 | * @param subComponent 当前 Component 节点的依赖节点
67 | * @param building 全局记录正在构建依赖链的 Component 列表
68 | * @param built 全局记录已经确定链位置的 Component 列表
69 | * @param dependencyChain 记录本次确定链位置的 Component 列表, 是本次依赖链产物
70 | */
71 | private fun dfsBuildDirectedAcyclicGraph(
72 | component: ComponentInfo,
73 | subComponent: ComponentInfo?,
74 | building: MutableSet,
75 | built: MutableSet,
76 | dependencyChain: DependencyChain
77 | ) {
78 | // 断言非循环依赖
79 | assertNotCycleDependency(component, building)
80 | // 已经构建好了就跳过
81 | if (built.contains { it.name == component.name }) {
82 | return
83 | }
84 | building.add(component)
85 | // 先初始化依赖的 Component
86 | chainDependencyComponent(component, building, built, dependencyChain)
87 | // 断言非异常进程定义
88 | assertLegalProcess(component.name, subComponent?.name)
89 | // 断言非异常线程定义
90 | assertLegalThread(component.name, subComponent?.name)
91 | // 构建
92 | building.remove(component)
93 | built.add(component)
94 | dependencyChain.chain.add(component)
95 | }
96 |
97 | /**
98 | * 断言非循环依赖
99 | *
100 | * @param componentInfo 当前 Component 节点
101 | * @param building 全局记录正在构建依赖链的 Component 列表
102 | */
103 | private fun assertNotCycleDependency(
104 | componentInfo: ComponentInfo,
105 | building: MutableSet
106 | ) {
107 | if (building.contains { it.name == componentInfo.name }) {
108 | val message = String.format(
109 | "Cannot initialize %s. Cycle detected.", componentInfo.name
110 | )
111 | throw CycleDependencyException(message)
112 | }
113 | }
114 |
115 | /**
116 | * 断言合法定义的进程信息:
117 | * A 初始化器依赖于 B 初始化器
118 | * A: 子初始化器
119 | * B: 父初始化器
120 | * 父初始化器的 supportProcess 必须大于等于子初始化器.
121 | *
122 | * @param componentName 父初始化器
123 | * @param subComponentName 子初始化器
124 | */
125 | private fun assertLegalProcess(
126 | componentName: String,
127 | subComponentName: String?
128 | ) {
129 | if ((subComponentName != null) &&
130 | !getSupportProcessList(componentName)
131 | .contains(getSupportProcessList(subComponentName))
132 | ) {
133 | val message = String.format(
134 | "父 Initializer(%s) 的 supportProcess 必须大于等于子 Initializer(%s).",
135 | componentName,
136 | subComponentName
137 | )
138 | throw IllegalProcessException(message)
139 | }
140 | }
141 |
142 | /**
143 | * 断言合法定义的线程信息:
144 | * A: 在主线程初始化
145 | * B: 在子线程初始化
146 | * A 初始化器不能依赖于 B 初始化器
147 | *
148 | * @param componentName 父初始化器
149 | * @param subComponentName 子初始化器
150 | */
151 | private fun assertLegalThread(
152 | componentName: String,
153 | subComponentName: String?
154 | ) {
155 | if (subComponentName.isNullOrEmpty()) {
156 | return
157 | }
158 | val component = allComponentList[componentName]
159 | val subComponent = allComponentList[subComponentName]
160 | if (component?.threadMode == ComponentInfo.ThreadMode.WorkThread &&
161 | subComponent?.threadMode == ComponentInfo.ThreadMode.MainThread
162 | ) {
163 | val message = String.format(
164 | "运行在主线程的 Initializer(%s) 不能依赖于运行在子线程的 Initializer(%s).",
165 | subComponent,
166 | component
167 | )
168 | throw IllegalThreadException(message)
169 | }
170 | }
171 |
172 | /**
173 | * 链接当前结点的所有依赖节点
174 | *
175 | * @param componentInfo 当前 Component 节点
176 | * @param building 全局记录正在构建依赖链的 Component 列表
177 | * @param built 全局记录已经确定链位置的 Component 列表
178 | * @param dependencyChain 记录本次确定链位置的 Component 列表, 是本次依赖链产物
179 | */
180 | private fun chainDependencyComponent(
181 | componentInfo: ComponentInfo,
182 | building: MutableSet,
183 | built: MutableSet,
184 | dependencyChain: DependencyChain
185 | ) {
186 | val dependencies = componentInfo.dependencies
187 | if (dependencies.isEmpty()) {
188 | return
189 | }
190 | for (clazzName in dependencies) {
191 | val dependencyComponentInfo = allComponentList[clazzName]
192 | if (dependencyComponentInfo == null || built.contains { it.name == clazzName }) {
193 | continue
194 | }
195 | dfsBuildDirectedAcyclicGraph(
196 | dependencyComponentInfo,
197 | componentInfo,
198 | building,
199 | built,
200 | dependencyChain
201 | )
202 | }
203 | }
204 |
205 | /**
206 | * 判断当前列表包含的内容是否大于等于 targetList
207 | * 如果当前列表包含 [ALL_PROCESS] 代表肯定包含 targetList
208 | */
209 | private fun List.contains(targetList: List): Boolean {
210 | if (this.contains(ALL_PROCESS)) return true
211 | targetList.forEach {
212 | if (!this.contains(it)) {
213 | return false
214 | }
215 | }
216 | return true
217 | }
218 |
219 | /**
220 | * 正常情况下这里不会为空,除非注解中真的没有定义 supportProcess, 那默认就在主进程初始化
221 | *
222 | * @param componentName 当前初始化器名字
223 | */
224 | private fun getSupportProcessList(componentName: String): List {
225 | val processList =
226 | allComponentList[componentName]?.supportProcess ?: emptyList()
227 | if (processList.isEmpty()) {
228 | return arrayListOf(MAIN_PROCESS)
229 | }
230 | return processList
231 | }
232 | }
--------------------------------------------------------------------------------
/startup-processor/src/main/java/com/geelee/startup/processor/InitializerProcessor.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup.processor
2 |
3 | import com.geelee.startup.annotation.Config
4 | import com.geelee.startup.annotation.IInitializerRegistry
5 | import com.geelee.startup.annotation.model.ComponentInfo
6 | import com.geelee.startup.annotation.model.DependencyChain
7 | import com.google.auto.service.AutoService
8 | import com.squareup.kotlinpoet.*
9 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
10 | import com.geelee.startup.processor.exception.IllegalAnnotationException
11 | import com.geelee.startup.processor.ktx.toListLiteral
12 | import com.geelee.startup.processor.ktx.toLiteral
13 | import com.geelee.startup.processor.ktx.toMapLiteral
14 | import java.io.File
15 | import javax.annotation.processing.*
16 | import javax.lang.model.SourceVersion
17 | import javax.lang.model.element.TypeElement
18 | import javax.lang.model.type.DeclaredType
19 | import javax.lang.model.type.MirroredTypesException
20 | import javax.lang.model.util.Elements
21 | import javax.tools.Diagnostic
22 |
23 | @AutoService(Processor::class)
24 | @SupportedSourceVersion(SourceVersion.RELEASE_8)
25 | class InitializerProcessor : AbstractProcessor() {
26 |
27 | companion object {
28 | private const val TAG = "InitializerProcessor"
29 | private const val GENERATE_CLASS_NAME = IInitializerRegistry.GENERATED_CLASS_NAME
30 | private const val GENERATE_CLASS_PACKAGE_NAME =
31 | IInitializerRegistry.GENERATED_CLASS_PACKAGE_NAME
32 |
33 | private const val ALL_LIST = "allList"
34 | private const val MAIN_THREAD_LIST = "mainThreadList"
35 | private const val WORK_THREAD_LIST = "workThreadList"
36 | }
37 |
38 | private lateinit var messager: Messager
39 | private lateinit var filer: Filer
40 | private lateinit var elementUtils: Elements
41 |
42 | private var writeRoundDone: Boolean = false
43 | private var round: Int = 0
44 |
45 | override fun init(processingEnvironment: ProcessingEnvironment) {
46 | super.init(processingEnvironment)
47 | messager = processingEnvironment.messager
48 | filer = processingEnvironment.filer
49 | elementUtils = processingEnvironment.elementUtils
50 | }
51 |
52 | override fun getSupportedAnnotationTypes(): MutableSet {
53 | return mutableSetOf(
54 | Config::class.java.canonicalName
55 | )
56 | }
57 |
58 | override fun process(
59 | set: MutableSet,
60 | roundEnvironment: RoundEnvironment
61 | ): Boolean {
62 | round++
63 | val processingOver = roundEnvironment.processingOver()
64 | log(
65 | Diagnostic.Kind.NOTE,
66 | "processing() round:$round processingOver:$processingOver annotations:$set"
67 | )
68 | if (processingOver) {
69 | if (set.isNotEmpty()) {
70 | log(
71 | Diagnostic.Kind.ERROR,
72 | "Unexpected processing state: annotations still available after processing over"
73 | )
74 | return false
75 | }
76 | }
77 | if (set.isEmpty()) {
78 | return false
79 | }
80 | if (writeRoundDone) {
81 | log(
82 | Diagnostic.Kind.ERROR,
83 | "Unexpected processing state: annotations still available after processing over"
84 | )
85 | }
86 | val result = writeToFile(roundEnvironment)
87 | writeRoundDone = true
88 | return result
89 | }
90 |
91 | private fun buildComponentInfoList(roundEnvironment: RoundEnvironment): List {
92 | return roundEnvironment.getElementsAnnotatedWith(Config::class.java)
93 | .filter {
94 | if (it !is TypeElement) {
95 | throw IllegalAnnotationException("${Config::class.java.simpleName} 只能注解到实现了 Initializer 接口的类上 --> ${it.simpleName}")
96 | }
97 | true
98 | }
99 | .map {
100 | val annotation = it.getAnnotation(Config::class.java)
101 | val componentName = (it as TypeElement).qualifiedName.toString()
102 | val supportProcess = annotation.supportProcess
103 | val threadMode = annotation.threadMode
104 | val dependencies = annotation.getClazzArrayNameList()
105 | ComponentInfo(
106 | name = componentName,
107 | supportProcess = supportProcess.toList(),
108 | threadMode = threadMode,
109 | dependencies = dependencies.toList()
110 | ) { }
111 | }
112 | }
113 |
114 | private fun Config.getClazzArrayNameList(): List {
115 | try {
116 | return this.dependencies.map { it.java.canonicalName }
117 | } catch (mte: MirroredTypesException) {
118 | if (mte.typeMirrors.isEmpty()) return emptyList()
119 | return mte.typeMirrors.map {
120 | ((it as DeclaredType).asElement() as TypeElement).qualifiedName.toString()
121 | }
122 | }
123 | }
124 |
125 | private fun writeToFile(roundEnvironment: RoundEnvironment): Boolean {
126 | val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"] ?: return false
127 |
128 | val componentList = buildComponentInfoList(roundEnvironment).sorted()
129 | val dependencyChainBuilder = DependencyChainBuilder(logger, componentList.toMap())
130 | val mainThreadComponentChainList =
131 | dependencyChainBuilder.buildComponentChainList(componentList.getMainThreadComponentList())
132 | val workThreadComponentChainList =
133 | dependencyChainBuilder.buildComponentChainList(componentList.getWorkThreadComponentList())
134 |
135 | FileSpec.builder(
136 | GENERATE_CLASS_PACKAGE_NAME,
137 | GENERATE_CLASS_NAME
138 | )
139 | .addComment("Auto Generated by the KAPT. DO NOT EDIT!")
140 | .addType(
141 | TypeSpec.classBuilder(GENERATE_CLASS_NAME)
142 | .addSuperinterface(IInitializerRegistry::class.asTypeName())
143 | .addProperties(componentList.generateComponentInfoProperties())
144 | .addProperty(
145 | PropertySpec.builder(
146 | ALL_LIST,
147 | Map::class.asClassName().parameterizedBy(
148 | String::class.asTypeName(),
149 | ComponentInfo::class.asTypeName()
150 | )
151 | )
152 | .addModifiers(KModifier.PRIVATE)
153 | .initializer(CodeBlock.of(componentList.toMapLiteral()))
154 | .build()
155 | )
156 | .addProperty(
157 | PropertySpec.builder(
158 | MAIN_THREAD_LIST,
159 | List::class.asClassName().parameterizedBy(
160 | DependencyChain::class.asTypeName()
161 | )
162 | )
163 | .addModifiers(KModifier.PRIVATE)
164 | .initializer(CodeBlock.of(mainThreadComponentChainList.toListLiteral()))
165 | .build()
166 | )
167 | .addProperty(
168 | PropertySpec.builder(
169 | WORK_THREAD_LIST,
170 | List::class.asClassName().parameterizedBy(
171 | DependencyChain::class.asTypeName()
172 | )
173 | )
174 | .addModifiers(KModifier.PRIVATE)
175 | .initializer(CodeBlock.of(workThreadComponentChainList.toListLiteral()))
176 | .build()
177 | )
178 | .addFunction(
179 | FunSpec.builder("getAllInitializer")
180 | .returns(
181 | Map::class.asClassName().parameterizedBy(
182 | String::class.asTypeName(),
183 | ComponentInfo::class.asTypeName()
184 | )
185 | )
186 | .addModifiers(KModifier.OVERRIDE)
187 | .addStatement("return $ALL_LIST")
188 | .build()
189 | )
190 | .addFunction(
191 | FunSpec.builder("getMainThreadComponentChainList")
192 | .returns(
193 | List::class.asClassName().parameterizedBy(
194 | DependencyChain::class.asTypeName()
195 | )
196 | )
197 | .addModifiers(KModifier.OVERRIDE)
198 | .addStatement("return $MAIN_THREAD_LIST")
199 | .build()
200 | )
201 | .addFunction(
202 | FunSpec.builder("getWorkThreadComponentChainList")
203 | .returns(
204 | List::class.asClassName().parameterizedBy(
205 | DependencyChain::class.asTypeName()
206 | )
207 | )
208 | .addModifiers(KModifier.OVERRIDE)
209 | .addStatement("return $WORK_THREAD_LIST")
210 | .build()
211 | )
212 | .build()
213 | )
214 | .build()
215 | .writeTo(File(kaptKotlinGeneratedDir))
216 | // 输出报告
217 | InitializerReporter(
218 | mainThreadComponentChainList,
219 | workThreadComponentChainList
220 | ).outputReport(processingEnv)
221 | return true
222 | }
223 |
224 | /**
225 | * 获取所有在主线程执行的初始化器
226 | */
227 | private fun List.getMainThreadComponentList(): List {
228 | return this.filter { it.threadMode == ComponentInfo.ThreadMode.MainThread }
229 | }
230 |
231 | /**
232 | * 获取所有在子线程执行的初始化器
233 | */
234 | private fun List.getWorkThreadComponentList(): List {
235 | return this.filter { it.threadMode == ComponentInfo.ThreadMode.WorkThread }
236 | }
237 |
238 | /**
239 | * 根据列表构造一个 初始化器类名 to ComponentInfo 的 Map
240 | */
241 | private fun List.toMap(): Map {
242 | val map = mutableMapOf()
243 | this.forEach {
244 | map[it.name] = it
245 | }
246 | return map
247 | }
248 |
249 | /**
250 | * 根据列表一个个构造 componentInfo 成员变量
251 | */
252 | private fun List.generateComponentInfoProperties(): List {
253 | return this.map {
254 | PropertySpec.builder(
255 | name = it.simpleName,
256 | type = ComponentInfo::class.asTypeName()
257 | )
258 | .addModifiers(KModifier.PRIVATE)
259 | .initializer(CodeBlock.of(it.toLiteral()))
260 | .build()
261 | }
262 | }
263 |
264 | private val logger = object : ILogger {
265 | override fun i(msg: String) {
266 | log(Diagnostic.Kind.NOTE, msg)
267 | }
268 |
269 | override fun e(msg: String, throwable: Throwable) {
270 | log(Diagnostic.Kind.ERROR, msg)
271 | }
272 | }
273 |
274 | private fun log(level: Diagnostic.Kind, msg: String) {
275 | val message = "$TAG --> $msg"
276 | println(message)
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/startup/src/test/java/com/geelee/startup/DependencyChainBuilderTest.kt:
--------------------------------------------------------------------------------
1 | package com.geelee.startup
2 |
3 | import com.geelee.startup.annotation.IInitializerRegistry.Companion.ALL_PROCESS
4 | import com.geelee.startup.annotation.model.ComponentInfo
5 | import com.geelee.startup.annotation.model.DependencyChain
6 | import com.geelee.startup.processor.DependencyChainBuilder
7 | import com.geelee.startup.processor.ILogger
8 | import com.geelee.startup.processor.exception.CycleDependencyException
9 | import com.geelee.startup.processor.exception.IllegalProcessException
10 | import org.hamcrest.MatcherAssert.assertThat
11 | import org.hamcrest.core.IsEqual
12 | import org.junit.Test
13 |
14 |
15 | /**
16 | * Created by zhiyueli on 2021/8/13.
17 | */
18 | class DependencyChainBuilderTest {
19 |
20 | private val logger = object : ILogger {
21 | override fun i(msg: String) {
22 | println(msg)
23 | }
24 |
25 | override fun e(msg: String, throwable: Throwable) {
26 | println(msg)
27 | }
28 | }
29 |
30 | @Test(expected = CycleDependencyException::class)
31 | fun `测试自己依赖自己`() {
32 | // given
33 | val allComponentMap = mapOf(
34 | "a" to ComponentInfo(
35 | name = "a",
36 | dependencies = listOf("a"),
37 | instanceProvider = {})
38 | )
39 | val builder = DependencyChainBuilder(logger, allComponentMap)
40 | // when
41 | builder.buildComponentChainList(allComponentMap.map { it.value })
42 | }
43 |
44 | /**
45 | * A -> B
46 | * B -> C
47 | * C -> D
48 | * D -> A (有一个循环依赖)
49 | */
50 | @Test(expected = CycleDependencyException::class)
51 | fun `测试不合法的链式循环依赖1`() {
52 | // given
53 | val allComponentMap = mapOf(
54 | "a" to ComponentInfo(
55 | name = "a",
56 | dependencies = listOf("b"),
57 | instanceProvider = {}),
58 | "b" to ComponentInfo(
59 | name = "b",
60 | dependencies = listOf("c"),
61 | instanceProvider = {}),
62 | "c" to ComponentInfo(
63 | name = "c",
64 | dependencies = listOf("d"),
65 | instanceProvider = {}),
66 | "d" to ComponentInfo(
67 | name = "d",
68 | dependencies = listOf("a"),
69 | instanceProvider = {}),
70 | )
71 | val builder = DependencyChainBuilder(logger, allComponentMap)
72 | // when
73 | builder.buildComponentChainList(allComponentMap.map { it.value })
74 | }
75 |
76 | /**
77 | * F -> G
78 | * G -> H, I
79 | * I -> F (循环依赖)
80 | */
81 | @Test(expected = CycleDependencyException::class)
82 | fun `测试不合法的链式循环依赖2`() {
83 | // given
84 | val allComponentMap = mapOf(
85 | "f" to ComponentInfo(
86 | name = "f",
87 | dependencies = listOf("g"),
88 | instanceProvider = {}),
89 | "g" to ComponentInfo(
90 | name = "g",
91 | dependencies = listOf("h", "i"),
92 | instanceProvider = {}),
93 | "i" to ComponentInfo(
94 | name = "i",
95 | dependencies = listOf("f"),
96 | instanceProvider = {}),
97 | "h" to ComponentInfo(
98 | name = "h",
99 | instanceProvider = {}),
100 | )
101 | val builder = DependencyChainBuilder(logger, allComponentMap)
102 | // when
103 | builder.buildComponentChainList(allComponentMap.map { it.value })
104 | }
105 |
106 | /**
107 | * F -> G
108 | * G -> H, I
109 | * J -> H
110 | */
111 | @Test
112 | fun `测试合法的链式依赖`() {
113 | // given
114 | val allComponentMap = mapOf(
115 | "f" to ComponentInfo(
116 | name = "f",
117 | dependencies = listOf("g"),
118 | instanceProvider = {}),
119 | "g" to ComponentInfo(
120 | name = "g",
121 | dependencies = listOf("h", "i"),
122 | instanceProvider = {}),
123 | "j" to ComponentInfo(
124 | name = "j",
125 | dependencies = listOf("h"),
126 | instanceProvider = {}),
127 | "h" to ComponentInfo(
128 | name = "h",
129 | instanceProvider = {}),
130 | )
131 | val builder = DependencyChainBuilder(logger, allComponentMap)
132 | // when
133 | builder.buildComponentChainList(allComponentMap.map { it.value })
134 | }
135 |
136 | /**
137 | * A 初始化器依赖于 B 初始化器
138 | * A: 子初始化器 processB
139 | * B: 父初始化器 processA
140 | * 父初始化器的 supportProcess 不包含子初始化器 -> 不合法
141 | */
142 | @Test(expected = IllegalProcessException::class)
143 | fun `测试被依赖的进程列表不包含依赖的进程列表1`() {
144 | // given
145 | val allComponentMap = mapOf(
146 | "a" to ComponentInfo(
147 | name = "a",
148 | supportProcess = listOf("processB"),
149 | dependencies = listOf("b"),
150 | instanceProvider = {}),
151 | "b" to ComponentInfo(
152 | name = "b",
153 | supportProcess = listOf("processA"),
154 | instanceProvider = {})
155 | )
156 | val builder = DependencyChainBuilder(logger, allComponentMap)
157 | // when
158 | builder.buildComponentChainList(allComponentMap.map { it.value })
159 | }
160 |
161 | /**
162 | * A 初始化器依赖于 B 初始化器
163 | * A: 子初始化器 allProcess
164 | * B: 父初始化器 processA
165 | * 父初始化器的 supportProcess 不包含子初始化器 -> 不合法
166 | */
167 | @Test(expected = IllegalProcessException::class)
168 | fun `测试被依赖的进程列表不包含依赖的进程列表2`() {
169 | // given
170 | val allComponentMap = mapOf(
171 | "a" to ComponentInfo(
172 | name = "a",
173 | supportProcess = listOf(ALL_PROCESS),
174 | dependencies = listOf("b"),
175 | instanceProvider = {}),
176 | "b" to ComponentInfo(
177 | name = "b",
178 | supportProcess = listOf("processA"),
179 | instanceProvider = {})
180 | )
181 | val builder = DependencyChainBuilder(logger, allComponentMap)
182 | // when
183 | builder.buildComponentChainList(allComponentMap.map { it.value })
184 | }
185 |
186 | /**
187 | * 输入:
188 | * F -> G
189 | * L
190 | * M
191 | * G -> H, K
192 | * J -> H
193 | * N
194 | * O
195 | *
196 | * 输出:
197 | * F -> G -> K -> H
198 | * L
199 | * M
200 | * J
201 | * N
202 | * O
203 | */
204 | @Test
205 | fun `测试构建依赖链是否正常`() {
206 | // given
207 | val f = ComponentInfo(
208 | name = "f",
209 | dependencies = listOf("g"),
210 | instanceProvider = {})
211 | val k = ComponentInfo(
212 | name = "k",
213 | instanceProvider = {})
214 | val h = ComponentInfo(
215 | name = "h",
216 | instanceProvider = {})
217 | val l = ComponentInfo(
218 | name = "l",
219 | instanceProvider = {})
220 | val m = ComponentInfo(
221 | name = "m",
222 | instanceProvider = {})
223 | val g = ComponentInfo(
224 | name = "g",
225 | dependencies = listOf("h", "k"),
226 | instanceProvider = {})
227 | val j = ComponentInfo(
228 | name = "j",
229 | dependencies = listOf("h"),
230 | instanceProvider = {})
231 | val n = ComponentInfo(
232 | name = "n",
233 | instanceProvider = {})
234 | val o = ComponentInfo(
235 | name = "o",
236 | instanceProvider = {})
237 | val allComponentMap = mapOf(
238 | "f" to f,
239 | "k" to k,
240 | "h" to h,
241 | "l" to l,
242 | "m" to m,
243 | "g" to g,
244 | "j" to j,
245 | "n" to n,
246 | "o" to o,
247 | )
248 | val builder = DependencyChainBuilder(logger, allComponentMap)
249 | // when
250 | val chainList = builder.buildComponentChainList(allComponentMap.map { it.value })
251 | chainList.forEachIndexed { index, dependencyChain ->
252 | when (index) {
253 | 0 -> {
254 | assertThat(
255 | dependencyChain.toString(),
256 | IsEqual(DependencyChain(linkedSetOf(h, k, g, f)).toString())
257 | )
258 | }
259 |
260 | 1 -> {
261 | assertThat(
262 | dependencyChain.toString(),
263 | IsEqual(DependencyChain(linkedSetOf(l)).toString())
264 | )
265 | }
266 |
267 | 2 -> {
268 | assertThat(
269 | dependencyChain.toString(),
270 | IsEqual(DependencyChain(linkedSetOf(m)).toString())
271 | )
272 | }
273 |
274 | 3 -> {
275 | assertThat(
276 | dependencyChain.toString(),
277 | IsEqual(DependencyChain(linkedSetOf(j)).toString())
278 | )
279 | }
280 |
281 | 4 -> {
282 | assertThat(
283 | dependencyChain.toString(),
284 | IsEqual(DependencyChain(linkedSetOf(n)).toString())
285 | )
286 | }
287 |
288 | 5 -> {
289 | assertThat(
290 | dependencyChain.toString(),
291 | IsEqual(DependencyChain(linkedSetOf(o)).toString())
292 | )
293 | }
294 |
295 | else -> {
296 | throw AssertionError("expect size is 6 but now is ${chainList.size}")
297 | }
298 | }
299 | }
300 | }
301 |
302 | /**
303 | * A 初始化器依赖于 B 初始化器
304 | * A: 子初始化器 mainThread
305 | * B: 父初始化器 workThread
306 | * A 初始化器不能依赖于 B 初始化器
307 | */
308 | @Test(expected = IllegalThreadStateException::class)
309 | fun `测试线程定义异常`() {
310 | // given
311 | val allComponentMap = mapOf(
312 | "a" to ComponentInfo(
313 | name = "a",
314 | threadMode = ComponentInfo.ThreadMode.MainThread,
315 | dependencies = listOf("b"),
316 | instanceProvider = {}),
317 | "b" to ComponentInfo(
318 | name = "b",
319 | threadMode = ComponentInfo.ThreadMode.WorkThread,
320 | instanceProvider = {})
321 | )
322 | val builder = DependencyChainBuilder(logger, allComponentMap)
323 | // when
324 | builder.buildComponentChainList(allComponentMap.map { it.value })
325 | }
326 |
327 | /**
328 | * 输入:
329 | * A -> B
330 | * B -> C
331 | * C -> D
332 | * D -> E
333 | * E -> F
334 | * F -> G
335 | *
336 | * 输出:
337 | * A -> B -> C -> D -> E -> F -> G
338 | */
339 | @Test
340 | fun `测试构建依赖链顺序是否正常`() {
341 | // given
342 | val a = ComponentInfo(
343 | name = "a",
344 | dependencies = listOf("b"),
345 | instanceProvider = {})
346 | val b = ComponentInfo(
347 | name = "b",
348 | dependencies = listOf("c"),
349 | instanceProvider = {})
350 | val c = ComponentInfo(
351 | name = "c",
352 | dependencies = listOf("d"),
353 | instanceProvider = {})
354 | val d = ComponentInfo(
355 | name = "d",
356 | dependencies = listOf("e"),
357 | instanceProvider = {})
358 | val e = ComponentInfo(
359 | name = "e",
360 | dependencies = listOf("f"),
361 | instanceProvider = {})
362 | val f = ComponentInfo(
363 | name = "f",
364 | dependencies = listOf("g"),
365 | instanceProvider = {})
366 | val g = ComponentInfo(
367 | name = "g",
368 | instanceProvider = {})
369 | val allComponentMap = mapOf(
370 | "a" to a,
371 | "b" to b,
372 | "c" to c,
373 | "d" to d,
374 | "e" to e,
375 | "f" to f,
376 | "g" to g
377 | )
378 | val builder = DependencyChainBuilder(logger, allComponentMap)
379 | // when
380 | val chainList = builder.buildComponentChainList(allComponentMap.map { it.value })
381 | val actual =
382 | chainList.first().chain.toList().map { it.name }.toTypedArray().contentToString()
383 | val expect =
384 | listOf("a", "b", "c", "d", "e", "f", "g").reversed().toTypedArray().contentToString()
385 | assertThat(actual, IsEqual(expect))
386 | }
387 | }
--------------------------------------------------------------------------------