I. Background description
Second, effect display
3. Problem analysis and module building
Fourth, problem solving and implementation
Five, a small surprise each day
Vi. Future prospects
I. Background description
As the size of the project grows, so does the compilation speed, sometimes requiring several minutes of compilation time for a change. Based on this common situation, RocketX was introduced to speed up full compilation by dynamically replacing the Module with an AAR during the compilation process. Let you experience the aar speed of all modules, but keep all modules easy to modify, perfect!
Second, effect display
2.1. Test project introduction
- Total of target projects
3W+
Class and resource files, full compilation4min
Or so - through
RocketX
Effect after full increment (average 3 times for each operation)
- The project dependencies are shown below.
app
Rely onbm
Business module,bm
Business modules depend on the top layerbase/comm
The module
rx(RocketX)
Compile – you can see thatrx(RocketX)
In no matter which module compilation speed is basically in control of 30s or so, because only compilationapp
And modified modules, other modules areaar
Packages do not participate in compilation.- Native compilation – when
base/comm
Module changes. All modules at the bottom must be compiled. becauseapp/bmxxx
The module may be in usebase
Interface or variable in the module, and do not know whether changes to. (Then it’s very slow.) - Native compilation – when
bmDiscover
It’s changed. It just needs toapp
The module andbmDiscover
Two modules participate in compilation (fast)
Iii. Problem analysis and module building
3.1 Analysis of ideas and problems
- The last mind map involved the following questions:
- Need to pass through
gradle plugin
The form of dynamic modification has not been changedmodule
Dependence is corresponding toaar
Depend on, ifmodule
Modify, degenerate intoproject
Project dependencies, so that only changes are made at a timemodule
和app
Two modules compile. - Need to put
implement/api moduleB
, modified toimplement/api aarB
And need to know how to add in the plug-inaar
Dependence and eliminate original dependence - We need to build
local maven
Store unmodifiedmodule
The correspondingaar
(You can also passflatDir
Instead of faster) - The compilation process starts and needs to find which one
module
Do the modification - You have to go through each of them
module
To replace the dependence of,module
How are dependencies obtained? Can all module dependencies be retrieved at once, or can they be called individually by module? Does modifying one module dependency block subsequent module dependency callbacks? - each
module
Change intoaar
And then, self dependentchild
Dependencies (network dependencies,aar
), toparent module
How do I find them allparent module
)? Or do you just give it to me?app module
? Is there anyapp
到module
Risk of dependency break? There needs to be a technical solution. - Need to be
hook
Compile the process and complete the displacementloacal maven
Is modified inaar
- provide
AS
The status barbutton
To implement the on/off function, speed up compilation and still allow developers to use the triangles they have become accustomed torun
button
3.2. Module construction
- According to the above analysis, although there are many problems, the whole project can be roughly divided into the following parts:
Iv. Problem Solving and Implementation:
4.1. How do I add them manuallyaar
Dependence, analysisimplement
Source code implementation entry inDynamicAddDependencyMethods
In thetryInvokeMethod
Methods. It’s a dynamic languagemethodMissing
function
tryInvokeMethod
The code analysis
public DynamicInvokeResult tryInvokeMethod(String name, Object... Arguments) {// omit some code... return DynamicInvokeResult.found(this.dependencyAdder.add(configuration, normalizedArgs.get(0), (Closure)null)); }Copy the code
dependencyAdder
Implementation is aDirectDependencyAdder
private class DirectDependencyAdder implements DependencyAdder<Dependency> { private DirectDependencyAdder() { } public Dependency add(Configuration configuration, Object dependencyNotation, @Nullable Closure configureAction) { return DefaultDependencyHandler.this.doAdd(configuration, dependencyNotation, configureAction); }}Copy the code
- Finally, in the
DefaultDependencyHandler.this.doAdd
I’m going to add, andDefaultDependencyHandler
在project
You can get
public interface Project extends Comparable<Project>, ExtensionAware, PluginAware { ... DependencyHandler getDependencies(); . }Copy the code
- while
doAdd
Method three parameters passdebug
Source code discovery,configuration
is"implementation", "api", "compileOnly"
The object generated by these three strings,dependencyNotation
Is aLinkHashMap
There are two key-value pairsname:aarName
.ext:aar
And the last oneconfigureAction
传null
That’s it. Callproject.dependencies.add
Eventually it will bedoAdd
Method, that is, simply call add.
public Dependency add(String configurationName, Object dependencyNotation) { return this.add(configurationName, dependencyNotation, (Closure)null); } public Dependency add(String configurationName, Object dependencyNotation, A Closure configureClosure) {/ / direct call to the doAdd return here enclosing doAdd (this) configurationContainer) getByName (configurationName), dependencyNotation, configureClosure); }Copy the code
- So add it in the same way
aar/jar
Implementation code:configName
是childProject
In theconfigName
, that is,"implementation", "api", "compileOnly"
These three strings, taken intact:
fun addAarDependencyToProject(aarName: String, configName: String, project: // implement/XXX (name: 'libaccount-2.0.0', ext) {// implement/XXX (name: 'libaccount-2.0.0', ext) Val map = linkedMapOf<String, String>() map.put("name", aarName) map.put("ext", "aar") project.dependencies.add(configName, map) }Copy the code
4.2,localMave
Preferred to useflatDir
This is done by specifying a cache directorygetLocalMavenCacheDir
The generatedaar/jar
The package is thrown in, and the dependency is modified by adding the corresponding package through 4.1 aboveaar
You can:
fun flatDirs() {
val map = mutableMapOf<String, File>()
map.put("dirs", File(getLocalMavenCacheDir()))
appProject.rootProject.allprojects {
it.repositories.flatDir(map)
}
}
Copy the code
4.3 The compilation process starts, which one needs to be foundmodule
Do the modification
- Use files that traverse the entire project
lastModifyTime
To do the implementation - Have each
module
For one granularity, recursively traverses the currentmodule
File, put each filelastModifyTime
The integration computes out a unique identifiercountTime
- through
countTime
Compare with last time, same indicates no change, different changes. And it needs to be synchronizedcountTime
Into the local cache - As a whole
3W
Time per file1.2 s
Acceptable, currently in classChangeModuleUtils.kt
implement
4.4 Obtaining module dependency relationship
- The purpose is to find the time to generate a dependency graph for the entire project, and to generate a dependency graph parser here. The time to
run
Compilation oftask
Previously, make sure that the substitution takes effect after the dependency is acquired, and globallymodule
After the dependency graph has been generated (that is, after executionbuild.gradle
), so there are currently two ways:DependencyResolutionListener
和projectsEvaluated
But so far there are problems.
- By listening
DependencyResolutionListener
And, inbeforeResolve
The callback method handles:
public interface DependencyResolutionListener {
void beforeResolve(ResolvableDependencies var1);
void afterResolve(ResolvableDependencies var1);
}
project.gradle.addListener(DependencyResolutionListener listener)
Copy the code
- But here’s the problem
beforeResolve
Callbacks are made multiple times and each one is executedmodule
thebuild.gradle
Resolving the dependency will result in a callback. So if you do it at the business level, wait until the last onemodule
Callback complete, pass againproject.configurations
Get allmodule
Dependency graph of? The answer is yes, but it’s too late. Wait until the last onemodule
Callback after parsingbeforeResolve
If a dependency cannot be modified, the following exception will be reported:
Cannot change dependencies of dependency configuration ':app:implementation' after it has been included in dependency resolution.
Copy the code
-
Change the way through the project. Gradle. ProjectsEvaluated {} after the callback to all module dependencies and modification. Dependency diagrams are available, but changing dependencies will still raise exceptions, which is too late after all.
-
So let’s go through gradle’s life cycle
- Discovery Completed
build.gradle
The life cycle after that is only: Project afterEvaluate
和Gradle.projectsEvaluated
(It didn’t work in 3). So onlyafterEvaluate
,afterEvaluate
Or eachmodule
The dependency is called back once and listens for the last onemodule
The timing of the callback and modification still report the same error. So there’s no time in the life cycle - Directly to find
gradle
Source code to find the exception occurs in:DefaultConfiguration.preventIllegalMutation
This method
private void preventIllegalParentMutation(MutationType type) { if (type ! = MutationType.DEPENDENCY_ATTRIBUTES) { if (this.resolvedState == InternalState.ARTIFACTS_RESOLVED) { throw new InvalidUserDataException(String.format("Cannot change %s of parent of %s after it has been resolved", type, this.getDisplayName())); } else if (this.resolvedState == InternalState.GRAPH_RESOLVED && type == MutationType.DEPENDENCIES) { throw new InvalidUserDataException(String.format("Cannot change %s of parent of %s after task dependencies have been resolved", type, this.getDisplayName())); }}}Copy the code
- The main reason for this anomaly is that
type
为MutationType.DEPENDENCY_ATTRIBUTES
, so where is it assigned toDEPENDENCY_ATTRIBUTES
And modify the dependency before it is resolved to give the correlationgradle
Source code call process:
- So basically pass
apply plugin: 'com.android.application'
Start calling in and pass the Settings
Project. Gradle. ProjectsEvaluated {} to monitor the callback, the type set to DEPENDENCY_ATTRIBUTES, Gradle also uses projectsEvaluated to resolve dependencies in the lifecycle. This is when all module dependency diagrams are generated.
- So the final solution is in
projectsEvaluated
Modify dependencies, but beforeGradlePluginUtils
Before a listener inside it, it filters out everything set in by reflectionprojectsEvaluated
The listenerAction
Anonymous internal object), executed firstrockectXPlugin
“, followed by other listeners. Something like this:
//AppProjectDependencies.kt init { val projectsEvaluatedList = hookProjectsEvaluatedAction() Project. Gradle. ProjectsEvaluated {/ / perform first heavy reliance on resolveDenpendency () / / executed after removing the listener (mainly adjust execution order, heavy dependence to work and not an error, May have AGP version compatibility) val clazz = Class. Class.forname (" org. Gradle. API. Invocation. Gradle ") val method = clazz.getDeclaredMethod("projectsEvaluated", Action::class.java) val mMethodInvocation = MethodInvocation(method, ArrayOf (it)) projectsEvaluatedList. ForEach {it. Dispatch (mMethodInvocation)}}} / / all monitored the projectsEvaluated removed an anonymous inner class fun hookProjectsEvaluatedAction(): List<BroadcastDispatch<BuildListener>> { var removeDispatch = mutableListOf<BroadcastDispatch<BuildListener>>() try { var buildListenerBroadcast: ListenerBroadcast<BuildListener>? = null val fBuildListenerBroadcast = DefaultGradle::class.java.getDeclaredField("buildListenerBroadcast") fBuildListenerBroadcast.isAccessible = true buildListenerBroadcast = fBuildListenerBroadcast.get(project.gradle) as? ListenerBroadcast<BuildListener> val fBroadcast = ListenerBroadcast::class.java.getDeclaredField("broadcast") fBroadcast.isAccessible = true val broadcast: BroadcastDispatch<BuildListener>? = fBroadcast.get(buildListenerBroadcast) as? BroadcastDispatch<BuildListener> val fDispatchers = broadcast? .javaClass? .getDeclaredField("dispatchers") fDispatchers? .isAccessible = true val dispatchers: ArrayList<BroadcastDispatch<BuildListener>>? = fDispatchers? .get(broadcast) as? ArrayList<BroadcastDispatch<BuildListener>> val clazz = Class.forName("org.gradle.internal.event.BroadcastDispatch\$ActionInvocationHandler") val iterator = dispatchers?.iterator() iterator?.let { while (iterator.hasNext()) { try { val next = iterator.next() val fDispatch = next.javaClass.getDeclaredField("dispatch") fDispatch.isAccessible = true val dispatch: Any? = fDispatch.get(next) val fMethodName = clazz.getDeclaredField("methodName") fMethodName.isAccessible = true val methodName = fMethodName.get(dispatch) as? String if (methodName?.contains("projectsEvaluated") == true) { removeDispatch.add(next) iterator.remove() } } catch (ignore: Exception) { } } } } catch (ignore: Exception) { } return removeDispatch }Copy the code
-
This is a perfect solution to the change dependency timing problem. See Issue18 for more details
- How to get each
module
Dependence, dependence is hidden inConfiguration.dependencies
, then throughproject.configurations.maybeCreate(configName)
Find allConfiguration
Object, you can get eachmodule
thedependencies
4.5,module
dependenciesproject
replaceaar
Technical solution
- each
module
The traversal order of dependency substitution is unordered, so the technical solution needs to support unordered substitution - The scheme currently used is: if the current module
A
No change, need to putA
throughlocalMaven
To swapA.aar
And put theA.aar
As well asA
的child
Dependency, given to the first levelparent module
Can. (You may wonder what to do if the parent Module is also an AAR. In fact, there is no problem with this part. I won’t elaborate on it here because it is too long.) - Why should I give it to you
parent
You can’t give it directlyapp
Below is a simple example ifB.aar
Don’t giveA
Module,A
useB
Module interface missing, causing compilation failure
- Give the technical solution demonstration of the overall project replacement:
- The overall realization is in
DependenciesHelper.kt
In this class, because it is too long to talk about, interested in the open source library code
4.5,hook
Compile the process and complete the displacementloacal maven
Is modified inaar
- Click on the triangle
run
, the command executed isapp:assembleDebug
That need to be inassembleDebug
I’ll fill it inuploadLocalMavenTask
Through thefinalizedBy
ourtask
Run to synchronize the modifiedaar
:
val localMavenTask = childProject.tasks.maybeCreate("uploadLocalMaven"+buildType.capitalize(),LocalMavenTask::class.java) localMavenTask.localMaven = this@AarFlatLocalMaven bundleTask? .finalizedBy(localMavenTask)Copy the code
4.6, provideAS
The status barbutton
The little rocket buttons one spitfire and one without spitfire representenable/disable
A broomclean rockectx
The cache needs to be writtenintellij idea plugin
That is, there are currently two plugins, onegradle
A pluginAS
Plug-in:
5. A Little surprise a day (bug
More)
5.1 Discovery clickrun
Button to execute the commandapp:assembleDebug
, eachmodule
在 output
It’s not packed outaar
${Flavor}${BuildType}Aar is a task that runs gradle :assembleDebug.
android.applicationVariants.forEach {
getAppAssembleTask(ASSEMBLE + it.flavorName.capitalize() + it.buildType.name.capitalize())?.let { task ->
hookBundleAarTask(task, it.buildType.name)
}
}
Copy the code
5.2. Repeated problems of multiple JAR packages were found after operation
Implementation fileTree(dir: “libs”, include: [“*.jar”]) implementation fileTree(dir: “libs”, include: [“*.jar”]) It can be determined by the following code:
// There are two kinds of dependencies: No need to add to parent, Because the jar package directly into its aar of libs folder if (childDepency is DefaultSelfResolvingDependency && (childDepency. Files is DefaultConfigurableFileCollection | | childDepency. Files is DefaultConfigurableFileTree)) {/ / here is dependent on the following two: No need to add to parent, // implementation RootProject. files("libs/tingyun-ea-agent-android-2.15.4.jar") // implementation rootProject.files("libs/tingyun-ea-agent-android-2.15.4.jar" implementation fileTree(dir: "libs", include: ["*.jar"]) } else { parentProject.key.dependencies.add(childConfig.name, childDepency) }Copy the code
5.3, discovery,aar/jar
There are multiple ways to rely on it
implementation (name: 'libXXX', ext: 'aar')
implementation files("libXXX.aar")
Copy the code
Solution: Using the first and the second to merge with the AAR leads to class duplication problems
5.4, discovery,aar
New postural dependence
configurations.maybeCreate("default")
artifacts.add("default", file('lib-xx.aar'))
Copy the code
Default Config is actually the holder of the final output AAR of the Module. Default Config can hold a list of AAR. So manually adding an AAR to default Config is equivalent to packaging the current Module.
Solution: by childProject. Configurations. MaybeCreate (” default “). Find all artifacts added aar, release localmaven separately
fun getAarByArtifacts(childProject: Project): MutableList<String> {// Find all current artifacts. Add ("default", File ('xxx.aar')) relies on the coming aar var listArtifact = mutableListOf< defaultartifact >() var aarList = mutableListOf<String>() childProject.configurations.maybeCreate("default").artifacts? .foreach {if (it is DefaultPublishArtifact && "aar".equals(it.type)) {listartifact.add (it)}} // copy a copy to localMaven listArtifact.forEach { it.file.copyTo(File(FileUtil.getLocalMavenCacheDir(), it.file.name), Add (removeExtension(it.file.name))} return aarList} true) // remove suffix (.aar) aarList.add(removeExtension(it.file.name))} return aarList}Copy the code
5.5, discovery,android module
It could be packed upjar
Solution: The code is implemented in jarflatLocalMaven.kt by finding a task named JAR and injecting uploadLocalMaven Task after the JAR task
5.6, discovery,arouter
有 bug
.transform
Don’t passoutputProvider.deleteAll()
Clean up the old cache
The arouter problem has been resolved and the code has been merged. However, no new plugin version was released to mavenCentral, so we solved it for Arouter first. However, arouter didn’t start incremental compilation, causing DexArchiveBuilderTask to run very slowly. In the project, I changed the arouter plug-in source code to support the TransForm increment speed to be doubled. The details will be discussed in the next section and dex speed optimization together.
Vi. Future prospects
At present, the preliminary version has been able to run in the project, but there are still many small problems emerging and solving, there is a long way to go, I will continue to search.
Next plan:
dexBuild task
To optimize the- Resolve compatibility issues
At present, the plugin tends to be stable, and friends who like to try it can access through github tutorial, and pay attention to the later progress together. The solutions of subsequent issues will be updated in this article. If you like this article, please give us star. Github open source address