kezong / fat-aar-android

A gradle plugin that merge dependencies into the final aar file works with AGP 3.+
MIT License
3.15k stars 625 forks source link

Implement bytecode patching to solve NoSuchFieldError issues #184

Open aasitnikov opened 4 years ago

aasitnikov commented 4 years ago

Because we embed modules into one fat-aar, aapt wont generate R class for these modules. So, fat-aar generates R.java classes by itself, and puts it into "lib/r-classes.jar" inside resulting aar.

Imagine we have a resource, that was present in one version of some other library, and was removed in next version. For example, take TextAppearance.MaterialComponents.Tab - it was a private resource of MaterialComponents:1.0.0, and it was removed in MaterialComponents:1.1.0. If we compile our using version 1.0.0, fat-aar generates following R.java class:

package my.emdedded.library;

public final class R {
    public R() {
    }

    public static final class style {
        ...
        public static final int TextAppearance_MaterialComponents_Tab;
        ...

        static {
            ...
            TextAppearance_MaterialComponents_Tab = my.fat.library.R.style.TextAppearance_MaterialComponents_Tab;
            ...
        }
    }
    ...
}

When consumer of this aar uses MaterialComponents 1.0.0 and compiles an apk, aapt will generate my.fat.library.R.java class with TextAppearance_MaterialComponents_Tab field in it. But if consumer compiles it's apk with MaterialComponents:1.1.0, my.fat.library.R.style.TextAppearance_MaterialComponents_Tab won't be generated by aapt, when compiling app module. So, in runtime, when my.emdedded.library.R class first loaded, NoSuchFieldError will be thrown in static initialiser.

Fat-aar plugin can solve this problem using bytecode patching. Plugin should patch classes from embedded aar, changing imports of R classes to match fat-aar's package. In example above, in every class of embedded library, references to my.emdedded.library.R should be replaced with my.fat.library.R. Then there will be no need to generate R.java classes into r-classes.jar.

If implemented, this change will solve a lot of current issues: #75, #90, #136, #161, #170 and probably #169, #180

aasitnikov commented 4 years ago

I've created Transformer, that implements bytecode transformation described above. It uses Transformer API from AGP, and uses JavaAssist to patch bytecode.

To apply it, write the following lines in build.gradle:

android.registerTransform(new EmbedRClassesBytecodeTransformer(
        ["my.embedded.library"], // list all embedded modules here
        "my.fat.library"
))

This transformer can be used in the plugin as is - plugin knows packages of current and embedded modules, and can register transform easily.

UPD: In newer versions (1.2.20+) we need to specify order between tasks

// Explicitly specify order between bytecode transformer and fat-aars mergeClasses
// If transform would occur before mergeClasses, output won't have emdedded classes
android.libraryVariants.all {
    def name = it.name.capitalize()
    tasks.named("transformClassesWithFatAarRTransformFor$name").configure {
        it.dependsOn(tasks.named("mergeClasses$name"))
    }
}
yallam08 commented 4 years ago

@aasitnikov do you have a working demo where you implemented this?

aasitnikov commented 4 years ago

@yallam08 Made a little example app, where this transformer is used https://github.com/aasitnikov/fat-aar-bytecode-patching

aasitnikov commented 3 years ago

@kezong What do you think of adding this bytecode transformer to plugin?

kezong commented 3 years ago

@aasitnikov This is a good idea. You can pull request for the bytecode transformer. To be safe, best to add a switch that can back to the original method.

aprz512 commented 3 years ago

@aasitnikov the demo works fine, even if I didn't uncomment the register transform statement.

kezong commented 3 years ago

It is supported in 1.3.1