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

Java tutorial

Introduction

Here is the source code for com.android.build.gradle.internal.incremental.InstantRunVerifier.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 static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.CLASS_ANNOTATION_CHANGE;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.COMPATIBLE;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.FIELD_ADDED;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.FIELD_REMOVED;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.FIELD_TYPE_CHANGE;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.IMPLEMENTED_INTERFACES_CHANGE;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.INSTANT_RUN_DISABLED;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.METHOD_ADDED;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.METHOD_ANNOTATION_CHANGE;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.METHOD_DELETED;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.PARENT_CLASS_CHANGED;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.REFLECTION_USED;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.R_CLASS_CHANGE;
import static com.android.build.gradle.internal.incremental.InstantRunVerifierStatus.STATIC_INITIALIZER_CHANGE;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.utils.ILogger;
import com.google.common.base.Objects;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceMethodVisitor;

/**
 * Instant Run Verifier responsible for checking that a class change (between two developers
 * iteration) can be safely hot swapped on the device or not.
 *
 * ThreadSafe
 */
public class InstantRunVerifier {

    private static final Comparator<MethodNode> METHOD_COMPARATOR = new MethodNodeComparator();

    @VisibleForTesting
    static final Comparator<AnnotationNode> ANNOTATION_COMPARATOR = new AnnotationNodeComparator();
    private static final Comparator<Object> OBJECT_COMPARATOR = Objects::equal;
    private static final Comparator<String> STRING_COMPARATOR = Objects::equal;

    private static final Comparator<Object> OBJECT_OR_ANNOTATION_NODE_COMPARATOR = (first, second) -> {
        if (first instanceof AnnotationNode && second instanceof AnnotationNode) {
            return ANNOTATION_COMPARATOR.areEqual((AnnotationNode) first, (AnnotationNode) second);
        }
        return OBJECT_COMPARATOR.areEqual(first, second);
    };

    private static final String KOTLIN_METADATA_ANNOTATION_DESC = "Lkotlin/Metadata;";

    public interface ClassBytesProvider {
        byte[] load() throws IOException;
    }

    public static class ClassBytesFileProvider implements ClassBytesProvider {

        private final File file;

        public ClassBytesFileProvider(File file) {
            this.file = file;
        }

        @Override
        public byte[] load() throws IOException {
            return Files.toByteArray(file);
        }

        @VisibleForTesting
        public File getFile() {
            return file;
        }
    }

    public static class ClassBytesJarEntryProvider implements ClassBytesProvider {

        private final JarFile jarFile;
        private final JarEntry jarEntry;

        public ClassBytesJarEntryProvider(JarFile jarFile, JarEntry jarEntry) {
            this.jarFile = jarFile;
            this.jarEntry = jarEntry;
        }

        @Override
        public byte[] load() throws IOException {
            try (InputStream is = jarFile.getInputStream(jarEntry)) {
                return ByteStreams.toByteArray(is);
            }
        }
    }

    /**
     * describe the difference between two collections of the same elements.
     */
    @VisibleForTesting
    enum Diff {
        /**
         * no change, the collections are equals
         */
        NONE,
        /**
         * an element was added to the first collection.
         */
        ADDITION,
        /**
         * an element was removed from the first collection.
         */
        REMOVAL,
        /**
         * an element was changed.
         */
        CHANGE
    }

    private InstantRunVerifier() {
    }

    public static InstantRunVerifierStatus run(@NonNull File original, @NonNull File updated,
            @NonNull ILogger logger) throws IOException {
        return run(new ClassBytesFileProvider(original), new ClassBytesFileProvider(updated), logger);
    }

    // ASM API not generified.
    @SuppressWarnings("unchecked")
    @NonNull
    public static InstantRunVerifierStatus run(@NonNull ClassBytesProvider original,
            @NonNull ClassBytesProvider updated, @NonNull ILogger logger) throws IOException {

        ClassNode originalClass = loadClass(original);
        ClassNode updatedClass = loadClass(updated);

        if (!originalClass.superName.equals(updatedClass.superName)) {
            return PARENT_CLASS_CHANGED;
        }

        if (diffList(originalClass.interfaces, updatedClass.interfaces, STRING_COMPARATOR) != Diff.NONE) {
            return IMPLEMENTED_INTERFACES_CHANGE;
        }

        if (diffList(originalClass.visibleAnnotations, updatedClass.visibleAnnotations,
                ANNOTATION_COMPARATOR) != Diff.NONE) {
            return CLASS_ANNOTATION_CHANGE;
        }

        // check if the class is InstantRunDisabled.
        List<AnnotationNode> invisibleAnnotations = originalClass.invisibleAnnotations;
        if (invisibleAnnotations != null) {
            for (AnnotationNode annotationNode : invisibleAnnotations) {

                if (annotationNode.desc.equals(IncrementalVisitor.DISABLE_ANNOTATION_TYPE.getDescriptor())) {
                    // potentially, we could try to see if anything has really changed between
                    // the two classes but the fact that we got an updated class means so far that
                    // we have a new version and should restart.
                    logger.info("Class %s$1 annotated with %s$2.", updatedClass.name,
                            IncrementalVisitor.DISABLE_ANNOTATION_TYPE.getClassName());
                    return INSTANT_RUN_DISABLED;
                }
            }
        }

        InstantRunVerifierStatus fieldChange = verifyFields(originalClass, updatedClass);
        if (fieldChange != COMPATIBLE) {
            return fieldChange;
        }

        return verifyMethods(originalClass, updatedClass, logger);
    }

    @NonNull
    private static InstantRunVerifierStatus verifyFields(@NonNull ClassNode originalClass,
            @NonNull ClassNode updatedClass) {

        //noinspection unchecked
        Diff diff = diffList(originalClass.fields, updatedClass.fields, new Comparator<FieldNode>() {

            @Override
            public boolean areEqual(@Nullable FieldNode first, @Nullable FieldNode second) {
                if ((first == null) && (second == null)) {
                    return true;
                }
                if (first == null || second == null) {
                    return true;
                }
                return first.name.equals(second.name) && first.desc.equals(second.desc)
                        && first.access == second.access && Objects.equal(first.value, second.value);
            }
        });

        if (diff != Diff.NONE) {
            // Detect R$something classes, and report changes in them separately.
            String name = originalClass.name;
            int index = name.lastIndexOf('/');
            if (index != -1 && name.startsWith("R$", index + 1) && (originalClass.access & Opcodes.ACC_PUBLIC) != 0
                    && (originalClass.access & Opcodes.ACC_FINAL) != 0 && originalClass.outerClass == null
                    && originalClass.interfaces.isEmpty() && originalClass.superName.equals("java/lang/Object")
                    && name.length() > 3 && Character.isLowerCase(name.charAt(2))) {
                return R_CLASS_CHANGE;
            }
        }

        switch (diff) {
        case NONE:
            return COMPATIBLE;
        case ADDITION:
            return FIELD_ADDED;
        case REMOVAL:
            return FIELD_REMOVED;
        case CHANGE:
            return FIELD_TYPE_CHANGE;
        default:
            throw new RuntimeException("Unhandled action : " + diff);
        }
    }

    @NonNull
    private static InstantRunVerifierStatus verifyMethods(@NonNull ClassNode originalClass,
            @NonNull ClassNode updatedClass, @NonNull ILogger logger) {

        @SuppressWarnings("unchecked") // ASM API.
        List<MethodNode> nonVisitedMethodsOnUpdatedClass = new ArrayList<>(updatedClass.methods);

        //noinspection unchecked
        for (MethodNode methodNode : (List<MethodNode>) originalClass.methods) {

            MethodNode updatedMethod = findMethod(updatedClass, methodNode.name, methodNode.desc);
            if (updatedMethod == null) {
                // although it's probably ok if a method got deleted since nobody should be calling
                // it anymore BUT the application might be using reflection to get the list of
                // methods and would still see the deleted methods.
                // Even if removing the static initializer should be fine in InstantRun mode, it
                // confuses people so it's safer to just restart.
                return METHOD_DELETED;
            }

            // remove the method from the visited ones on the updated class.
            nonVisitedMethodsOnUpdatedClass.remove(updatedMethod);

            InstantRunVerifierStatus change = methodNode.name.equals(ByteCodeUtils.CLASS_INITIALIZER)
                    ? visitClassInitializer(methodNode, updatedMethod)
                    : verifyMethod(methodNode, updatedMethod, logger);

            if (change != COMPATIBLE) {
                return change;
            }
        }

        if (!nonVisitedMethodsOnUpdatedClass.isEmpty()) {
            return METHOD_ADDED;
        }
        return COMPATIBLE;
    }

    @NonNull
    private static InstantRunVerifierStatus visitClassInitializer(MethodNode originalClassInitializer,
            MethodNode updateClassInitializer) {

        return METHOD_COMPARATOR.areEqual(originalClassInitializer, updateClassInitializer) ? COMPATIBLE
                : STATIC_INITIALIZER_CHANGE;
    }

    @SuppressWarnings("unchecked") // ASM API
    @NonNull
    private static InstantRunVerifierStatus verifyMethod(MethodNode methodNode, MethodNode updatedMethod,
            ILogger logger) {

        // check for annotations changes
        if (diffList(methodNode.visibleAnnotations, updatedMethod.visibleAnnotations,
                new AnnotationNodeComparator()) != Diff.NONE) {
            return METHOD_ANNOTATION_CHANGE;
        }

        // the method exist in both classes, check if the original method was disabled for
        // instantRun or contained calls to blacklisted APIs. If either of these conditions
        // is true, and the method implementation has changed, a restart is needed.
        boolean disabledMethod = false;
        List<AnnotationNode> invisibleAnnotations = methodNode.invisibleAnnotations;
        if (invisibleAnnotations != null) {
            for (AnnotationNode originalMethodAnnotation : invisibleAnnotations) {
                if (originalMethodAnnotation.desc
                        .equals(IncrementalVisitor.DISABLE_ANNOTATION_TYPE.getDescriptor())) {
                    disabledMethod = true;
                }
            }
        }

        boolean usingBlackListedAPIs = InstantRunMethodVerifier.verifyMethod(updatedMethod) != COMPATIBLE;

        // either disabled or using blacklisted APIs, let it through only if the method
        // implementation is unchanged.
        if ((disabledMethod || usingBlackListedAPIs) && !METHOD_COMPARATOR.areEqual(methodNode, updatedMethod)) {

            if (disabledMethod) {
                logger.info("Instant Run disabled for method %s.", updatedMethod.name);
                return INSTANT_RUN_DISABLED;
            } else {
                return REFLECTION_USED;
            }

        }
        return COMPATIBLE;
    }

    @Nullable
    private static MethodNode findMethod(@NonNull ClassNode classNode, @NonNull String name,
            @Nullable String desc) {

        //noinspection unchecked
        for (MethodNode methodNode : (List<MethodNode>) classNode.methods) {

            if (methodNode.name.equals(name)
                    && ((desc == null && methodNode.desc == null) || (methodNode.desc.equals(desc)))) {
                return methodNode;
            }
        }
        return null;
    }

    private interface Comparator<T> {
        boolean areEqual(@Nullable T first, @Nullable T second);
    }

    private static class MethodNodeComparator implements Comparator<MethodNode> {

        @Override
        public boolean areEqual(@Nullable MethodNode first, @Nullable MethodNode second) {
            if (first == null && second == null) {
                return true;
            }
            if (first == null || second == null) {
                return false;
            }
            if (!first.name.equals(second.name) || !first.desc.equals(second.desc)) {
                return false;
            }
            VerifierTextifier firstMethodTextifier = new VerifierTextifier();
            VerifierTextifier secondMethodTextifier = new VerifierTextifier();
            first.accept(new TraceMethodVisitor(firstMethodTextifier));
            second.accept(new TraceMethodVisitor(secondMethodTextifier));

            StringWriter firstText = new StringWriter();
            StringWriter secondText = new StringWriter();
            firstMethodTextifier.print(new PrintWriter(firstText));
            secondMethodTextifier.print(new PrintWriter(secondText));

            return firstText.toString().equals(secondText.toString());
        }
    }

    /**
     * Subclass of {@link Textifier} that will pretty print method bytecodes but will swallow the
     * line numbers notification as it is not pertinent for the InstantRun hot swapping.
     */
    private static class VerifierTextifier extends Textifier {

        protected VerifierTextifier() {
            super(Opcodes.ASM5);
        }

        @Override
        public void visitLineNumber(int i, Label label) {
            // don't care about line numbers
        }
    }

    public static class AnnotationNodeComparator implements Comparator<AnnotationNode> {

        @Override
        public boolean areEqual(@Nullable AnnotationNode first, @Nullable AnnotationNode second) {
            // probably deep compare for values...
            //noinspection unchecked
            if (first == null || second == null) {
                return first == second;
            }

            if (!STRING_COMPARATOR.areEqual(first.desc, second.desc)) {
                return false;
            }

            // Kotlin adds a kotlin.Metadata annotation to each compiled class:
            // https://github.com/JetBrains/kotlin/blob/master/core/runtime.jvm/src/kotlin/Metadata.kt
            // The annotation contains information that cannot be represented in Java's
            // class file format. It includes binary protocol buffers serialized as
            // Java strings -- in particular, the "d1" and "d2" fields.  If you make any
            // change to the class, e.g. adding a method, it can change the contents of
            // the metadata annotation.  We don't try to compare them -- we rely only on
            // overt changes to the classfile (e.g. METHOD_ADDED) for verification.
            //
            // Note that if it's ever necessary, we can implement an isKotlinClass(<class>)
            // method by checking <class>.visibleAnnotations.get(0).desc against this value.
            if (first.desc.equals(KOTLIN_METADATA_ANNOTATION_DESC)) {
                return true;
            }

            List firstEntries = splitToEntries(first.values);
            List secondEntries = splitToEntries(second.values);

            return diffList(firstEntries, secondEntries, OBJECT_COMPARATOR) == Diff.NONE;
        }
    }

    @VisibleForTesting
    @NonNull
    static <T> Diff diffList(@Nullable List<T> one, @Nullable List<T> two, @NonNull Comparator<T> comparator) {

        if (one == null && two == null) {
            return Diff.NONE;
        }
        if (one == null) {
            return Diff.ADDITION;
        }
        if (two == null) {
            return Diff.REMOVAL;
        }
        List<T> copyOfOne = new ArrayList<T>(one);
        List<T> copyOfTwo = new ArrayList<T>(two);

        for (T elementOfTwo : two) {
            T commonElement = getElementOf(copyOfOne, elementOfTwo, comparator);
            if (commonElement != null) {
                copyOfOne.remove(commonElement);
            }
        }

        for (T elementOfOne : one) {
            T commonElement = getElementOf(copyOfTwo, elementOfOne, comparator);
            if (commonElement != null) {
                copyOfTwo.remove(commonElement);
            }
        }
        if ((!copyOfOne.isEmpty()) && (copyOfOne.size() == copyOfTwo.size())) {
            return Diff.CHANGE;
        }
        if (!copyOfOne.isEmpty()) {
            return Diff.REMOVAL;
        }
        return copyOfTwo.isEmpty() ? Diff.NONE : Diff.ADDITION;
    }

    @Nullable
    public static <T> T getElementOf(List<T> list, T element, Comparator<T> comparator) {
        for (T elementOfList : list) {
            if (comparator.areEqual(elementOfList, element)) {
                return elementOfList;
            }
        }
        return null;
    }

    static ClassNode loadClass(ClassBytesProvider classFile) throws IOException {
        byte[] classBytes = classFile.load();
        ClassReader classReader = new ClassReader(classBytes);

        org.objectweb.asm.tree.ClassNode classNode = new org.objectweb.asm.tree.ClassNode();
        classReader.accept(classNode, ClassReader.EXPAND_FRAMES);
        return classNode;
    }

    static class AnnotationEntryAndValue {

        private final String name;

        private final Object value;

        AnnotationEntryAndValue(@NonNull String name, @Nullable Object value) {
            this.name = name;
            this.value = value;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof AnnotationEntryAndValue)) {
                return false;
            }

            AnnotationEntryAndValue other = (AnnotationEntryAndValue) obj;
            if (!STRING_COMPARATOR.areEqual(name, other.name)) {
                return false;
            }

            //Asm incorrectly populates data into AnnotationNode so all array types processing is required:
            //http://forge.ow2.org/tracker/?func=detail&aid=317626&group_id=23&atid=100023
            //https://code.google.com/p/android/issues/detail?id=209051
            Object otherValue = other.value;
            if (value instanceof byte[] && otherValue instanceof byte[]) {
                return Arrays.equals((byte[]) value, (byte[]) otherValue);
            } else if (value instanceof boolean[] && otherValue instanceof boolean[]) {
                return Arrays.equals((boolean[]) value, (boolean[]) otherValue);
            } else if (value instanceof short[] && otherValue instanceof short[]) {
                return Arrays.equals((short[]) value, (short[]) otherValue);
            } else if (value instanceof char[] && otherValue instanceof char[]) {
                return Arrays.equals((char[]) value, (char[]) otherValue);
            } else if (value instanceof int[] && otherValue instanceof int[]) {
                return Arrays.equals((int[]) value, (int[]) otherValue);
            } else if (value instanceof long[] && otherValue instanceof long[]) {
                return Arrays.equals((long[]) value, (long[]) otherValue);
            } else if (value instanceof float[] && otherValue instanceof float[]) {
                return Arrays.equals((float[]) value, (float[]) otherValue);
            } else if (value instanceof double[] && otherValue instanceof double[]) {
                return Arrays.equals((double[]) value, (double[]) otherValue);
            } else if (value instanceof String[] && otherValue instanceof String[]) {
                //Enum entry values are stored in String []
                //https://code.google.com/p/android/issues/detail?id=209047
                return Arrays.equals((String[]) value, (String[]) otherValue);
            }

            if (value instanceof List && otherValue instanceof List) {
                //properly compare arrays of annotations (OBJECT_OR_ANNOTATION_NODE_COMPARATOR)
                List list = (List) value;
                List otherList = (List) otherValue;
                if (list.size() != otherList.size()) {
                    return false;
                }

                Iterator iterator = list.iterator();
                Iterator otherIterator = otherList.iterator();
                while (iterator.hasNext() && otherIterator.hasNext()) {
                    if (!OBJECT_OR_ANNOTATION_NODE_COMPARATOR.areEqual(iterator.next(), otherIterator.next())) {
                        return false;
                    }
                }
                return true;
            }

            return OBJECT_OR_ANNOTATION_NODE_COMPARATOR.areEqual(value, otherValue);
        }

        @Override
        public int hashCode() {
            return name.hashCode();
        }
    }

    @NonNull
    private static List<AnnotationEntryAndValue> splitToEntries(@Nullable List values) {
        if (values == null)
            return Collections.emptyList();
        List<AnnotationEntryAndValue> result = new ArrayList<AnnotationEntryAndValue>();
        for (int i = 0; i < values.size(); i += 2) {
            String name = (String) values.get(i);
            Object value = values.get(i + 1);
            result.add(new AnnotationEntryAndValue(name, value));
        }
        return result;
    }
}