First, open source background

You may have encountered this confusion when writing about library: Some methods or classes in a library class only want to be used by the class in the library, but do not want to be exposed, but because of the hierarchical relationship of the project package, we have to write the method as public, resulting in exposure to the outside world!!

At that time, this question really puzzled me for some time. I can’t write methods/classes as non-public in order not to expose them. How do I call my own library? Do you write reflection yourself? That’s stupid.

Think about hooking up your library generated JAR/AAR packages. Head a hot thigh a clap, damn it, write a plug-in!

Here comes Seeker(Github Portal), the subject of this article.

Second, self-reflection

Before I start, let’s make a mistake here, as I started to get a little bit hot, but the solution to this problem has already been made, @restrictto, and you can check it out if you’re interested.

Before solving the problem, I suggest you to search for more existing solutions. I only noticed @restrictto, choking ing when I finished writing it right away.

Third, the solution

It seems to me that there are two ways to solve this problem:

  • hook libraryAnd then finally packaged intoaar/jarSource code, change methodmodifier
  • inbuildThe procedure directly reports an error, telling the user that the method cannot be called.

Since the second option is a little violent and impersonal, why would you expose it if you wouldn’t let me use it? What the hell is exposing it and making a mistake? With these considerations in mind, I chose a difficult path.

Have a general direction, began to prepare for the implementation of the code, first of all, the need to design the Api layer for users to use, after all, the big guys use is really good;)

I define an @hide annotation with an enum parameter that can be used to specify the modifier:

public enum Modifier {
    /** The modifier {@code public} */ PUBLIC,
    /** The modifier {@code protected} */ PROTECTED,
    /** The modifier {@code private} */ PRIVATE,
    /** The modifier with the default value */ DEFAULT;
}
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Hide {
    Modifier value(a) default Modifier.PRIVATE;
}
Copy the code

Add @hide to hook methods, you can also specify a different modifier, and finally apply my plugin to your library build.gradle.

The Api design is very simple and not invasive to the business, because our library needs to be packaged as aar/ JAR for others to call, so we need to hook up the execution process of uploadArchives Task.

Get @hide

After we add @hide to our methods, we need to find these methods and use them for hook bytecode. What better way to do this than APT?

The use of APT is relatively simple, there is nothing to pay attention to, so it is omitted here. If you are interested, you can learn about it by yourself.

Anyway, we need to get all the @hide methods in this step, and then save a local copy. In this case, I’m saving a JSON file.

Hook process

Here we need to break it down into two steps:

  • hook uploadArchives task
  • Hook bytecode file

Because we ultimately want the packaged JAR/AAR to change, and the packaging is done through the uploadArchives Task, we need to analyze this task and at some point.

5.1. Find tasks that need to be hooked

To analyze the task, we need to know which tasks the task depends on

Add the following code to build. Gradle that contains uploadArchives Task to print the uploadArchives dependencies.

void printTaskDependency(Task task) {
    task.getTaskDependencies().getDependencies(task).any() {
        println(">>${it.path}")
        printTaskDependency(it)
    }
}
gradle.getTaskGraph().whenReady {
    printTaskDependency project.tasks.findByName('uploadArchives')}Copy the code

Next, run a random gradle command and, for convenience, run./gradlew clean to view the printed logs.

UploadArchives depends on Tasks: Click to see details
>>:mock-lib:sourcesJar
>>:mock-lib:bundleRelease
>>:mock-lib:mergeReleaseConsumerProguardFiles
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:prepareLintJar
>>:mock-lib:extractReleaseAnnotations
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:transformClassesAndResourcesWithSyncLibJarsForRelease
>>:mock-lib:extractReleaseAnnotations
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:transformResourcesWithMergeJavaResForRelease
>>:mock-lib:processReleaseJavaRes
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:packageReleaseRenderscript
>>:mock-lib:transformNativeLibsWithSyncJniLibsForRelease
>>:mock-lib:transformNativeLibsWithMergeJniLibsForRelease
>>:mock-lib:mergeReleaseJniLibFolders
>>:mock-lib:generateReleaseAssets
>>:mock-lib:compileReleaseShaders
>>:mock-lib:mergeReleaseShaders
>>:mock-lib:compileReleaseNdk
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:packageReleaseAssets
>>:mock-lib:generateReleaseAssets
>>:mock-lib:compileReleaseShaders
>>:mock-lib:mergeReleaseShaders
>>:mock-lib:compileReleaseShaders
>>:mock-lib:mergeReleaseShaders
Copy the code

According to the printed information above, we can see that there are still a lot of tasks that we rely on, so we check them step by step from front to back. Note: Each person may print different contents and define different tasks.

SourceJar: the first task, sourceJar, is a self-defined task that is used to package Java source code. Since it is custom, it can be ignored and go to the next task.

BundleRelease: What does this task do? Gradle: Build. Gradle: Build. Gradle: Build.

Lucky! AndroidZip class AndroidZip class AndroidZip class AndroidZip class

Package */compile*/generate*/ package*/ generate*/ package*/compile*/generate*/

As a result of the above analysis and bold guess, we need to hook the bundle* task, since the task is packaged, we need to find the bytecode location before the package, and then hook it!!

5.2 hook task

Gradle plugin and gradle life cycle are not described in this section.

We look for the bundle* task in afterEvaluate of our custom plug-in:

mProject.afterEvaluate {
    processVariant()
}
void processVariant() throws NotFoundException {
    // Variant usually has debug and release
    mProject.android.libraryVariants.all { variant ->
        process(variant)
    }
}
void process(variant){
    String taskPath = 'bundle' + mVariant.name.capitalize()
    Task bundleTask = mProject.tasks.findByPath(taskPath)
    if (bundleTask == null) {
        throw new RuntimeException("Can not find task ${taskPath}!")
    }
    bundleTask.doFirst {
        // do hook}}Copy the code

We just execute the bytecode hook before packaging.

5.3 Hook bytecode file deng

To hook bytecode files, we need to consider the following things on our side.

  • Where are the bytecode files stored? json file
  • How do I change a bytecode file?
  • How to change?

Where are the bytecode files stored?

After a series of searches (I didn’t find out how to get this path in Gradle, I don’t know how to get it), I finally found the relative path: /intermediates/packaged-classes/(release/debug)

How do I change a bytecode file?

A third-party library, Javassist, was introduced to alter the bytecode files.

How to change?

Through the JSON file generated during APT, traverse the bytecode file, find the corresponding method, change the modifier to the modifier corresponding to @hide, and then delete @hide.

We all know the solutions to the above problems, and the rest is the implementation process.javassistThe following are some of the problems I encountered while writing this plugin.


Javassist finds classes

In Javassist, we need to find a class through a class ClassPool. Once again, we need to import the bytecode path of the class we need to use into the ClassPool. This is where we encounter the first problem: some classes in gradle projects are directly cached ~/. Gradle /, some classes reference project libs directory, and some.jar, some.aar, how to import these classes one by one?

Answer: Obtain the dependencies dependency of gradle and add the local bytecode file to it. If it is a.jar file, unpack it into a temporary folder. If it is a.aar file, delete it. Unzip.aar and then the classes.jar file.

   // The procedure to get gradle Dependencies
   private List<Configuration> mCopyDependencies
   private void copyDependencies(Configuration configuration) {
       if (configuration == null) {
           return
       }
       Configuration copyConf = null
       try {
           copyConf = mProject.configurations.getByName("${configuration.name}Copy")}catch (Exception ignore) {
       }
       if (copyConf == null) {
           copyConf = mProject.configurations.create("${configuration.name}Copy")
       }
       copyConf.visible = false
       copyConf.extendsFrom configuration
       mCopyDependencies.add(copyConf)
   }
   private void configureDependencies() {
       mCopyDependencies = new ArrayList<>()
       copyDependencies(mProject.configurations.getByName("implementation"))
       copyDependencies(mProject.configurations.getByName("api"))
       copyDependencies(mProject.configurations.getByName("compile"))
       copyDependencies(mProject.configurations.getByName("compileOnly"))
       copyDependencies(mProject.configurations.getByName("provided"))}Copy the code
    // Get the local path for Dependencies
    // This method is implemented in afterEvaluate
    private void resolveArtifacts() {
       def set = new HashSet<>()
       mCopyDependencies.forEach({
           it.each {
               set.add(it.path)
           }
       })
       // ...
   }
Copy the code

In the meantime, you can get/change/delete third-party libraries that you rely on, depending on your needs.

What happens to the place where the method is called when it becomes non-public?

For this problem, there is no elegant way to deal with it. I generated a reflection proxy class in APT process, one @Hide corresponds to one reflected method, and the reflection will be cached to ensure that the reflection of each method will only be called once to ensure performance.

6. Effect demonstration

Library directory structure

Part of the class

The directory structure of the.jar generated by the plug-in

_ * RefDelegate class

Out of the JAR package part of the source code

Call @hide to compare the old and new classes

As can be seen from the picture above, the generatedaar/jarIn the bytecode, the modifier for the method has been changed to the specified modifier, and the place where the call is made is called using the reflection proxy class.

Seven,

For this open source, it was a failure on the whole, but IN the process of writing this open source, I really learned a lot, including how to hook the task of the system, how to hook the bytecode and so on. I think what is more important is the idea of solving the problem. How to solve it step by step, want to customize a Gradle plug-in, where to start.

Finally, if you have any problems looking at Seeker’s source code, You can submit the issue directly. If you are interested in some content in the article, you can comment directly. I will take time to write corresponding content according to the situation

Have the chutziness to place your Seeker Github portal again.