Welcome to follow my public numberEfficient Android Development“Focuses on Android project efficiency and development experience, covering topics such as infrastructure, Kotlin Multiplatform, Gradle construction and optimization, etc. At the same time, we also talk about overseas work and life, and push the latest Podcast of” Two-part radio “(topics such as Internet/geek/engineer).

“Build North” is a series of articles exploring Android building. It covers Gradle, Android Gradle Plugin, Kotlin Script, and other tools, as well as related architecture applications. To find the problem to solve the problem as the starting point, transfer new knowledge to improve production efficiency as the foothold.

Issue review

In the last two days I’ve been maintaining ScratchPaper, updating the AGP (Android Gradle Plugin) version to 3.6.1. During the upgrade process, it was found that the Hook point of AppPlugin used in the original project was invalid:

To recap, reflection retrieval of the BuildToolInfo class was enabled around the 3.3.x-3.5.x iteration. The purpose is to retrieve the path of Aapt2 executables to support custom regenerating of the binary Icon corresponding to App Icon.

package me.xx2bab.scratchpaper.utils

import com.android.build.gradle.AppPlugin
import com.android.build.gradle.BasePlugin
import com.android.build.gradle.internal.scope.GlobalScope
import com.android.sdklib.BuildToolInfo
import org.gradle.api.Project

class AndroidPluginUtils(val project: Project) {

    @Throws(Exception::class)
    fun buildToolInfo(a): BuildToolInfo {
        val basePlugin = project.plugins.findPlugin(AppPlugin::class.java) as BasePlugin<*>
        val scope = getField(BasePlugin::class.java, basePlugin,
                "globalScope") as GlobalScope
        return scope.sdkComponents.buildToolInfoProvider.get()}fun <T> getField(clazz: Class<T>, instance: T, fieldName: String): Any {
        val field = clazz.declaredFields.filter { it.name == fieldName }[0]
        field.isAccessible = true
        return field.get(instance) as Any
    }

}
Copy the code

Let’s upgrade the AGP version that the project depends on to 3.6.1, and run a Demo after resolving common build errors associated with the upgrade:

FAILURE: Build failed with an exception.

* What went wrong:

Execution failed for task ‘:app:processDemoDebugResources’.

Index: 0, Size: 0

.

Caused by: java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

at me.xx2bab.scratchpaper.utils.AndroidPluginUtils.getField(AndroidPluginUtils.kt:21)

at me.xx2bab.scratchpaper.utils.AndroidPluginUtils.buildToolInfo(AndroidPluginUtils.kt:15)

at me.xx2bab.scratchpaper.utils.Aapt2Utils.compileResDir(Aapt2Utils.kt:13)

That’s weird. WhyglobalScopeFailed field reflection?

Problem analysis & source code reproduction

The first step, of course, is to look at the source code for AppPlugin and BasePlugin to see if the field has been removed or renamed. However, I was surprised to find that BasePlugin and AppPlugin were completely empty:

package com.android.build.gradle

import org.gradle.api.Project

/** * The plugin applied with `com.android.application' */
class AppPlugin: BasePlugin() {
    override fun apply(project: Project) {
        super.apply(project)

        project.apply(INTERNAL_PLUGIN_ID)
    }
}

private val INTERNAL_PLUGIN_ID = mapOf("plugin" to "com.android.internal.application")
Copy the code

Really useful code is just a line of the introduction of the internal plug-in, plug-in names is com. The android. Internal. Application. Take a look at the META-INF plugin registry directory

// com.android.internal.application.properties
implementation-class=com.android.build.gradle.internal.plugins.AppPlugin
Copy the code

Unexpectedly appeared another AppPlugin, view the corresponding source code, find the previous version AppPlugin BasePlugin etc class most moved to com. The android. Build. Gradle. Internal. Plugins package, Now, many plugins in com.android.build.gradle package are just proxies for internal plugins.

After that, I want to find out if there is any explanation for this change. After all, the current comment is too simple and does not explain the meaning of proxy too much. Go to GoogleSource and find this commit information:

New public plugin and move existing to internal.

All current plugin classes are considered public API because of how Gradle allows finding plugins. Therefore we need these classes to not change.

However, we also want to have plugin authors target gradle-api instead of the ‘gradle’ artifact. This change forks the current plugin classes into a new set of public class (name unchanged) and the actual implementations as private, internal classes.

The new public plugins delegate to the internal plugins by applying them as separate “internal” plugins. For now the public plugins stay in gradle-core but we’ll move them to gradle-api at some point. This is currently limited by the presence of getExtension on BasePlugin, both of which are now deprecated.

Because our classes have no other public API this should not break anything.

In short, they want to distinguish gradle-core from Gradle-API, And let the plugin authors rely on com. Android. View the build: gradle – API rather than com. Android. View the build: gradle. The agency is just keeping the logic in place, and taking a stake means we’re going to get to work. Interestingly, most of the original plug-in code was written in Java, but now these proxy classes are in Kotlin, which also demonstrates the beginning of the AGP revolution.

The solution

Since the original logic is still in the internal plugin, we can simply replace the import path:

import com.android.build.gradle.internal.plugins.AppPlugin
import com.android.build.gradle.internal.plugins.BasePlugin
Copy the code

You can find old versions of plug-ins such as AppPlugin by findPlugin. Refer to the Commit Message for this iteration for detailed modifications.

conclusion

I am not sure whether agP-based hooks will be as operable as before. It seems that the class changes in version iteration are more frequent now, and THE AGP-based Plugins I maintain may also increase maintenance costs. Polyfill, previously considered for developing a third party AGP, must also be implemented to separate concerns, reduce plug-in development and maintenance costs, and focus on achieving a point goal on the plug-in.

(Written in 2020, Polyfill is now online and the AGP has a new Artifact API, see The Polyfill project description.)