Happy New Year!
At the beginning of the New Year, everything is new.
First of all, thank you for your support. I finally like to mention the outstanding author of nuggets this year.
Kneel to everyone, I wish everyone android students happy New Year ah.
The opening
First introduce the article of Xu Gong big man, if there is a need for the front, it is suggested to see this series.
Start optimization of this series can have a good look, thanks to Xu Gong big man.
This article will not publish anything related to directed acyclic graphs (DAGs), but will go into more detail about some of my own strange ideas and how to make some interesting adjustments from a few points of view.
The current warehouse is still in an iterative state, not a particularly stable state, so this article is more to open some small ideas for everyone.
If you have any ideas, please leave a comment. I personally feel that an iterative library can be continuously evolving.
The demo address AndroidStartup
A lot of code in the demo refers to Android-startup, thanks to the big guy, u1S1 is very strong.
Task granularity
This point is actually quite important, I believe that many people after access to the startup framework, more things are to use the original code, directly with several tasks to wrap the previous code, and then it is equivalent to complete the simple startup framework access.
In fact, this basically violates the original intention of the startup framework design. I’ll start with the point that the startup framework doesn’t really help you start up much faster, it just solves the scenario of making your SDK initialization more orderly, allowing you to safely add new SDKS during long iterations.
For example, when your buried framework relies on the network library, the ABTest configuration center relies on the network library, the network library relies on DNS, etc., and then all the business depends on the buried configuration center image library, etc., after the SDK is initialized.
Of course, there are still limited cases of dependency loop problem, at this time, students may need to develop manual to solve the dependency problem. For example, in special cases, the network library needs a unique ID, and the reporting library relies on the network library, which in turn depends on the unique ID, and the unique ID needs to be reported
So my personal view is that the granularity of the startup framework should be down to the initialization of each SDK, and the more granular the better. In fact, the general startup framework will count the time of each task, which makes it easier for us to follow up the corresponding problems, such as whether the time of certain tasks has increased, etc.
In order to solve this dependency loop problem, it is possible to split the initialization of an SDK into three parts.
Wait between child threads
It was discovered earlier that the startup framework within the project only ensures that threads are placed in the order of daG execution. This is ok if only the main thread and pool size is 1 thread pool. But if you have multiple threads running concurrently, this becomes a dangerous operation.
Therefore, we need to add a wait in the concurrent scenario to ensure that the dependent task is completed before continuing to execute the initialization code.
With CountDownLatch, await will be released and await will continue after the dependent task is completed. As for the design, I still use decorator, which can continue to be used without the user changing the original logic.
The code is as follows, which is mainly a task completion distribution, and latch-1 is found when the current dependency has the task. When the latch reaches 0, the current thread is released.
class StartupAwaitTask(val task: StartupTask) : StartupTask {
private var dependencies = task.dependencies()
private lateinit var countDownLatch: CountDownLatch
private lateinit var rightDependencies: List<String>
var awaitDuration: Long = 0
override fun run(context: Context) {
val timeUsage = SystemClock.elapsedRealtime()
countDownLatch.await()
awaitDuration = (SystemClock.elapsedRealtime() - timeUsage) / 1000
KLogger.i(
TAG, "taskName:${task.tag()} await costa:${awaitDuration} "
)
task.run(context)
}
override fun dependencies(a): MutableList<String> {
return dependencies
}
fun allTaskTag(tags: HashSet<String>) {
rightDependencies = dependencies.filter { tags.contains(it) }
countDownLatch = CountDownLatch(rightDependencies.size)
}
fun dispatcher(taskName: String) {
if (rightDependencies.contains(taskName)) {
countDownLatch.countDown()
}
}
override fun mainThread(a): Boolean {
return task.mainThread()
}
override fun await(a): Boolean {
return task.await()
}
override fun tag(a): String {
return task.tag()
}
override fun onTaskStart(a) {
task.onTaskStart()
}
override fun onTaskCompleted(a) {
task.onTaskCompleted()
}
override fun toString(a): String {
return task.toString()
}
companion object {
const val TAG = "StartupAwaitTask"}}Copy the code
This is a complement to the ability to complete, but also part of the multithreaded dependency must be completed.
The dependency mode has also been changed from class to tag, but the final design of this area has not been completed and it is still a bit undecided. Mainly to solve the componentized case, you can be a little more arbitrary.
Thread pool shutdown
It is my personal concern to consider whether the thread pool should be turned off by default when the startup process is complete. I found that a lot of them were not written, causing some thread use leakage problems.
fun dispatcherEnd(a) {
if(executor ! = mExecutor) { KLogger.i(TAG,"auto shutdown default executor")
mExecutor.shutdown()
}
}
Copy the code
Consider closing the thread pool after execution if the current thread pool is not an incoming thread pool.
DSL + anchor point
Because I’m both a developer and a user of the framework. Therefore, I found many problems in the original design in the process of using, and IT was very inconvenient for me to insert a task after all the SDK was completed.
Then I considered this part to write the dynamically added task in the way of DSL. Kotlin really smells good. I’ll be a loser if there’s no sugar in the future.
This is where I jumped. Oh, my God, grammar candy smells good.
fun Application.createStartup(a): Startup.Builder = run {
startUp(this) {
addTask {
simpleTask("taskA") {
info("taskA")
}
}
addTask {
simpleTask("taskB") {
info("taskB")
}
}
addTask {
simpleTask("taskC") {
info("taskC")
}
}
addTask {
simpleTaskBuilder("taskD") {
info("taskD")
}.apply {
dependOn("taskC")
}.build()
}
addTask("taskC") {
info("taskC")
}
setAnchorTask {
MyAnchorTask()
}
addTask {
asyncTask("asyncTaskA", {
info("asyncTaskA")
}, {
dependOn("asyncTaskD")
})
}
addAnchorTask {
asyncTask("asyncTaskB", {
info("asyncTaskB")
}, {
dependOn("asyncTaskA")
await = true
})
}
addAnchorTask {
asyncTaskBuilder("asyncTaskC") {
info("asyncTaskC")
sleep(1000)
}.apply {
await = true
dependOn("asyncTaskE")
}.build()
}
addTaskGroup { taskGroup() }
addTaskGroup { StartupTaskGroupApplicationKspMain() }
addMainProcTaskGroup { StartupTaskGroupApplicationKspAll() }
addProcTaskGroup { StartupProcTaskGroupApplicationKsp() }
}
}
Copy the code
This DSL is good for inserting simple tasks, either without dependencies or if you’re just lazy and want to do it. The advantage is that you can avoid writing too much redundant code in the form of inheritance and so on, and then you can see what you’ve done in the startup process.
Typically, several anchor tasks are set up after the project stabilizes. Their role is to perform the follow-up tasks as long as they are mounted to the anchor point, and set some standards so that subsequent students can access more quickly.
We’ll set this up as a baseline for some task group, such as network library, image library, buried frame,abtest, etc., and when these tasks are done, other business code can be initialized here. This eliminates the need for everyone to write some basic dependencies and makes the developer a little more comfortable.
Why is it looped again
In the previous sorting phase, there was a very nasty problem that if you depended on a task that didn’t exist in the current diagram, you would get a dependency loop problem, but you didn’t know why it was looped.
This is very inconvenient for the developer to debug the problem, so I added the validity judgment of the pre-task, if there is no Log will be directly printed, and also added the debugmode, if the test case can directly end the task does not exist crash.
ksp
I wanted to be lazy so I generated some code with KSP, and I wanted my startup framework to be used for componentization and plugins as well, which was awesome anyway.
Start Task Group
One feature that is currently being done is to generate a group of startup tasks via annotation + KSP. This time we are using version 1.5.30 of KSP, and there are some API changes.
The system provides a finish method because it restarts the process method whenever classes are generated, resulting in stackOverflow. So subsequent code generation can be considered for migration to the new method.
class StartupProcessor( val codeGenerator: CodeGenerator, private val logger: KSPLogger, val moduleName: String ) : SymbolProcessor { private lateinit var startupType: KSType private var isload = false private val taskGroupMap = hashMapOf<String, MutableList<ClassName>>() private val procTaskGroupMap = hashMapOf<String, MutableList<Pair<ClassName, ArrayList<String>>>>() override fun process(resolver: Resolver): List<KSAnnotated> { logger.info("StartupProcessor start") val symbols = resolver.getSymbolsWithAnnotation(StartupGroup::class.java.name) startupType = resolver.getClassDeclarationByName( resolver.getKSNameFromString(StartupGroup::class.java.name) )?.asType() ?: kotlin.run { logger.error("JsonClass type not found on the classpath.") return emptyList() } symbols.asSequence().forEach { add(it) } return emptyList() } private fun add(type: KSAnnotated) { logger.check(type is KSClassDeclaration && type.origin == Origin.KOTLIN, type) { "@JsonClass can't be applied to $type: must be a Kotlin class" } if (type ! is KSClassDeclaration) return //class type val routerAnnotation = type.findAnnotationWithType(startupType) ? : return val groupName = routerAnnotation.getMember<String>("group") val strategy = routerAnnotation.arguments.firstOrNull { it.name?.asString() == "strategy" }?.value.toString().toValue() ?: return if (strategy.equals("other", true)) { val key = groupName if (procTaskGroupMap[key] == null) { procTaskGroupMap[key] = mutableListOf() } val list = procTaskGroupMap[key] ?: return list.add(type.toClassName() to (routerAnnotation.getMember("processName"))) } else { val key = "${groupName}${strategy}" if (taskGroupMap[key] == null) { taskGroupMap[key] = mutableListOf() } val list = taskGroupMap[key] ?: return list.add(type.toClassName()) } } private fun String.toValue(): String { var lastIndex = lastIndexOf(".") + 1 if (lastIndex <= 0) { lastIndex = 0 } return subSequence(lastIndex, Lowercase ().upcaseKeyFirstChar ()} Override fun Finish () {super.finish() // logger.error("className:${moduleName}") try { taskGroupMap.forEach { it -> val generateKt = GenerateGroupKt( "${moduleName.upCaseKeyFirstChar()}${it.key.upCaseKeyFirstChar()}", codeGenerator ) it.value.forEach { className -> generateKt.addStatement(className) } generateKt.generateKt() } procTaskGroupMap.forEach { val generateKt = GenerateProcGroupKt( "${moduleName.upCaseKeyFirstChar()}${it.key.upCaseKeyFirstChar()}", codeGenerator ) it.value.forEach { pair -> generateKt.addStatement(pair.first, pair.second) } generateKt.generateKt() } } catch (e: Exception) { logger.error( "Error preparing :" + " ${e.stackTrace.joinToString("\n")}" ) } } } class StartupProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment ): SymbolProcessor { return StartupProcessor( environment.codeGenerator, environment.logger, environment.options[KEY_MODULE_NAME] ?: "application" ) } } fun String.upCaseKeyFirstChar(): String { return if (Character.isUpperCase(this[0])) { this } else { StringBuilder().append(Character.toUpperCase(this[0])).append(this.substring(1)).toString() } } const val KEY_MODULE_NAME = "MODULE_NAME"Copy the code
The Processor is split into two parts, with SymbolProcessorProvider building it and SymbolProcessor handling the AST logic. The old initAPI was moved to the SymbolProcessorProvider.
The logic is also simple: collect annotations and then generate a taskGroup logic based on the annotation’s entry. This group will be manually added to the startup process.
unfinished
Another thing I would like to do is generate a Task from annotations, and then compose a new Task by permutations and combinations of different annotations.
This part of the function is still in the design, after the completion of the water to give you a good.
Debugging components
This part is the most important part of my design recently. More often than not, you need to trace the slow start, which is what we call degradation. How to quickly locate deterioration problems is also a concern of the startup framework.
We are going to through the log report at the beginning, after derivation online again after release the task of time consuming, but because the average is calculated, and our automated testing students prior to the release of each version will run automation case, observe the condition of the startup time, if the time average longer would have to inform us, It’s really hard to find problems when you look at buried data at this point.
The core reason is that I want to be lazy, because troubleshooting must be based on the previous version and the current version of the comparison between each task time, we should have about 30+ startup tasks, which is killing me.
Therefore, AFTER communicating with my boss, I set up a project for this part, planning to use a debugging tool, which can record the time of starting tasks and the list of starting tasks. Through the form of local comparison, we can quickly deduce the tasks with problems, which is convenient for us to locate problems quickly.
Tip: Debug tools should not be developed with too many dependencies and then added with debug buildType so use contentProvider to initialize
Startup timeline
River’s lake has been spreading my nickname – UI big wet, the next wave is not a false name, UI big wet drawing out of the graphics that call a picturesque ah.
The principle of this part is relatively simple. We collect the data of the current starting task, distribute it according to the thread name, record the node where the task starts and ends, and then display it graphically.
If you don’t get it at first glance, check out the list of stock picks. Each column represents a timeline of execution by a thread.
Check whether the startup sequence is changed
We will record the current startup sequence in the database at each startup, and then find out the tasks that are different from the current Hashcode through the database, and then compare them and display them in the form of TextView, which is convenient for testing students’ feedback.
The principle of this place is that I concatenate the entire startup task through strings, and then generate a string, and then use the hashcode of the string as a unique identifier. Different strings generate different Hashcodes.
The silly thing here is that I started comparing hashCode to StringBuilder and then found that the same task changed its value. Silly me.
Don’t ask, ask is UI big wet, textView is not fragrant?
Average Task Time
The database design of this place made me think for a long time, then I took the dimension of day, recorded the time and times, and took out the mean value during rendering.
After that, the previous historical data is retrieved and summarized for statistics. After that, the list is regenerated, with a current task followed by a historical task. And then do some awesome UI rendering.
This time you have to spray ah, why you are all textView and call UI big wet ah.
Have you heard of shrimp and eggs? Yeah, that’s what it is.
conclusion
Roll to, the day does not give birth to me to catch shrimp door, roll way as long as night.
Together with you.
Really summarize
In terms of UI, I will continue to iterate. After all, the first version is ugly and mainly aims to complete the data of the phone. Moreover, the development does not look obvious, and the differences may be directly output later.
Make it big, make a big story.