com.android.build.gradle.shrinker.AbstractShrinker Maven / Gradle / Ivy
Show all versions of gradle-core Show documentation
/*
* 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.google.common.base.Preconditions.checkState;
import com.android.annotations.NonNull;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.ide.common.internal.LoggedErrorException;
import com.android.ide.common.internal.WaitableExecutor;
import com.android.utils.FileUtils;
import com.google.common.base.Optional;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Iterables;
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.ClassWriter;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Predicate;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
/**
* Common code for both types of shrinker runs, {@link FullRunShrinker} and
* {@link IncrementalShrinker}.
*/
public abstract class AbstractShrinker {
/**
* Specifies whether the shrinker should assume certain package names are specific to the SDK,
* to trim the graph as early as possible.
*/
private static final boolean IGNORE_PACKAGE_NAME =
Boolean.getBoolean("android.newShrinker.ignorePackageName");
protected final WaitableExecutor mExecutor;
protected final ShrinkerGraph mGraph;
protected final ShrinkerLogger mShrinkerLogger;
protected AbstractShrinker(
ShrinkerGraph graph, WaitableExecutor executor, ShrinkerLogger shrinkerLogger) {
mGraph = graph;
mExecutor = executor;
mShrinkerLogger = shrinkerLogger;
}
/**
* Checks if a given class name starts with a package name that we assume to be an SDK class.
*
* This way we can make the check cheaper in the common case and also filter out a lot of
* unnecessary edges from the graph early on, where we don't yet know which class is which.
*/
static boolean isSdkPackage(@NonNull String className) {
if (IGNORE_PACKAGE_NAME) {
return false;
} else {
return className.startsWith("java/")
|| (className.startsWith("android/")
&& !className.contains("/databinding/")
// Match android/support and android/preview/support, possibly others.
&& !className.contains("/support/"));
}
}
/**
* Tries to determine the output class file, for rewriting the given class file.
*
*
This will return {@link Optional#absent()} if the class is not part of the program to
* shrink (e.g. comes from a platform JAR).
*/
@NonNull
protected Optional chooseOutputFile(
@NonNull T klass,
@NonNull File classFile,
@NonNull Iterable inputs,
@NonNull TransformOutputProvider output) {
String classFilePath = classFile.getAbsolutePath();
for (TransformInput input : inputs) {
Iterable directoriesAndJars =
Iterables.concat(input.getDirectoryInputs(), input.getJarInputs());
for (QualifiedContent directoryOrJar : directoriesAndJars) {
File file = directoryOrJar.getFile();
if (classFilePath.startsWith(file.getAbsolutePath())) {
File outputDir = output.getContentLocation(
FileUtils.getDirectoryNameForJar(file),
directoryOrJar.getContentTypes(),
directoryOrJar.getScopes(),
Format.DIRECTORY);
return Optional.of(new File(outputDir, mGraph.getClassName(klass) + ".class"));
}
}
}
return Optional.absent();
}
/**
* Determines all directories where class files can be found in the given
* {@link TransformInput}.
*/
@NonNull
protected static Collection getAllDirectories(@NonNull TransformInput input) {
return input.getDirectoryInputs().stream()
.map(DirectoryInput::getFile)
.collect(Collectors.toList());
}
/**
* Determines all directories where class files can be found in the given
* {@link TransformInput}.
*/
@NonNull
protected static Collection getAllJars(@NonNull TransformInput input) {
return input.getJarInputs().stream()
.map(JarInput::getFile)
.collect(Collectors.toList());
}
/**
* Increments the counter on the given graph node. If the node just became reachable, keeps on
* walking the graph to find newly reachable nodes.
*
* @param node node to increment
* @param dependencyType type of counter to increment
* @param counterSet set of counters to work on
*/
protected void incrementCounter(
@NonNull T node,
@NonNull DependencyType dependencyType,
@NonNull CounterSet counterSet) {
if (mGraph.incrementAndCheck(node, dependencyType, counterSet)) {
for (Dependency dependency : mGraph.getDependencies(node)) {
incrementCounter(dependency.target, dependency.type, counterSet);
}
}
}
/**
* Finds existing methods or fields (graph nodes) which encountered opcodes refer to. Updates
* the graph with additional edges accordingly.
*/
protected void resolveReferences(
@NonNull Iterable> unresolvedReferences) {
for (final PostProcessingData.UnresolvedReference unresolvedReference : unresolvedReferences) {
mExecutor.execute(new Callable() {
@Override
public Void call() {
T target = unresolvedReference.target;
T source = unresolvedReference.method;
T startClass = mGraph.getOwnerClass(target);
if (unresolvedReference.invokespecial) {
// With invokespecial we disregard the class in target and start walking up
// the type hierarchy, starting from the superclass of the caller.
T sourceClass = mGraph.getOwnerClass(source);
try {
startClass = mGraph.getSuperclass(sourceClass);
checkState(startClass != null);
} catch (ClassLookupException e) {
mShrinkerLogger.invalidClassReference(
mGraph.getClassName(sourceClass),
e.getClassName());
}
}
if (!mGraph.isClassKnown(startClass)) {
mShrinkerLogger.invalidClassReference(
mGraph.getFullMemberName(source),
mGraph.getClassName(startClass));
return null;
}
TypeHierarchyTraverser traverser =
TypeHierarchyTraverser.superclassesAndInterfaces(mGraph, mShrinkerLogger);
for (T currentClass : traverser.preOrderTraversal(startClass)) {
T matchingMethod = mGraph.findMatchingMethod(
currentClass,
target);
if (matchingMethod != null) {
if (isProgramClass(mGraph.getOwnerClass(matchingMethod))) {
mGraph.addDependency(
source,
currentClass,
unresolvedReference.dependencyType);
mGraph.addDependency(
source,
matchingMethod,
unresolvedReference.dependencyType);
}
return null;
}
}
mShrinkerLogger.invalidMemberReference(
mGraph.getFullMemberName(source),
mGraph.getFullMemberName(target));
return null;
}
});
}
}
protected boolean isProgramClass(T klass) {
return !mGraph.isLibraryClass(klass);
}
/**
* Rewrites the given class (read from file) to only include used methods and fields. Returns
* the new class bytecode as {@code byte[]}.
*/
@NonNull
protected static byte[] rewrite(
@NonNull String className,
@NonNull File classFile,
@NonNull Set membersToKeep,
@NonNull Predicate keepInterface) throws IOException {
byte[] bytes;
if (Files.getFileExtension(classFile.getName()).equals("class")) {
bytes = Files.toByteArray(classFile);
} else {
try (JarFile jarFile = new JarFile(classFile)) {
JarEntry jarEntry = jarFile.getJarEntry(className + ".class");
bytes = ByteStreams.toByteArray(jarFile.getInputStream(jarEntry));
}
}
ClassReader classReader = new ClassReader(bytes);
// Don't pass the reader as an argument to the writer. This forces the writer to recompute
// the constant pool, which we want, since it can contain unused entries that end up in the
// dex file.
ClassWriter classWriter = new ClassWriter(0);
ClassVisitor filter = new FilterMembersVisitor(membersToKeep, keepInterface, classWriter);
classReader.accept(filter, 0);
return classWriter.toByteArray();
}
/**
* Walks the entire graph, starting from the roots, and increments counters for reachable nodes.
*/
protected void setCounters(@NonNull final CounterSet counterSet) {
Map roots = mGraph.getRoots(counterSet);
for (final Map.Entry toIncrementEntry : roots.entrySet()) {
mExecutor.execute(new Callable() {
@Override
public Void call() throws Exception {
incrementCounter(
toIncrementEntry.getKey(),
toIncrementEntry.getValue(),
counterSet);
return null;
}
});
}
waitForAllTasks();
}
/**
* Writes updates class files to the outputs.
*/
protected void updateClassFiles(
@NonNull Iterable classesToWrite,
@NonNull Iterable classFilesToDelete,
@NonNull Iterable inputs,
@NonNull TransformOutputProvider output) throws IOException {
for (final T klass : classesToWrite) {
final File sourceFile = mGraph.getSourceFile(klass);
checkState(sourceFile != null, "Program class has no source file.");
final Optional outputFile = chooseOutputFile(klass, sourceFile, inputs, output);
if (!outputFile.isPresent()) {
// The class is from code we don't control.
continue;
}
Files.createParentDirs(outputFile.get());
final Predicate keepInterfacePredicate = input -> {
T iface = mGraph.getClassReference(input);
return !isProgramClass(iface)
|| mGraph.isReachable(iface, CounterSet.SHRINK);
};
mExecutor.execute(() -> {
byte[] newBytes = rewrite(
mGraph.getClassName(klass),
sourceFile,
mGraph.getReachableMembersLocalNames(klass, CounterSet.SHRINK),
keepInterfacePredicate);
Files.write(newBytes, outputFile.get());
return null;
});
}
for (File classFile : classFilesToDelete) {
FileUtils.delete(classFile);
}
waitForAllTasks();
}
protected void waitForAllTasks() {
try {
mExecutor.waitForTasksWithQuickFail(true);
} catch (InterruptedException | LoggedErrorException e) {
throw new RuntimeException(e);
}
}
/**
* Set of counters, for keeping different sets of reachable nodes for different purposes.
*/
public enum CounterSet {
/** Counters for removing dead code. */
SHRINK,
/** Counters for finding classes that have to be in the main classes.dex file. */
LEGACY_MULTIDEX
}
public static void logTime(String section, Stopwatch stopwatch) {
if (System.getProperty("android.newShrinker.profile") != null) {
System.out.println(section + ": " + stopwatch);
stopwatch.reset();
stopwatch.start();
}
}
}