This article is sponsored by Yugangshuo writing platform, and the copyright belongs to yugangshuo’S wechat official account

Original author: Joker

Copyright: May not be reproduced in any form without permission of Yu Gang said

Welcome to follow my official account, scan the qr code below or search the official id: MXSZGG

This Plugin is based on Android Gradle Plugin version 3.0.1

preface

In daily development, in order to avoid the reflection failure of R files at runtime, R files are generally kept when confused, but this will also lead to a certain increase in package volume, so do you reduce the volume increase caused by R files not confused?

As we all know, the R file in Android is used to store the mapping values of resources, and the number of field values in the R file in an APK is often very large. After decompressing a Debug APK without any operation, the number of lines in the file is more than 1800 —

To reduce packet size, obfuscation compression can be reduced to about 500 lines + —

But there are two problems with obfuscation — if the R file is obfuscated, the resource reflection cannot be used; The other R$*. Class items except R$styleable. Class were removed during the oblibility process, but R$styleable. So how to solve these two problems? One option is not to turn on obfuscation, which is obviously unrealistic; Another option is to enable obfuscation, keep R file in proGuard-rules. pro, and manually delete the field information in R file. In fact, the meilishuo.com team has opened an open source thinrPlugin earlier, which is the use of scheme 2, but it is aimed at Gradle Plugin version 1.0/2.0 and does not support 3.0. This paper will rewrite the plugin and name it ThinR3.

Why can fields in R files be deleted? Fields in R files are classified into two types: static final int and static final int[]. Static final int is a constant that is not changed at runtime, so typing static final int into APK is obviously redundant, so a large part of the R file packaged into APK is redundant.

For example, r.layout. activity in the figure above is perfectly capable of being replaced by its corresponding constant, but it won’t be replaced because keep holds the R file.

The idea of thinR3 is to find places where an R field is used, replace it with the value of that constant if it is a constant, and remove that field from the R file, removing redundancy —

To run the plug-in, the.class file is traversed twice. In the first traversal, the r.class file is obtained, all constant values are traversed and encapsulated into key-value pairs to replace fields with corresponding constants. The second iteration can be done either by deleting constants from R files or replacing R field references with previous key pairs in other.class files. In order to operate on.class files, you need to introduce ASM. It doesn’t matter if you don’t know ASM, not many of the plug-ins described in this article use ASM.

So when and where to get the.class file is essentially the heart of the task — in theory, the later the.class file is, the less risk it will be modified, The author chose the confusing task (transformClassesAndResourcesWithProguardFor ${variant. The name. Capitalize ()}) to perform after enable the plugin. There are two reasons for this. One is that the execution of the task is late. Second, the artifacts of a confusedTask contain all.class file information (the task interface contains the outputs field, and the developer can retrieve the artifacts of the Task through task.elsions.files.files).

To sum up, Will in transformClassesAndResourcesWithProguardFor ${variant. The name. Capitalize ()} task execution after traversal twice to get the task of outputs. The files. The files, The first traversal is to find the R file and collect the key-value pairs of static final Int constants in that file. The second traversal is to replace the R file fields in other files based on the key-value pairs and delete the R file fields.

Create a project

  1. Create a new project and set minifyEnabled in the release closure under app/build.gralde to true to enable release package obfuscation, and to avoid obfuscation of R files, You need to add the following code under the R file to keep the R file —

    -keepclassmembers class **.R$* { public static <fields>; } -keep class **.R {*; } -keep class **.R$* {*; } -keep class **.R$* -keep class **.RCopy the code
  2. Create a new Java Module, name it buildSrc, change SRC /main/ Java to SRC /main/groovy, and add SRC /main/resources/ meta-INF /gradle-plugins. In the folder to create com. Joker. Thinr3.. the properties file and fill in the implementation – the class point to the plugin, eventually the diagram below:

  1. Modify the build.gradle file in the Module folder:

    apply plugin: 'groovy'
    
    dependencies {
        implementation gradleApi()
        implementation localGroovy()
        implementation 'com. Android. Tools. Build: gradle: 3.0.1'
    }
    
    allprojects {
        repositories {
            jcenter()
            google ()
        }
    }
    Copy the code

Because Android Gradle Plugin depends on ASM library, so in the premise of relying on the base library and then rely on Android Gradle Plugin.

The plugin in field

Create thinR3plugin.groovy with the following source code:

package com.joker.thinr3

import com.android.build.gradle.api.ApkVariantOutput
import com.android.build.gradle.api.ApplicationVariant
import org.gradle.api.Plugin
import org.gradle.api.Project

class ThinR3Plugin implements Plugin<Project> {
  @Override
  void apply(Project project) {
    project.afterEvaluate {
      project.plugins.withId('com.android.application') {
        project.android.applicationVariants.all { ApplicationVariant variant ->
          variant.outputs.each { ApkVariantOutput variantOutput ->
            if (variantOutput.name.contains("release")) {
              project.tasks.
                  findByName("transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}")
                  .doLast { ProcessAndroidResources task ->
                    task.outputs.files.files.each {
                      collectRInfo()
                    }
                    task.outputs.files.files.each {
                      replaceAndDelRInfo()
                    }
                  }
            }
          }
        }
      }
    }
  }
}
Copy the code

Hook project afterEvaluate {} to get all the task information in the project. Since ThinR3 is implemented by hook obfuscating tasks, this means that the current project must be the main project, Project.plugins.withid (‘com.android.application’) can be used to determine whether the current project is the primary project. In general, plugins will do R file reduction for the Release package and this is not necessary for other variants, as the applicationVariants closure in AppExtension will contain all apK variants. In ApplicationVariant, a field is called “AbstractTask”, which is a collection of all final variants of the task. The output of the outputs is the final variant of the task. In fact, we only need to obtain the name information of the variant, and then pass String#contains(“release”) to determine whether the current apk is a release package; FindByName (taskName) to find the obfuscated task and hook its final execution phase with doLast {}. Finally, the way to obtain obfuscated task artifacts is the above mentioned elsion.files.files.

Now that you have the obfuscated product, you can operate on it. First of all, there is no doubt that the final operation must be on a class file, so the result of the confusion is a class file? Print the path to the file in the each {} closure:

Json is not a key file. Only 0.jar is a key file. Open 0.jar and take a look

Jar = jar = jar = jar = jar = jar = jar = jar = jar = jar The modified code is as follows:

it.outputs
	.files
	.files
	.grep { File file -> file.isDirectory() }
	.each { File dir ->
          dir.listFiles({ File file -> file.name.endsWith(".jar"Asmhelper. collectRInfo(jar)}} as FileFilter). Each {File jar -> //Copy the code

Collect R file information in jar package source as follows:

static void collectRInfo(File jar) { JarFile jarFile = new JarFile(jar) jarFile .entries() .grep { JarEntry entry -> isRFile(entry.name) } .each { JarEntry jarEntry -> jarFile.getInputStream(jarEntry).withStream { InputStream inputStream  -> ClassReader classReader = new ClassReader(inputStream) ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM4) { @Override FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {if (value instanceof Integer) {
              map.put(jarEntry.name - ".class" + name, value)
            } else {
              styleableSet.add(jarEntry.name)
            }
            return super.visitField(access, name, desc, signature, value)
          }
        }
        classReader.accept(classVisitor, 0)
      }
    }

    jarFile.close()
  }
Copy the code

Get the jar Enumeration with JarFile#entries(), and each object in the Enumeration is actually a file in the jar. Similarly, r.class files are filtered out using Groovy notation matching class names. Finally, ASM is used to obtain the key-value pairs of all the replaceable fields in R.class. Byte []/InputStream/className creates a ClassReader object. The second step is to create the ClassVisitor class and implement its visitField() method, whose name already lets developers know that it is used to access fields in the class. As mentioned earlier, the fields that can and can only be replaced are static final Ints. Therefore, you can determine whether the current field can be replaced based on whether the type of the last parameter in the method is Integer. If it can be replaced, it can be stored in the Map. The third step is to call ClassReader# Accept (ClassVisitor, flag) so that the ClassVisitor gets the class file information through the ClassReader.

After collecting the information, it is necessary to replace the information in other class files and delete the information in R.class. The source code is as follows:

  static void replaceAndDelRInfo(File jar) {
    File newFile = new File(jar.parentFile, jar.name + ".bak")
    JarFile jarFile = new JarFile(jar)
    new JarOutputStream(new FileOutputStream(newFile)).withStream { OutputStream jarOutputStream ->
      jarFile.entries()
          .grep { JarEntry entry -> entry.name.endsWith(".class") }
          .each { JarEntry entry ->
        jarFile.getInputStream(entry).withStream { InputStream inputStream ->
          def fileBytes = inputStream.bytes

          switch (entry) {
            case { isRFileExceptStyleable(entry.name) }:
              fileBytes = null
              break
            case { isRFile(entry.name) }:
              fileBytes = deleteRInfo(fileBytes)
              break
            default:
              fileBytes = replaceRInfo(fileBytes)
              break
          }

          if(fileBytes ! = null) { jarOutputStream.putNextEntry(new ZipEntry(entry.name)) jarOutputStream.write(fileBytes) jarOutputStream.closeEntry() } } } jarFile.close() jar.delete() newFile.renameTo(jar) } }Copy the code

Bak to replace the original 0.jar; Likewise, take advantage of Groovy’s language to filter out.class files; Obtain bytes from the 0.jar file [] to modify it. There are three cases:

  • R file and not R$styleable.class file (e.g. R$id.class), then the file will be deleted.
  • Is R$styleable. Class file, passdeleteRInfo()Return using ASM removedstatic final intField (reservedstatic final int[]Field) class file bytes.
  • It’s not an R file and it’s not an inner class file, so it’s a normal class file, passedreplaceRInfo()Returns the plain class file bytes after the fields are replaced with ASM and the map that contains the replacement field information.

JarEntry is a subclass of ZipEntry that extends attributes such as certificates. But the class file does not contain these) and fills it with the bytes returned by the previous method. Finally, don’t forget to close the resource, delete 0.jar, and rename 0.jar to 0.jar.

  • replaceRInfo()The source code is as follows:
private static byte[] replaceRInfo(byte[] bytes) { ClassReader classReader = new ClassReader(bytes) ClassWriter classWriter = new ClassWriter(classReader, 0) ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM4, classWriter) { @Override MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { def methodVisitor = super.visitMethod(access, name, desc, signature, exceptions) methodVisitor = new MethodVisitor(Opcodes.ASM4, methodVisitor) { @Override void visitFieldInsn(int opcode, String owner, String name1, String desc1) { Integer constantValue = map.get(owner + name1) constantValue ! = null ? super.visitLdcInsn(constantValue) : super.visitFieldInsn(opcode, owner, name1, desc1) } }return methodVisitor
      }
    }
    classReader.accept(classVisitor, 0)

    classWriter.toByteArray()
  }
Copy the code

The core content is in visitMethod(), and the rest are set pieces. Because you need to modify the class file, it’s definitely not ok to use the old MethodVisitor, so create a new MethodVisitor with the old MethodVisitor and return, Overwrite the visitFieldInsn() of the new MethodVisitor to replace the field value with the map of the previous section to see if the current field exists and if so, replace it with the corresponding constant. Otherwise unchanged (The visitFieldInsn() of MethodVisitor replaces fields not only in the method, but also in the class).

  • deleteRInfo()The source code is as follows:
  private static byte[] deleteRInfo(byte[] fileBytes) {
    ClassReader classReader = new ClassReader(fileBytes)
    ClassWriter classWriter = new ClassWriter(classReader, 0)
    ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM4, classWriter) {
      @Override
      FieldVisitor visitField(int access, String name, String desc, String signature,
          Object value) {
        value instanceof Integer ? null : super.visitField(access, name, desc, signature, value)
      }
    }
    classReader.accept(classVisitor, 0)

    return classWriter.toByteArray()
  }
Copy the code

Just use the visitField() of the ClassVisitor visitor to check if the current field is of type Integer. If so, return null. Otherwise, no changes are made.

The latter

This article project source please stamp me

In addition, I set up a wechat group. If you are interested in Gradle or have any discussions with me, you are welcome to join the group. There are also a lot of helpful friends in the group. As the group is full of 100 people, the author’s wechat needs to be added first. Note: Enter the group. (Only in the group to grab red envelopes and post articles please detour)