com.android.build.gradle.internal.incremental.IncrementalVisitor.java Source code

Java tutorial

Introduction

Here is the source code for com.android.build.gradle.internal.incremental.IncrementalVisitor.java

Source

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.build.gradle.internal.incremental;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.utils.FileUtils;
import com.android.utils.ILogger;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.commons.SerialVersionUIDAdder;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;

public class IncrementalVisitor extends ClassVisitor {

    /**
     * Defines the output type from this visitor.
     */
    public enum OutputType {
        /**
         * provide instrumented classes that can be hot swapped at runtime with an override class.
         */
        INSTRUMENT,
        /**
         * provide override classes that be be used to hot swap an instrumented class.
         */
        OVERRIDE
    }

    public static final String RUNTIME_PACKAGE = "com/android/tools/ir/runtime";
    public static final String ABSTRACT_PATCHES_LOADER_IMPL = RUNTIME_PACKAGE + "/AbstractPatchesLoaderImpl";
    public static final String APP_PATCHES_LOADER_IMPL = RUNTIME_PACKAGE + "/AppPatchesLoaderImpl";

    protected static final Type INSTANT_RELOAD_EXCEPTION = Type
            .getObjectType(RUNTIME_PACKAGE + "/InstantReloadException");
    protected static final Type RUNTIME_TYPE = Type.getObjectType(RUNTIME_PACKAGE + "/AndroidInstantRuntime");
    public static final Type DISABLE_ANNOTATION_TYPE = Type
            .getObjectType("com/android/tools/ir/api/DisableInstantRun");
    public static final Type TARGET_API_TYPE = Type.getObjectType("android/annotation/TargetApi");

    protected static final boolean TRACING_ENABLED = Boolean.getBoolean("FDR_TRACING");

    public static final Type CHANGE_TYPE = Type.getObjectType(RUNTIME_PACKAGE + "/IncrementalChange");

    protected String visitedClassName;
    protected String visitedSuperName;
    @NonNull
    protected final ClassNode classNode;
    @NonNull
    protected final List<ClassNode> parentNodes;
    @NonNull
    protected final ILogger logger;

    /**
     * Enumeration describing a method of field access rights.
     */
    protected enum AccessRight {
        PRIVATE, PACKAGE_PRIVATE, PROTECTED, PUBLIC;

        @NonNull
        static AccessRight fromNodeAccess(int nodeAccess) {
            if ((nodeAccess & Opcodes.ACC_PRIVATE) != 0)
                return PRIVATE;
            if ((nodeAccess & Opcodes.ACC_PROTECTED) != 0)
                return PROTECTED;
            if ((nodeAccess & Opcodes.ACC_PUBLIC) != 0)
                return PUBLIC;
            return PACKAGE_PRIVATE;
        }
    }

    public IncrementalVisitor(@NonNull ClassNode classNode, @NonNull List<ClassNode> parentNodes,
            @NonNull ClassVisitor classVisitor, @NonNull ILogger logger) {
        super(Opcodes.ASM5, classVisitor);
        this.classNode = classNode;
        this.parentNodes = parentNodes;
        this.logger = logger;
        this.logger.verbose("%s: Visiting %s", getClass().getSimpleName(), classNode.name);
    }

    @NonNull
    protected static String getRuntimeTypeName(@NonNull Type type) {
        return "L" + type.getInternalName() + ";";
    }

    @Nullable
    FieldNode getFieldByName(@NonNull String fieldName) {
        FieldNode fieldNode = getFieldByNameInClass(fieldName, classNode);
        Iterator<ClassNode> iterator = parentNodes.iterator();
        while (fieldNode == null && iterator.hasNext()) {
            ClassNode parentNode = iterator.next();
            fieldNode = getFieldByNameInClass(fieldName, parentNode);
        }
        return fieldNode;
    }

    @Nullable
    protected static FieldNode getFieldByNameInClass(@NonNull String fieldName, @NonNull ClassNode classNode) {
        //noinspection unchecked ASM api.
        List<FieldNode> fields = classNode.fields;
        for (FieldNode field : fields) {
            if (field.name.equals(fieldName)) {
                return field;
            }
        }
        return null;
    }

    @Nullable
    protected MethodNode getMethodByName(String methodName, String desc) {
        MethodNode methodNode = getMethodByNameInClass(methodName, desc, classNode);
        Iterator<ClassNode> iterator = parentNodes.iterator();
        while (methodNode == null && iterator.hasNext()) {
            ClassNode parentNode = iterator.next();
            methodNode = getMethodByNameInClass(methodName, desc, parentNode);
        }
        return methodNode;
    }

    @Nullable
    protected static MethodNode getMethodByNameInClass(String methodName, String desc, ClassNode classNode) {
        //noinspection unchecked ASM API
        List<MethodNode> methods = classNode.methods;
        for (MethodNode method : methods) {
            if (method.name.equals(methodName) && method.desc.equals(desc)) {
                return method;
            }
        }
        return null;
    }

    protected static void trace(@NonNull GeneratorAdapter mv, @Nullable String s) {
        mv.push(s);
        mv.invokeStatic(Type.getObjectType(RUNTIME_PACKAGE + "/AndroidInstantRuntime"),
                Method.getMethod("void trace(String)"));
    }

    @SuppressWarnings("unused")
    protected static void trace(@NonNull GeneratorAdapter mv, @Nullable String s1, @Nullable String s2) {
        mv.push(s1);
        mv.push(s2);
        mv.invokeStatic(Type.getObjectType(RUNTIME_PACKAGE + "/AndroidInstantRuntime"),
                Method.getMethod("void trace(String, String)"));
    }

    @SuppressWarnings("unused")
    protected static void trace(@NonNull GeneratorAdapter mv, @Nullable String s1, @Nullable String s2,
            @Nullable String s3) {
        mv.push(s1);
        mv.push(s2);
        mv.push(s3);
        mv.invokeStatic(Type.getObjectType(RUNTIME_PACKAGE + "/AndroidInstantRuntime"),
                Method.getMethod("void trace(String, String, String)"));
    }

    protected static void trace(@NonNull GeneratorAdapter mv, @Nullable String s1, @Nullable String s2,
            @Nullable String s3, @Nullable String s4) {
        mv.push(s1);
        mv.push(s2);
        mv.push(s3);
        mv.push(s4);
        mv.invokeStatic(Type.getObjectType(RUNTIME_PACKAGE + "/AndroidInstantRuntime"),
                Method.getMethod("void trace(String, String, String, String)"));
    }

    protected static void trace(@NonNull GeneratorAdapter mv, int argsNumber) {
        StringBuilder methodSignature = new StringBuilder("void trace(String");
        for (int i = 0; i < argsNumber - 1; i++) {
            methodSignature.append(", String");
        }
        methodSignature.append(")");
        mv.invokeStatic(Type.getObjectType(RUNTIME_PACKAGE + "/AndroidInstantRuntime"),
                Method.getMethod(methodSignature.toString()));
    }

    /**
     * Simple Builder interface for common methods between all byte code visitors.
     */
    public interface VisitorBuilder {
        @NonNull
        IncrementalVisitor build(@NonNull ClassNode classNode, @NonNull List<ClassNode> parentNodes,
                @NonNull ClassVisitor classVisitor, @NonNull ILogger logger);

        @NonNull
        String getMangledRelativeClassFilePath(@NonNull String originalClassFilePath);

        @NonNull
        OutputType getOutputType();
    }

    /**
     * Defines when a method access flags are compatible with InstantRun technology.
     *
     * - If the method is a bridge method, we do not enable it for instantReload.
     *   it is most likely only calling a twin method (same name, same parameters).
     * - if the method is abstract or native, we don't add a redirection.
     *
     * @param access the method access flags
     * @return true if the method should be InstantRun enabled, false otherwise.
     */
    protected static boolean isAccessCompatibleWithInstantRun(int access) {
        return (access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_BRIDGE | Opcodes.ACC_NATIVE)) == 0;
    }

    @Nullable
    public static File instrumentClass(int targetApiLevel, @NonNull File inputRootDirectory,
            @NonNull File inputFile, @NonNull File outputDirectory, @NonNull VisitorBuilder visitorBuilder,
            @NonNull ILogger logger) throws IOException {

        byte[] classBytes;
        String path = FileUtils.relativePath(inputFile, inputRootDirectory);

        // if the class is not eligible for IR, return the non instrumented version or null if
        // the override class is requested.
        if (!isClassEligibleForInstantRun(inputFile)) {
            if (visitorBuilder.getOutputType() == OutputType.INSTRUMENT) {
                File outputFile = new File(outputDirectory, path);
                Files.createParentDirs(outputFile);
                Files.copy(inputFile, outputFile);
                return outputFile;
            } else {
                return null;
            }
        }
        classBytes = Files.toByteArray(inputFile);
        ClassReader classReader = new ClassReader(classBytes);
        // override the getCommonSuperClass to use the thread context class loader instead of
        // the system classloader. This is useful as ASM needs to load classes from the project
        // which the system classloader does not have visibility upon.
        // TODO: investigate if there is not a simpler way than overriding.
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES) {
            @Override
            protected String getCommonSuperClass(final String type1, final String type2) {
                Class<?> c, d;
                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                try {
                    c = Class.forName(type1.replace('/', '.'), false, classLoader);
                    d = Class.forName(type2.replace('/', '.'), false, classLoader);
                } catch (ClassNotFoundException e) {
                    // This may happen if we're processing class files which reference APIs not
                    // available on the target device. In this case return a dummy value, since this
                    // is ignored during dx compilation.
                    return "instant/run/NoCommonSuperClass";
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                if (c.isAssignableFrom(d)) {
                    return type1;
                }
                if (d.isAssignableFrom(c)) {
                    return type2;
                }
                if (c.isInterface() || d.isInterface()) {
                    return "java/lang/Object";
                } else {
                    do {
                        c = c.getSuperclass();
                    } while (!c.isAssignableFrom(d));
                    return c.getName().replace('.', '/');
                }
            }
        };

        ClassNode classNode = AsmUtils.readClass(classReader);

        // when dealing with interface, we just copy the inputFile over without any changes unless
        // this is a package private interface.
        AccessRight accessRight = AccessRight.fromNodeAccess(classNode.access);
        File outputFile = new File(outputDirectory, path);
        if ((classNode.access & Opcodes.ACC_INTERFACE) != 0) {
            if (visitorBuilder.getOutputType() == OutputType.INSTRUMENT) {
                // don't change the name of interfaces.
                Files.createParentDirs(outputFile);
                if (accessRight == AccessRight.PACKAGE_PRIVATE) {
                    classNode.access = classNode.access | Opcodes.ACC_PUBLIC;
                    classNode.accept(classWriter);
                    Files.write(classWriter.toByteArray(), outputFile);
                } else {
                    // just copy the input file over, no change.
                    Files.write(classBytes, outputFile);
                }
                return outputFile;
            } else {
                return null;
            }
        }

        AsmUtils.DirectoryBasedClassReader directoryClassReader = new AsmUtils.DirectoryBasedClassReader(
                getBinaryFolder(inputFile, classNode));

        // if we are targeting a more recent version than the current device, disable instant run
        // for that class.
        List<ClassNode> parentsNodes = isClassTargetingNewerPlatform(targetApiLevel, TARGET_API_TYPE,
                directoryClassReader, classNode, logger) ? ImmutableList.of()
                        : AsmUtils.parseParents(logger, directoryClassReader, classNode, targetApiLevel);
        // if we could not determine the parent hierarchy, disable instant run.
        if (parentsNodes.isEmpty() || isPackageInstantRunDisabled(inputFile)) {
            if (visitorBuilder.getOutputType() == OutputType.INSTRUMENT) {
                Files.createParentDirs(outputFile);
                Files.write(classBytes, outputFile);
                return outputFile;
            } else {
                return null;
            }
        }

        outputFile = new File(outputDirectory, visitorBuilder.getMangledRelativeClassFilePath(path));
        Files.createParentDirs(outputFile);
        IncrementalVisitor visitor = visitorBuilder.build(classNode, parentsNodes, classWriter, logger);

        if (visitorBuilder.getOutputType() == OutputType.INSTRUMENT) {
            /*
             * Classes that do not have a serial version unique identifier, will be updated to
             * contain one. This is accomplished by using the {@link SerialVersionUIDAdder} class
             * visitor that is added when this visitor is created (see the constructor). This way,
             * the serialVersionUID is the same for instrumented and non-instrumented classes. All
             * classes will have a serialVersionUID, so if some of the classes that is extended
             * starts implementing {@link java.io.Serializable}, serialization and deserialization
             * will continue to work correctly.
             */
            classNode.accept(new SerialVersionUIDAdder(visitor));
        } else {
            classNode.accept(visitor);
        }

        Files.write(classWriter.toByteArray(), outputFile);
        return outputFile;
    }

    @NonNull
    private static File getBinaryFolder(@NonNull File inputFile, @NonNull ClassNode classNode) {
        return new File(inputFile.getAbsolutePath().substring(0,
                inputFile.getAbsolutePath().length() - (classNode.name.length() + ".class".length())));
    }

    /**
     * If the passed class is annotated with an annotation type that denotes the target
     * api level like {@link #TARGET_API_TYPE}, and will return true if the passed targetApiLevel
     * is lower than the value of the annotation.
     * @param targetApiLevel the target api level we are instrumenting against.
     * @param targetApiAnnotationType the type of the annotation to look for
     * @param locator a class locator implementation that can locate and load outclasses if any
     * @param classNode the class of interest
     * @param logger to log messages
     * @return true if the class of interest is annotated with targetApiAnnotationType and the
     * annotation value is superior to the passed targetApiLevel.
     * @throws IOException when outer classes cannot be loaded successfully.
     */
    @VisibleForTesting
    static boolean isClassTargetingNewerPlatform(int targetApiLevel, @NonNull Type targetApiAnnotationType,
            @NonNull AsmUtils.ClassReaderProvider locator, @NonNull ClassNode classNode, @NonNull ILogger logger)
            throws IOException {

        List<AnnotationNode> invisibleAnnotations = AsmUtils.getInvisibleAnnotationsOnClassOrOuterClasses(locator,
                classNode, logger);
        for (AnnotationNode classAnnotation : invisibleAnnotations) {
            if (classAnnotation.desc.equals(targetApiAnnotationType.getDescriptor())) {
                int valueIndex = 0;
                List values = classAnnotation.values;
                while (valueIndex < values.size()) {
                    String name = (String) values.get(valueIndex);
                    if (name.equals("value")) {
                        Object value = values.get(valueIndex + 1);
                        return Integer.class.cast(value) > targetApiLevel;
                    }
                    valueIndex = valueIndex + 2;
                }
            }
        }
        return false;
    }

    private static boolean isPackageInstantRunDisabled(@NonNull File inputFile) throws IOException {

        ClassNode packageInfoClass = AsmUtils.parsePackageInfo(inputFile);
        if (packageInfoClass != null) {
            //noinspection unchecked
            List<AnnotationNode> annotations = packageInfoClass.invisibleAnnotations;
            if (annotations == null) {
                return false;
            }
            for (AnnotationNode annotation : annotations) {
                if (annotation.desc.equals(DISABLE_ANNOTATION_TYPE.getDescriptor())) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Return true of the class is eligible to be InstantRun enabled, false otherwise.
     * @param inputFile the input file containing the byte codes.
     * @return true if the class should be instrumented for InstantRun, false otherwise.
     */
    @VisibleForTesting
    static boolean isClassEligibleForInstantRun(@NonNull File inputFile) {

        if (inputFile.getPath().endsWith(SdkConstants.DOT_CLASS)) {
            String fileName = inputFile.getName();
            return !fileName.equals("R" + SdkConstants.DOT_CLASS) && !fileName.startsWith("R$");
        } else {
            return false;
        }
    }
}