Designing Testable App Architecture (Part 1)
Since bytecode manipulation became widespread on mobile platforms, many app architectures have adopted it. The most typical example is IoC frameworks built with the Service Locator pattern. These frameworks all share a similar approach: the less elegant ones use reflection to instantiate objects, while the better ones use apt to generate Factory code. But they all face the same problem – they need a static mapping (registry) to resolve implementations from interfaces, and this registry is typically generated at compile time through bytecode manipulation.
Not Testable
If you’ve never written unit tests on top of such frameworks, you might not see the problem. To make it concrete, let’s first look at how an App is built and run versus how Local UT (local unit tests) are built and run.
Local UT Build and Run
App Build and Run
See the problem?
Transform logic does not execute in the Local UT environment!!!
If the IoC framework’s static mapping (registry) is generated during Transform, then we’re in trouble.
That’s right – Transform plays no part when running Local UT. So how do we solve this?
Making It Testable
Since Local UT doesn’t execute Transform, we have to take matters into our own hands: make Local UT execute Transform logic at runtime. But here’s the catch – AGP‘s native Transform API depends on AGP itself. How do you run AGP in a Local UT environment?
The answer: give up on that path and switch to Booster Transformer!
Why does Booster Transformer work?
Because Booster‘s Transformer was designed to be decoupled from AGP from the start! Now you can see the brilliance of Booster‘s design.
OK, but how does Booster Transformer actually implement Runtime Transform?
Hold on – let’s first revisit the
ClassLoaderfrom Java fundamentals. Looking at its source code:
1 | protected final Class<?> defineClass(String name, byte[] b, int off, int len) |
Give a ClassLoader a byte[], and it gives you back a Class. So as long as we can get the raw byte data of a Class, we can redefine it. Of course, what we want is to manipulate its bytecode before redefining it. How do we get the raw byte data of a Class? – ClassLoader again:
1 | public InputStream getResourceAsStream(String name) { |
So here’s what we need to do:
- Create a custom
ClassLoaderto load classes - During class loading, use Booster‘s API to invoke existing
Transformers - Use the custom
ClassLoaderto run Local UT
For convenience, we extend URLClassLoader directly:
1 | class TransformClassLoader(urls: Array<URL>) : URLClassLoader(urls) { |
This uses AsmTransformer and AbstractTransformContext from Booster. Just add a dependency on booster-transform-asm:
1 | dependencies { |
The overall architecture looks like this:
As you can see, ClassLoader plays a crucial role in running Local UT. With TransformerClassLoader, we can invoke Transformers through Booster at runtime to swap in the classes we need. But two questions remain:
- When do we invoke this
TransformerClassLoader? - How do we use this
TransformerClassLoader?
To be continued…
- Blog Link: https://johnsonlee.io/2021/11/23/testable-app-architecture-design-1.en/
- Copyright Declaration: 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
