All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.build.gradle.shrinker.FullRunShrinker Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2015 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.shrinker;

import static com.android.utils.FileUtils.getAllFiles;

import com.android.annotations.NonNull;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.ide.common.internal.WaitableExecutor;
import com.google.common.base.Stopwatch;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.TreeTraverser;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * Code shrinker. It analyzes the input classes and the SDK jar and outputs minified classes. Uses
 * the given implementation of {@link ShrinkerGraph} to keep state and persist it for later
 * incremental runs.
 */
public class FullRunShrinker extends AbstractShrinker {

    /** Suffix for "fake methods", inserted to forward dependencies between unrelated classes. */
    static final String SHRINKER_FAKE_MARKER = "$shrinker_fake";

    private final Set mPlatformJars;

    public FullRunShrinker(
            WaitableExecutor executor,
            ShrinkerGraph graph,
            Set platformJars,
            ShrinkerLogger shrinkerLogger) {
        super(graph, executor, shrinkerLogger);
        mPlatformJars = platformJars;
    }

    /**
     * Performs the full shrinking run. This clears previous incremental state, creates a new
     * {@link ShrinkerGraph} and fills it with data read from the platform JARs as well as input
     * classes. Then we find "entry points" that match {@code -keep} rules from the config file,
     * and walk the graph, setting the counters and finding reachable classes and members. In the
     * last step we rewrite all reachable class files to only contain kept class members and put
     * them in the matching output directories.
     */
    public void run(
            @NonNull Collection inputs,
            @NonNull Collection referencedClasses,
            @NonNull TransformOutputProvider output,
            @NonNull ImmutableMap keepRules,
            boolean saveState) throws IOException {
        output.deleteAll();

        buildGraph(inputs, referencedClasses);

        Stopwatch stopwatch = Stopwatch.createStarted();
        setCounters(keepRules);
        logTime("Set counters", stopwatch);
        writeOutput(inputs, output);
        logTime("Write output", stopwatch);

        if (saveState) {
            mGraph.saveState();
            logTime("Saving state", stopwatch);
        }
    }

    /**
     * Populates the graph with all nodes (classes, members) and edges (dependencies, references),
     * so that it's ready to be traversed in search of reachable ndoes.
     */
    private void buildGraph(
            @NonNull Iterable programInputs,
            @NonNull Iterable libraryInputs) throws IOException {
        Stopwatch stopwatch = Stopwatch.createStarted();
        final PostProcessingData postProcessingData = new PostProcessingData<>();

        readPlatformJars();

        for (TransformInput input : libraryInputs) {
            for (File directory : getAllDirectories(input)) {
                for (final File classFile : getClassFiles(directory)) {
                    mExecutor.execute(() -> {
                        processLibraryClass(Files.toByteArray(classFile));
                        return null;
                    });
                }
            }

            for (final File jarFile : getAllJars(input)) {
                processJarFile(jarFile, this::processLibraryClass);
            }
        }

        for (TransformInput input : programInputs) {
            for (File directory : getAllDirectories(input)) {
                for (final File classFile : getClassFiles(directory)) {
                    mExecutor.execute(new Callable() {
                        @Override
                        public Void call() throws Exception {
                            processProgramClassFile(
                                    Files.toByteArray(classFile),
                                    classFile,
                                    postProcessingData);
                            return null;
                        }
                    });
                }
            }

            for (final File jarFile : getAllJars(input)) {
                processJarFile(jarFile, new ByteCodeConsumer() {
                    @Override
                    public void process(byte[] bytes) throws IOException {
                        processProgramClassFile(
                                bytes,
                                jarFile,
                                postProcessingData);
                    }
                });
            }
        }
        waitForAllTasks();
        logTime("Read input", stopwatch);

        handleOverrides(postProcessingData.getVirtualMethods());
        handleMultipleInheritance(postProcessingData.getMultipleInheritance());
        handleInterfaceInheritance(postProcessingData.getInterfaceInheritance());
        resolveReferences(postProcessingData.getUnresolvedReferences());
        waitForAllTasks();
        logTime("Finish graph", stopwatch);

        mGraph.checkDependencies(mShrinkerLogger);
    }

    private void handleInterfaceInheritance(@NonNull Set interfaceInheritance) {
        for (final T klass : interfaceInheritance) {
            mExecutor.execute(new Callable() {
                @Override
                public Void call() throws Exception {
                    TreeTraverser interfaceTraverser =
                            TypeHierarchyTraverser.interfaces(mGraph, mShrinkerLogger);

                    if ((mGraph.getModifiers(klass) & Opcodes.ACC_INTERFACE) != 0) {

                        // The "children" name is unfortunate: in the type hierarchy tree traverser,
                        // these are the interfaces that klass (which is an interface itself)
                        // extends (directly).
                        Iterable superinterfaces = interfaceTraverser.children(klass);

                        for (T superinterface : superinterfaces) {
                            if (!mGraph.isLibraryClass(superinterface)) {
                                // Add the arrow going "down", from the superinterface to this one.
                                mGraph.addDependency(
                                        superinterface,
                                        klass,
                                        DependencyType.SUPERINTERFACE_KEPT);
                            } else {
                                // The superinterface is part of the SDK, so it's always kept. As
                                // long as there's any class that implements this interface, it
                                // needs to be kept.
                                mGraph.incrementAndCheck(
                                        klass,
                                        DependencyType.SUPERINTERFACE_KEPT,
                                        CounterSet.SHRINK);
                            }
                        }
                    }

                    for (T iface : interfaceTraverser.preOrderTraversal(klass)) {
                        if (!mGraph.isLibraryClass(iface)) {
                            mGraph.addDependency(
                                    klass,
                                    iface,
                                    DependencyType.INTERFACE_IMPLEMENTED);
                        }
                    }

                    return null;
                }
            });
        }
    }

    @NonNull
    private static FluentIterable getClassFiles(@NonNull File dir) {
        return getAllFiles(dir).filter(f -> Files.getFileExtension(f.getName()).equals("class"));
    }

    /**
     * Updates the graph to handle a case when a class inherits an interface method implementation
     * from a super class which does not implement the given interface.
     *
     * 

We handle it by inserting fake nodes into the graph, equivalent to just calling super() * to invoke the inherited implementation. This way an "invokeinterface" opcode can cause the * fake method to be kept, which in turn causes the real method to be kept, even though on the * surface it has nothing to do with the interface. */ private void handleMultipleInheritance(@NonNull Set multipleInheritance) { for (final T klass : multipleInheritance) { mExecutor.execute(new Callable() { Set methods = mGraph.getMethods(klass); @Override public Void call() throws Exception { if (!isProgramClass(mGraph.getSuperclass(klass))) { // All the superclass methods are kept anyway. return null; } Iterable interfaces = TypeHierarchyTraverser .interfaces(mGraph, mShrinkerLogger) .preOrderTraversal(klass); for (T iface : interfaces) { for (T method : mGraph.getMethods(iface)) { handleMethod(method); } } return null; } private void handleMethod(T method) { if (this.methods.contains(method)) { // We implement this interface method directly in the class, which is the // common case. Nothing left to do. return; } // Otherwise, look in the superclasses for the implementation. FluentIterable superclasses = TypeHierarchyTraverser .superclasses(mGraph, mShrinkerLogger) .preOrderTraversal(klass); for (T current : superclasses) { if (!isProgramClass(current)) { // We will not remove the method anyway. return; } T matchingMethod = mGraph.findMatchingMethod(current, method); if (matchingMethod != null) { String name = mGraph.getMemberName(method) + SHRINKER_FAKE_MARKER; String desc = mGraph.getMemberDescriptor(method); T fakeMethod = mGraph.addMember( klass, name, desc, mGraph.getModifiers(method)); // Simulate a super call. mGraph.addDependency(fakeMethod, matchingMethod, DependencyType.REQUIRED_CLASS_STRUCTURE); if (!isProgramClass(mGraph.getOwnerClass(method))) { mGraph.addDependency(klass, fakeMethod, DependencyType.REQUIRED_CLASS_STRUCTURE); } else { mGraph.addDependency(klass, fakeMethod, DependencyType.CLASS_IS_KEPT); mGraph.addDependency(method, fakeMethod, DependencyType.IF_CLASS_KEPT); } return; } } } }); } } /** * Updates the graph to add edges which model how overridden methods should be handled. * *

A method overriding another one (from a class or interface), is kept if it's invoked * directly (naturally) or if the class is kept for whatever reason and the overridden method * is also invoked - we don't know if the call site for the overridden method actually operates * on objects of the subclass. */ private void handleOverrides(@NonNull Set virtualMethods) { for (final T method : virtualMethods) { mExecutor.execute(new Callable() { @Override public Void call() throws Exception { if (isJavaLangObjectMethod( mGraph.getMemberName(method), mGraph.getMemberDescriptor(method))) { // If we override an SDK method, it just has to be there at runtime // (if the class itself is kept). mGraph.addDependency( mGraph.getOwnerClass(method), method, DependencyType.REQUIRED_CLASS_STRUCTURE); return null; } FluentIterable superTypes = TypeHierarchyTraverser .superclassesAndInterfaces(mGraph, mShrinkerLogger) .preOrderTraversal(mGraph.getOwnerClass(method)); for (T klass : superTypes) { if (mGraph.getClassName(klass).equals("java/lang/Object")) { continue; } T superMethod = mGraph.findMatchingMethod(klass, method); if (superMethod != null && !superMethod.equals(method)) { if (!isProgramClass(mGraph.getOwnerClass(superMethod))) { // If we override an SDK method, it just has to be there at runtime // (if the class itself is kept). mGraph.addDependency( mGraph.getOwnerClass(method), method, DependencyType.REQUIRED_CLASS_STRUCTURE); return null; } else { // If we override a program method, there's a chance this method is // never called and we will get rid of it. Set up the dependencies // appropriately. mGraph.addDependency( mGraph.getOwnerClass(method), method, DependencyType.CLASS_IS_KEPT); mGraph.addDependency( superMethod, method, DependencyType.IF_CLASS_KEPT); } } } return null; } }); } } private static boolean isJavaLangObjectMethod( @NonNull String name, @NonNull String descriptor) { return (name.equals("hashCode") && descriptor.equals("()I")) || (name.equals("equals") && descriptor.equals("(Ljava/lang/Object;)Z")) || (name.equals("toString") && descriptor.equals("()Ljava/lang/String;")); } /** * Updates the graph with nodes from a library (read-only) class. There's no point creating * edges, since library classes cannot references program classes and we don't shrink library * code. */ private void processLibraryClass(@NonNull byte[] source) throws IOException { ClassReader classReader = new ClassReader(source); classReader.accept( new ClassStructureVisitor<>(mGraph, null, null), ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); } /** * Updates the graph with nodes and edges based on the given class file. */ private void processProgramClassFile( byte[] bytes, @NonNull File classFile, @NonNull final PostProcessingData postProcessingData) throws IOException { ClassNode classNode = new ClassNode(Opcodes.ASM5); ClassVisitor depsFinder = new DependencyFinderVisitor(mGraph, classNode) { @Override protected void handleDependency(T source, T target, DependencyType type) { mGraph.addDependency(source, target, type); } @Override protected void handleMultipleInheritance(T klass) { postProcessingData.getMultipleInheritance().add(klass); } @Override protected void handleVirtualMethod(T method) { postProcessingData.getVirtualMethods().add(method); } @Override protected void handleInterfaceInheritance(T klass) { postProcessingData.getInterfaceInheritance().add(klass); } @Override protected void handleUnresolvedReference(PostProcessingData.UnresolvedReference reference) { postProcessingData.getUnresolvedReferences().add(reference); } }; ClassVisitor structureVisitor = new ClassStructureVisitor<>(mGraph, classFile, depsFinder); ClassReader classReader = new ClassReader(bytes); classReader.accept(structureVisitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); } private interface ByteCodeConsumer { void process(byte[] bytes) throws IOException; } private void readPlatformJars() throws IOException { for (File platformJar : mPlatformJars) { processJarFile(platformJar, this::processLibraryClass); } } private void processJarFile(File platformJar, final ByteCodeConsumer consumer) throws IOException { try (JarFile jarFile = new JarFile(platformJar)) { for (Enumeration entries = jarFile.entries(); entries.hasMoreElements(); ) { JarEntry entry = entries.nextElement(); if (!entry.getName().endsWith(".class")) { continue; } try (InputStream inputStream = jarFile.getInputStream(entry)) { final byte[] bytes = ByteStreams.toByteArray(inputStream); mExecutor.execute(new Callable() { @Override public Void call() throws Exception { consumer.process(bytes); return null; } }); } } } } /** * Sets the roots (i.e. entry points) of the graph and marks all nodes reachable from them. */ private void setCounters(@NonNull ImmutableMap allKeepRules) { final CounterSet counterSet = CounterSet.SHRINK; final KeepRules keepRules = allKeepRules.get(counterSet); for (final T klass : mGraph.getAllProgramClasses()) { mExecutor.execute(new Callable() { @Override public Void call() throws Exception { mGraph.addRoots(keepRules.getSymbolsToKeep(klass, mGraph), counterSet); return null; } }); } waitForAllTasks(); setCounters(counterSet); } private void writeOutput( @NonNull Collection inputs, @NonNull TransformOutputProvider output) throws IOException { updateClassFiles( mGraph.getReachableClasses(CounterSet.SHRINK), Collections.emptyList(), inputs, output); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy