I improved our build system and activated incremental builds and compilation as described in this question. To my disappointment, incremental compilation didn't improve build times as much as I expected from reading Gradles blog post.
After some investigation, I realized that the issue is that even though I only add a comment to a small class somewhere deep in the app apparently almost the whole codebase is rebuilt. In fact, it doesn't really matter which class I touch, Gradles --debug
output reveals that it basically always recompiles 476 classes.
Incremental compilation of 476 classes completed in 12.51 secs.
While I understand that public static
constants in the changed file trigger a full recompilation (which is only slightly slower), I don't understand how to properly break up class dependencies so that incremental compilation actually works. What are the exact rules to determine class dependencies that affect incremental compilation? I could read about some examples here, but it doesn't appear to apply to our (fairly standard) project at all.
Some of my own testings yielded the following:
// One of my main classes that has lots of class dependencies
public class A{
public void foo() {
// This line produces a dependency between A and B. So changing just
// a comment in B triggers recompilation of all classes attached to A
B b1 = new B();
}
}
// A small helper class that I want to change
public class B {
public void bar() {
// This line does not create a dependency, so B can still be compiled by
// itself. But usually, that's not the "common" direction you have.
A a1 = new A();
// I make the change here and then trigger a new build
}
}
Why does A need recompilation when an implementation detail, but not the interface of B changed?
I also tried "hiding" B behind an interface C. I thought that would be the proper (even though often times very cumbersome) way to break up the dependencies. But it turns out it didn't help at all.
public class A{
public void foo() {
C c1 = C.cFactory();
}
}
public class B implements C {
public void bar() {
// I make the change here and then trigger a new build
}
}
public interface C {
void bar();
public static C cFactory() {
return new B();
}
}
It appears to me that we have this big dependency blob and I am uncertain this could reasonably be changed even though I would argue we have a reasonably designed codebase. Are there best practices, guidelines or design patterns that are common among Android developers that would effectively improve incremental compilations?
I do wonder whether others have the same problem as we do and if not what did you do?
The problem is in fact that all our classes are part of a big dependency graph. This shows in the quote
Incremental compilation of 476 classes completed in 12.51 secs.
Basically, any class I touch causes a recompilation of 476 classes. In our particular case, this is caused by two patterns. We use Explicit Intents in Android, which I am not sure how to better handle. Additionally, we use Dagger in such a way, that we connected all classes in a circle.
Concerning the interface problematic I made a rather obvious mistake with implementing the cFactory()
. As this creates a class dependency from C
to B
and thus transitively from A
to C
.
The following snippet breaks the dependency from A
to B
but creates one from B
to A
.
public class A{
public static void foo(C input) {
input.foo();
}
}
public class B implements C {
public void bar() {
// I make the change here and then trigger a new build
A.foo(this);
}
}
public interface C {
void bar();
}
来源:https://stackoverflow.com/questions/54600574/how-to-properly-deal-with-class-dependencies-to-utilize-incremental-compilation