blob: 64294bd105f259145a1a7bdf274d7e24c6f1232c [file] [log] [blame]
// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.shaking;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.KeyedDexItem;
import com.android.tools.r8.graph.PresortedComparable;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.android.tools.r8.utils.InternalOptions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class TreePruner {
private DexApplication application;
private final AppInfoWithLiveness appInfo;
private final InternalOptions options;
private UsagePrinter usagePrinter;
public TreePruner(
DexApplication application, AppInfoWithLiveness appInfo, InternalOptions options) {
this.application = application;
this.appInfo = appInfo;
this.options = options;
this.usagePrinter = options.proguardConfiguration.isPrintUsage()
? new UsagePrinter() : UsagePrinter.DONT_PRINT;
}
public DexApplication run() throws IOException {
application.timing.begin("Pruning application...");
if (options.debugKeepRules && !options.skipMinification) {
System.out.println(
"NOTE: Debugging keep rules on a minified build might yield broken builds, as\n"
+ " minification also depends on the used keep rules. We recommend using\n"
+ " --skip-minification.");
}
DexApplication result;
try {
result = removeUnused(application).appendDeadCode(usagePrinter.toByteArray()).build();
} finally {
application.timing.end();
}
return result;
}
private DexApplication.Builder removeUnused(DexApplication application) {
return new DexApplication.Builder(application)
.replaceProgramClasses(getNewProgramClasses(application.classes()));
}
private List<DexProgramClass> getNewProgramClasses(List<DexProgramClass> classes) {
List<DexProgramClass> newClasses = new ArrayList<>();
for (DexProgramClass clazz : classes) {
if (!appInfo.liveTypes.contains(clazz.type)) {
// The class is completely unused and we can remove it.
if (Log.ENABLED) {
Log.debug(getClass(), "Removing class: " + clazz);
}
usagePrinter.printUnusedClass(clazz);
} else {
newClasses.add(clazz);
if (!appInfo.instantiatedTypes.contains(clazz.type) &&
(!options.debugKeepRules || !hasDefaultConstructor(clazz))) {
// The class is only needed as a type but never instantiated. Make it abstract to reflect
// this.
if (clazz.accessFlags.isFinal()) {
// We cannot mark this class abstract, as it is final (not supported on Android).
// However, this might extend an abstract class and we might have removed the
// corresponding methods in this class. This might happen if we only keep this
// class around for its constants.
// For now, we remove the final flag to still be able to mark it abstract.
clazz.accessFlags.unsetFinal();
}
clazz.accessFlags.setAbstract();
}
// The class is used and must be kept. Remove the unused fields and methods from
// the class.
usagePrinter.visiting(clazz);
clazz.setDirectMethods(reachableMethods(clazz.directMethods(), clazz));
clazz.setVirtualMethods(reachableMethods(clazz.virtualMethods(), clazz));
clazz.setInstanceFields(reachableFields(clazz.instanceFields()));
clazz.setStaticFields(reachableFields(clazz.staticFields()));
usagePrinter.visited();
}
}
return newClasses;
}
private boolean hasDefaultConstructor(DexProgramClass clazz) {
for (DexEncodedMethod method : clazz.directMethods()) {
if (isDefaultConstructor(method)) {
return true;
}
}
return false;
}
private <S extends PresortedComparable<S>, T extends KeyedDexItem<S>> int firstUnreachableIndex(
T[] items, Set<S> live) {
for (int i = 0; i < items.length; i++) {
if (!live.contains(items[i].getKey())) {
return i;
}
}
return -1;
}
private boolean isDefaultConstructor(DexEncodedMethod method) {
return method.isInstanceInitializer()
&& method.method.proto.parameters.isEmpty();
}
private DexEncodedMethod[] reachableMethods(DexEncodedMethod[] methods, DexClass clazz) {
int firstUnreachable = firstUnreachableIndex(methods, appInfo.liveMethods);
// Return the original array if all methods are used.
if (firstUnreachable == -1) {
return methods;
}
ArrayList<DexEncodedMethod> reachableMethods = new ArrayList<>(methods.length);
for (int i = 0; i < firstUnreachable; i++) {
reachableMethods.add(methods[i]);
}
for (int i = firstUnreachable; i < methods.length; i++) {
DexEncodedMethod method = methods[i];
if (appInfo.liveMethods.contains(methods[i].getKey())) {
reachableMethods.add(method);
} else if (options.debugKeepRules && isDefaultConstructor(method)) {
// Keep the method but rewrite its body, if it has one.
reachableMethods.add(methods[i].accessFlags.isAbstract()
? method
: method.toMethodThatLogsError(application.dexItemFactory));
} else if (appInfo.targetedMethods.contains(method.getKey())) {
if (Log.ENABLED) {
Log.debug(getClass(), "Making method %s abstract.", method.method);
}
// Final classes cannot be abstract, so we have to keep the method in that case.
// Also some other kinds of methods cannot be abstract, so keep them around.
boolean allowAbstract = clazz.accessFlags.isAbstract()
&& !method.accessFlags.isFinal()
&& !method.accessFlags.isNative()
&& !method.accessFlags.isStrict()
&& !method.accessFlags.isSynchronized();
// By construction, private and static methods cannot be reachable but non-live.
assert !method.accessFlags.isPrivate() && !method.accessFlags.isStatic();
reachableMethods.add(allowAbstract
? method.toAbstractMethod()
: method.toEmptyThrowingMethod());
} else {
if (Log.ENABLED) {
Log.debug(getClass(), "Removing method %s.", method.method);
}
usagePrinter.printUnusedMethod(method);
}
}
return reachableMethods.toArray(new DexEncodedMethod[reachableMethods.size()]);
}
private DexEncodedField[] reachableFields(DexEncodedField[] fields) {
int firstUnreachable = firstUnreachableIndex(fields, appInfo.liveFields);
// Return the original array if all fields are used.
if (firstUnreachable == -1) {
return fields;
}
if (Log.ENABLED) {
Log.debug(getClass(), "Removing field: " + fields[firstUnreachable]);
}
usagePrinter.printUnusedField(fields[firstUnreachable]);
ArrayList<DexEncodedField> reachableFields = new ArrayList<>(fields.length);
for (int i = 0; i < firstUnreachable; i++) {
reachableFields.add(fields[i]);
}
for (int i = firstUnreachable + 1; i < fields.length; i++) {
DexEncodedField field = fields[i];
if (appInfo.liveFields.contains(field.getKey())) {
reachableFields.add(field);
} else {
if (Log.ENABLED) {
Log.debug(getClass(), "Removing field: " + field);
}
usagePrinter.printUnusedField(field);
}
}
return reachableFields.toArray(new DexEncodedField[reachableFields.size()]);
}
}