blob: 42f9174072f01b5b4be37080602740e8c9073c80 [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 static com.android.tools.r8.naming.IdentifierNameStringUtils.identifyIdentiferNameString;
import static com.android.tools.r8.naming.IdentifierNameStringUtils.isReflectionMethod;
import static com.android.tools.r8.shaking.ProguardConfigurationUtils.buildIdentifierNameStringRule;
import com.android.tools.r8.Diagnostic;
import com.android.tools.r8.dex.IndexedItemCollection;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppInfo.ResolutionResult;
import com.android.tools.r8.graph.AppInfoWithSubtyping;
import com.android.tools.r8.graph.Descriptor;
import com.android.tools.r8.graph.DexAnnotation;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexCallSite;
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.DexField;
import com.android.tools.r8.graph.DexItem;
import com.android.tools.r8.graph.DexItemBasedString;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexMethodHandle;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexTypeList;
import com.android.tools.r8.graph.DirectMappedDexApplication;
import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.graph.KeyedDexItem;
import com.android.tools.r8.graph.PresortedComparable;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.Invoke.Type;
import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.shaking.RootSetBuilder.ConsequentRootSet;
import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
import com.android.tools.r8.shaking.protolite.ProtoLiteExtension;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
import it.unimi.dsi.fastutil.objects.Reference2IntMap;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Approximates the runtime dependencies for the given set of roots.
* <p>
* <p>The implementation filters the static call-graph with liveness information on classes to
* remove virtual methods that are reachable by their static type but are unreachable at runtime as
* they are not visible from any instance.
* <p>
* <p>As result of the analysis, an instance of {@link AppInfoWithLiveness} is returned. See the
* field descriptions for details.
*/
public class Enqueuer {
private final boolean forceProguardCompatibility;
private boolean tracingMainDex = false;
private final AppInfoWithSubtyping appInfo;
private final InternalOptions options;
private RootSet rootSet;
private final Map<DexType, Set<DexMethod>> virtualInvokes = Maps.newIdentityHashMap();
private final Map<DexType, Set<DexMethod>> interfaceInvokes = Maps.newIdentityHashMap();
private final Map<DexType, Set<TargetWithContext<DexMethod>>> superInvokes =
Maps.newIdentityHashMap();
private final Map<DexType, Set<DexMethod>> directInvokes = Maps.newIdentityHashMap();
private final Map<DexType, Set<DexMethod>> staticInvokes = Maps.newIdentityHashMap();
private final Map<DexType, Set<TargetWithContext<DexField>>> instanceFieldsWritten =
Maps.newIdentityHashMap();
private final Map<DexType, Set<TargetWithContext<DexField>>> instanceFieldsRead =
Maps.newIdentityHashMap();
private final Map<DexType, Set<TargetWithContext<DexField>>> staticFieldsRead =
Maps.newIdentityHashMap();
private final Map<DexType, Set<TargetWithContext<DexField>>> staticFieldsWritten =
Maps.newIdentityHashMap();
private final ProtoLiteExtension protoLiteExtension;
private final Set<DexField> protoLiteFields = Sets.newIdentityHashSet();
private final Set<DexItem> identifierNameStrings = Sets.newIdentityHashSet();
/**
* This map keeps a view of all virtual methods that are reachable from virtual invokes. A method
* is reachable even if no live subtypes exist, so this is not sufficient for inclusion in the
* live set.
*/
private final Map<DexType, SetWithReason<DexEncodedMethod>> reachableVirtualMethods = Maps
.newIdentityHashMap();
/**
* Tracks the dependency between a method and the super-method it calls, if any. Used to make
* super methods become live when they become reachable from a live sub-method.
*/
private final Map<DexEncodedMethod, Set<DexEncodedMethod>> superInvokeDependencies = Maps
.newIdentityHashMap();
/**
* Set of instance fields that can be reached by read/write operations.
*/
private final Map<DexType, SetWithReason<DexEncodedField>> reachableInstanceFields = Maps
.newIdentityHashMap();
/**
* Set of types that are mentioned in the program. We at least need an empty abstract class item
* for these.
*/
private final Set<DexType> liveTypes = Sets.newIdentityHashSet();
/**
* Set of types that are actually instantiated. These cannot be abstract.
*/
private final SetWithReason<DexType> instantiatedTypes = new SetWithReason<>();
/**
* Set of methods that are the immediate target of an invoke. They might not actually be live but
* are required so that invokes can find the method. If a method is only a target but not live,
* its implementation may be removed and it may be marked abstract.
*/
private final SetWithReason<DexEncodedMethod> targetedMethods = new SetWithReason<>();
/**
* Set of methods that belong to live classes and can be reached by invokes. These need to be
* kept.
*/
private final SetWithReason<DexEncodedMethod> liveMethods = new SetWithReason<>();
/**
* Set of fields that belong to live classes and can be reached by invokes. These need to be
* kept.
*/
private final SetWithReason<DexEncodedField> liveFields = new SetWithReason<>();
/**
* Set of interface types for which a lambda expression can be reached. These never have a single
* interface implementation.
*/
private final SetWithReason<DexType> instantiatedLambdas = new SetWithReason<>();
/**
* A queue of items that need processing. Different items trigger different actions:
*/
private final Queue<Action> workList = Queues.newArrayDeque();
/**
* A queue of items that have been added to try to keep Proguard compatibility.
*/
private final Queue<Action> proguardCompatibilityWorkList = Queues.newArrayDeque();
/**
* A set of methods that need code inspection for Proguard compatibility rules.
*/
private final Set<DexEncodedMethod> pendingProguardReflectiveCompatibility =
Sets.newLinkedHashSet();
/**
* A cache for DexMethod that have been marked reachable.
*/
private final Set<DexMethod> virtualTargetsMarkedAsReachable = Sets.newIdentityHashSet();
/**
* A set of dexitems we have reported missing to dedupe warnings.
*/
private final Set<DexItem> reportedMissing = Sets.newIdentityHashSet();
/**
* A set of items that we are keeping due to keep rules. This may differ from the rootSet due to
* dependent keep rules.
*/
private final Set<DexItem> pinnedItems = Sets.newIdentityHashSet();
/**
* A map from classes to annotations that need to be processed should the classes ever become
* live.
*/
private final Map<DexType, Set<DexAnnotation>> deferredAnnotations = new IdentityHashMap<>();
/**
* Set of keep rules generated for Proguard compatibility in Proguard compatibility mode.
*/
private final ProguardConfiguration.Builder compatibility;
public Enqueuer(AppInfoWithSubtyping appInfo, InternalOptions options,
boolean forceProguardCompatibility) {
this(appInfo, options, forceProguardCompatibility, null, null);
}
public Enqueuer(AppInfoWithSubtyping appInfo, InternalOptions options,
boolean forceProguardCompatibility,
ProguardConfiguration.Builder compatibility, ProtoLiteExtension protoLiteExtension) {
this.appInfo = appInfo;
this.compatibility = compatibility;
this.options = options;
this.protoLiteExtension = protoLiteExtension;
this.forceProguardCompatibility = forceProguardCompatibility;
}
private void enqueueRootItems(Map<DexItem, ProguardKeepRule> items) {
items.entrySet().forEach(this::enqueueRootItem);
pinnedItems.addAll(items.keySet());
}
private void enqueueRootItem(Map.Entry<DexItem, ProguardKeepRule> root) {
DexItem item = root.getKey();
KeepReason reason = KeepReason.dueToKeepRule(root.getValue());
if (item instanceof DexClass) {
DexClass clazz = (DexClass) item;
workList.add(Action.markInstantiated(clazz, reason));
if (forceProguardCompatibility && clazz.hasDefaultInitializer()) {
ProguardKeepRule rule = ProguardConfigurationUtils.buildDefaultInitializerKeepRule(clazz);
proguardCompatibilityWorkList.add(Action.markMethodLive(
clazz.getDefaultInitializer(), KeepReason.dueToProguardCompatibilityKeepRule(rule)));
}
} else if (item instanceof DexEncodedField) {
workList.add(Action.markFieldKept((DexEncodedField) item, reason));
} else if (item instanceof DexEncodedMethod) {
workList.add(Action.markMethodKept((DexEncodedMethod) item, reason));
} else {
throw new IllegalArgumentException(item.toString());
}
}
//
// Things to do with registering events. This is essentially the interface for byte-code
// traversals.
//
private <S extends DexItem, T extends Descriptor<S, T>> boolean registerItemWithTarget(
Map<DexType, Set<T>> seen, T item) {
DexType holder = item.getHolder();
if (holder.isArrayType()) {
holder = holder.toBaseType(appInfo.dexItemFactory);
}
if (!holder.isClassType()) {
return false;
}
markTypeAsLive(holder);
return seen.computeIfAbsent(item.getHolder(), (ignore) -> Sets.newIdentityHashSet()).add(item);
}
private <S extends DexItem, T extends Descriptor<S, T>> boolean registerItemWithTargetAndContext(
Map<DexType, Set<TargetWithContext<T>>> seen, T item, DexEncodedMethod context) {
DexType holder = item.getHolder();
if (holder.isArrayType()) {
holder = holder.toBaseType(appInfo.dexItemFactory);
}
if (!holder.isClassType()) {
return false;
}
markTypeAsLive(holder);
return seen.computeIfAbsent(item.getHolder(), (ignore) -> new HashSet<>())
.add(new TargetWithContext<>(item, context));
}
private class UseRegistry extends com.android.tools.r8.graph.UseRegistry {
private final DexEncodedMethod currentMethod;
private UseRegistry(DexEncodedMethod currentMethod) {
this.currentMethod = currentMethod;
}
@Override
public boolean registerInvokeVirtual(DexMethod method) {
if (appInfo.dexItemFactory.classMethods.isReflectiveMemberLookup(method)) {
if (forceProguardCompatibility) {
// TODO(b/76181966): whether or not add this rule in normal mode.
if (identifierNameStrings.add(method) && compatibility != null) {
compatibility.addRule(buildIdentifierNameStringRule(method));
}
pendingProguardReflectiveCompatibility.add(currentMethod);
}
}
if (!registerItemWithTarget(virtualInvokes, method)) {
return false;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register invokeVirtual `%s`.", method);
}
workList.add(Action.markReachableVirtual(method, KeepReason.invokedFrom(currentMethod)));
return true;
}
@Override
public boolean registerInvokeDirect(DexMethod method) {
if (!registerItemWithTarget(directInvokes, method)) {
return false;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register invokeDirect `%s`.", method);
}
handleInvokeOfDirectTarget(method, KeepReason.invokedFrom(currentMethod));
return true;
}
@Override
public boolean registerInvokeStatic(DexMethod method) {
if (method == appInfo.dexItemFactory.classMethods.forName
|| appInfo.dexItemFactory.atomicFieldUpdaterMethods.isFieldUpdater(method)) {
if (forceProguardCompatibility) {
// TODO(b/76181966): whether or not add this rule in normal mode.
if (identifierNameStrings.add(method) && compatibility != null) {
compatibility.addRule(buildIdentifierNameStringRule(method));
}
pendingProguardReflectiveCompatibility.add(currentMethod);
}
}
if (!registerItemWithTarget(staticInvokes, method)) {
return false;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register invokeStatic `%s`.", method);
}
handleInvokeOfStaticTarget(method, KeepReason.invokedFrom(currentMethod));
return true;
}
@Override
public boolean registerInvokeInterface(DexMethod method) {
if (!registerItemWithTarget(interfaceInvokes, method)) {
return false;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register invokeInterface `%s`.", method);
}
workList.add(Action.markReachableInterface(method, KeepReason.invokedFrom(currentMethod)));
return true;
}
@Override
public boolean registerInvokeSuper(DexMethod method) {
// We have to revisit super invokes based on the context they are found in. The same
// method descriptor will hit different targets, depending on the context it is used in.
DexMethod actualTarget = getInvokeSuperTarget(method, currentMethod);
if (!registerItemWithTargetAndContext(superInvokes, method, currentMethod)) {
return false;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register invokeSuper `%s`.", actualTarget);
}
workList.add(Action.markReachableSuper(method, currentMethod));
return true;
}
@Override
public boolean registerInstanceFieldWrite(DexField field) {
if (!registerItemWithTargetAndContext(instanceFieldsWritten, field, currentMethod)) {
return false;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register Iput `%s`.", field);
}
// TODO(herhut): We have to add this, but DCR should eliminate dead writes.
workList.add(Action.markReachableField(field, KeepReason.fieldReferencedIn(currentMethod)));
return true;
}
@Override
public boolean registerInstanceFieldRead(DexField field) {
if (!registerItemWithTargetAndContext(instanceFieldsRead, field, currentMethod)) {
return false;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register Iget `%s`.", field);
}
workList.add(Action.markReachableField(field, KeepReason.fieldReferencedIn(currentMethod)));
return true;
}
@Override
public boolean registerNewInstance(DexType type) {
markInstantiated(type, currentMethod);
return true;
}
@Override
public boolean registerStaticFieldRead(DexField field) {
if (!registerItemWithTargetAndContext(staticFieldsRead, field, currentMethod)) {
return false;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register Sget `%s`.", field);
}
markStaticFieldAsLive(field, KeepReason.fieldReferencedIn(currentMethod));
return true;
}
@Override
public boolean registerStaticFieldWrite(DexField field) {
if (!registerItemWithTargetAndContext(staticFieldsWritten, field, currentMethod)) {
return false;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register Sput `%s`.", field);
}
// TODO(herhut): We have to add this, but DCR should eliminate dead writes.
markStaticFieldAsLive(field, KeepReason.fieldReferencedIn(currentMethod));
return true;
}
@Override
public boolean registerConstClass(DexType type) {
return registerConstClassOrCheckCast(type);
}
@Override
public boolean registerCheckCast(DexType type) {
return registerConstClassOrCheckCast(type);
}
@Override
public boolean registerTypeReference(DexType type) {
DexType baseType = type.toBaseType(appInfo.dexItemFactory);
if (baseType.isClassType()) {
markTypeAsLive(baseType);
return true;
}
return false;
}
@Override
public void registerCallSite(DexCallSite callSite) {
super.registerCallSite(callSite);
for (DexEncodedMethod method :
appInfo.lookupLambdaImplementedMethods(callSite, options.reporter)) {
markLambdaInstantiated(method.method.holder, currentMethod);
}
}
private boolean registerConstClassOrCheckCast(DexType type) {
if (forceProguardCompatibility) {
DexType baseType = type.toBaseType(appInfo.dexItemFactory);
if (baseType.isClassType()) {
DexClass baseClass = appInfo.definitionFor(baseType);
if (baseClass != null && baseClass.isProgramClass()
&& baseClass.hasDefaultInitializer()) {
markClassAsInstantiatedWithCompatRule(baseClass);
} else {
// This also handles reporting of missing classes.
markTypeAsLive(baseType);
}
return true;
}
return false;
} else {
return registerTypeReference(type);
}
}
}
private DexMethod getInvokeSuperTarget(DexMethod method, DexEncodedMethod currentMethod) {
DexClass holderClass = appInfo.definitionFor(currentMethod.method.getHolder());
if (holderClass == null || holderClass.superType == null) {
// We do not know better.
return method;
}
// Return the invoked method on the supertype.
return appInfo.dexItemFactory.createMethod(holderClass.superType, method.proto, method.name);
}
//
// Actual actions performed.
//
private void markTypeAsLive(DexType type) {
assert type.isClassType();
if (liveTypes.add(type)) {
if (Log.ENABLED) {
Log.verbose(getClass(), "Type `%s` has become live.", type);
}
DexClass holder = appInfo.definitionFor(type);
if (holder == null) {
reportMissingClass(type);
return;
}
for (DexType iface : holder.interfaces.values) {
markTypeAsLive(iface);
}
if (holder.superType != null) {
markTypeAsLive(holder.superType);
if (holder.isLibraryClass()) {
// Library classes may only extend other implement library classes.
ensureFromLibraryOrThrow(holder.superType, type);
for (DexType iface : holder.interfaces.values) {
ensureFromLibraryOrThrow(iface, type);
}
}
}
if (!holder.annotations.isEmpty()) {
processAnnotations(holder.annotations.annotations);
}
// We also need to add the corresponding <clinit> to the set of live methods, as otherwise
// static field initialization (and other class-load-time sideeffects) will not happen.
if (!holder.isLibraryClass() && holder.hasNonTrivialClassInitializer()) {
DexEncodedMethod clinit = holder.getClassInitializer();
if (clinit != null) {
assert clinit.method.holder == holder.type;
markDirectStaticOrConstructorMethodAsLive(clinit, KeepReason.reachableFromLiveType(type));
}
}
// If this type has deferred annotations, we have to process those now, too.
Set<DexAnnotation> annotations = deferredAnnotations.remove(type);
if (annotations != null) {
annotations.forEach(this::handleAnnotationOfLiveType);
}
if (forceProguardCompatibility) {
// Add all dependent members to the workqueue.
enqueueRootItems(rootSet.getDependentItems(type));
} else {
// Add all dependent static members to the workqueue.
enqueueRootItems(rootSet.getDependentStaticMembers(type));
}
}
}
private void handleAnnotationOfLiveType(DexAnnotation annotation) {
AnnotationReferenceMarker referenceMarker = new AnnotationReferenceMarker(
annotation.annotation.type, appInfo.dexItemFactory);
annotation.annotation.collectIndexedItems(referenceMarker);
}
private void processAnnotations(DexAnnotation[] annotations) {
for (DexAnnotation annotation : annotations) {
processAnnotation(annotation);
}
}
private void processAnnotation(DexAnnotation annotation) {
DexType type = annotation.annotation.type;
if (liveTypes.contains(type)) {
// The type of this annotation is already live, so pick up its dependencies.
handleAnnotationOfLiveType(annotation);
} else {
// Remember this annotation for later.
deferredAnnotations.computeIfAbsent(type, ignore -> new HashSet<>()).add(annotation);
}
}
private void handleInvokeOfStaticTarget(DexMethod method, KeepReason reason) {
// We have to mark the resolved method as targeted even if it cannot actually be invoked
// to make sure the invocation will keep failing in the appropriate way.
ResolutionResult resolutionResult = appInfo.resolveMethod(method.holder, method);
if (resolutionResult == null) {
reportMissingMethod(method);
return;
}
resolutionResult.forEachTarget(m -> markMethodAsTargeted(m, reason));
// Only mark methods for which invocation will succeed at runtime live.
DexEncodedMethod targetMethod = appInfo.dispatchStaticInvoke(resolutionResult);
if (targetMethod != null) {
markDirectStaticOrConstructorMethodAsLive(targetMethod, reason);
}
}
private void handleInvokeOfDirectTarget(DexMethod method, KeepReason reason) {
// We have to mark the resolved method as targeted even if it cannot actually be invoked
// to make sure the invocation will keep failing in the appropriate way.
ResolutionResult resolutionResult = appInfo.resolveMethod(method.holder, method);
if (resolutionResult == null) {
reportMissingMethod(method);
return;
}
resolutionResult.forEachTarget(m -> markMethodAsTargeted(m, reason));
// Only mark methods for which invocation will succeed at runtime live.
DexEncodedMethod target = appInfo.dispatchDirectInvoke(resolutionResult);
if (target != null) {
markDirectStaticOrConstructorMethodAsLive(target, reason);
}
}
private void ensureFromLibraryOrThrow(DexType type, DexType context) {
if (tracingMainDex) {
// b/72312389: android.jar contains parts of JUnit and most developers include JUnit in
// their programs. This leads to library classes extending program classes. When tracing
// main dex lists we allow this.
return;
}
DexClass holder = appInfo.definitionFor(type);
if (holder != null && !holder.isLibraryClass()) {
Diagnostic message = new StringDiagnostic("Library class " + context.toSourceString()
+ (holder.isInterface() ? " implements " : " extends ")
+ "program class " + type.toSourceString());
if (forceProguardCompatibility) {
options.reporter.warning(message);
} else {
options.reporter.error(message);
}
}
}
private void reportMissingClass(DexType clazz) {
if (Log.ENABLED && reportedMissing.add(clazz)) {
Log.verbose(Enqueuer.class, "Class `%s` is missing.", clazz);
}
}
private void reportMissingMethod(DexMethod method) {
if (Log.ENABLED && reportedMissing.add(method)) {
Log.verbose(Enqueuer.class, "Method `%s` is missing.", method);
}
}
private void reportMissingField(DexField field) {
if (Log.ENABLED && reportedMissing.add(field)) {
Log.verbose(Enqueuer.class, "Field `%s` is missing.", field);
}
}
private void markMethodAsTargeted(DexEncodedMethod encodedMethod, KeepReason reason) {
markTypeAsLive(encodedMethod.method.holder);
if (Log.ENABLED) {
Log.verbose(getClass(), "Method `%s` is targeted.", encodedMethod.method);
}
targetedMethods.add(encodedMethod, reason);
if (forceProguardCompatibility) {
// Keep targeted default methods in compatibility mode. The tree pruner will otherwise make
// these methods abstract, whereas Proguard does not (seem to) touch their code.
DexClass clazz = appInfo.definitionFor(encodedMethod.method.holder);
if (!encodedMethod.accessFlags.isAbstract()
&& clazz.isInterface() && !clazz.isLibraryClass()) {
markMethodAsKeptWithCompatRule(encodedMethod);
}
}
}
/**
* Adds the class to the set of instantiated classes and marks its fields and methods live
* depending on the currently seen invokes and field reads.
*/
private void processNewlyInstantiatedClass(DexClass clazz, KeepReason reason) {
if (!instantiatedTypes.add(clazz.type, reason)) {
return;
}
collectProguardCompatibilityRule(reason);
if (Log.ENABLED) {
Log.verbose(getClass(), "Class `%s` is instantiated, processing...", clazz);
}
// This class becomes live, so it and all its supertypes become live types.
markTypeAsLive(clazz.type);
// For all methods of the class, if we have seen a call, mark the method live.
// We only do this for virtual calls, as the other ones will be done directly.
transitionMethodsForInstantiatedClass(clazz.type);
// For all instance fields visible from the class, mark them live if we have seen a read.
transitionFieldsForInstantiatedClass(clazz.type);
// Add all dependent members to the workqueue.
enqueueRootItems(rootSet.getDependentItems(clazz.type));
}
/**
* Marks all methods live that can be reached by calls previously seen.
* <p>
* <p>This should only be invoked if the given type newly becomes instantiated. In essence, this
* method replays all the invokes we have seen so far that could apply to this type and marks the
* corresponding methods live.
* <p>
* <p>Only methods that are visible in this type are considered. That is, only those methods that
* are either defined directly on this type or that are defined on a supertype but are not
* shadowed by another inherited method. Furthermore, default methods from implemented interfaces
* that are not otherwise shadowed are considered, too.
*/
private void transitionMethodsForInstantiatedClass(DexType instantiatedType) {
ScopedDexMethodSet seen = new ScopedDexMethodSet();
Set<DexType> interfaces = Sets.newIdentityHashSet();
DexType type = instantiatedType;
do {
DexClass clazz = appInfo.definitionFor(type);
if (clazz == null) {
reportMissingClass(type);
// TODO(herhut): In essence, our subtyping chain is broken here. Handle that case better.
break;
}
// We only have to look at virtual methods here, as only those can actually be executed at
// runtime. Illegal dispatch situations and the corresponding exceptions are already handled
// by the reachability logic.
SetWithReason<DexEncodedMethod> reachableMethods = reachableVirtualMethods.get(type);
if (reachableMethods != null) {
transitionNonAbstractMethodsToLiveAndShadow(reachableMethods.getItems(), instantiatedType,
seen);
}
Collections.addAll(interfaces, clazz.interfaces.values);
type = clazz.superType;
} while (type != null && !instantiatedTypes.contains(type));
// The set now contains all virtual methods on the type and its supertype that are reachable.
// In a second step, we now look at interfaces. We have to do this in this order due to JVM
// semantics for default methods. A default method is only reachable if it is not overridden in
// any superclass. Also, it is not defined which default method is chosen if multiple
// interfaces define the same default method. Hence, for every interface (direct or indirect),
// we have to look at the interface chain and mark default methods as reachable, not taking
// the shadowing of other interface chains into account.
// See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4.3.3
for (DexType iface : interfaces) {
DexClass clazz = appInfo.definitionFor(iface);
if (clazz == null) {
reportMissingClass(iface);
// TODO(herhut): In essence, our subtyping chain is broken here. Handle that case better.
break;
}
transitionDefaultMethodsForInstantiatedClass(iface, instantiatedType, seen);
}
}
private void transitionDefaultMethodsForInstantiatedClass(DexType iface, DexType instantiatedType,
ScopedDexMethodSet seen) {
DexClass clazz = appInfo.definitionFor(iface);
assert clazz != null : "Missing class " + iface.toSourceString();
assert clazz.accessFlags.isInterface();
SetWithReason<DexEncodedMethod> reachableMethods = reachableVirtualMethods.get(iface);
if (reachableMethods != null) {
transitionNonAbstractMethodsToLiveAndShadow(
reachableMethods.getItems(), instantiatedType, seen.newNestedScope());
}
for (DexType subInterface : clazz.interfaces.values) {
transitionDefaultMethodsForInstantiatedClass(subInterface, instantiatedType, seen);
}
}
private void transitionNonAbstractMethodsToLiveAndShadow(Iterable<DexEncodedMethod> reachable,
DexType instantiatedType, ScopedDexMethodSet seen) {
for (DexEncodedMethod encodedMethod : reachable) {
if (seen.addMethod(encodedMethod)) {
// Abstract methods do shadow implementations but they cannot be live, as they have no
// code.
if (!encodedMethod.accessFlags.isAbstract()) {
markVirtualMethodAsLive(encodedMethod,
KeepReason.reachableFromLiveType(instantiatedType));
}
}
}
}
/**
* Marks all fields live that can be reached by a read assuming that the given type or one of its
* subtypes is instantiated.
*/
private void transitionFieldsForInstantiatedClass(DexType type) {
do {
DexClass clazz = appInfo.definitionFor(type);
if (clazz == null) {
// TODO(herhut) The subtype chain is broken. We need a way to deal with this better.
reportMissingClass(type);
break;
}
SetWithReason<DexEncodedField> reachableFields = reachableInstanceFields.get(type);
if (reachableFields != null) {
for (DexEncodedField field : reachableFields.getItems()) {
markInstanceFieldAsLive(field, KeepReason.reachableFromLiveType(type));
}
}
type = clazz.superType;
} while (type != null && !instantiatedTypes.contains(type));
}
private void markStaticFieldAsLive(DexField field, KeepReason reason) {
// Mark the type live here, so that the class exists at runtime. Note that this also marks all
// supertypes as live, so even if the field is actually on a supertype, its class will be live.
markTypeAsLive(field.clazz);
DexType fieldType = field.type;
if (fieldType.isArrayType()) {
fieldType = fieldType.toBaseType(appInfo.dexItemFactory);
}
if (fieldType.isClassType()) {
markTypeAsLive(fieldType);
}
// Find the actual field.
DexEncodedField encodedField = appInfo.resolveFieldOn(field.clazz, field);
if (encodedField == null) {
reportMissingField(field);
return;
}
// This field might be an instance field reachable from a static context, e.g. a getStatic that
// resolves to an instance field. We have to keep the instance field nonetheless, as otherwise
// we might unmask a shadowed static field and hence change semantics.
if (encodedField.accessFlags.isStatic()) {
if (Log.ENABLED) {
Log.verbose(getClass(), "Adding static field `%s` to live set.", encodedField.field);
}
} else {
if (Log.ENABLED) {
Log.verbose(getClass(), "Adding instance field `%s` to live set (static context).",
encodedField.field);
}
}
liveFields.add(encodedField, reason);
collectProguardCompatibilityRule(reason);
// Add all dependent members to the workqueue.
enqueueRootItems(rootSet.getDependentItems(encodedField));
}
private void markInstanceFieldAsLive(DexEncodedField field, KeepReason reason) {
assert field != null;
markTypeAsLive(field.field.clazz);
DexType fieldType = field.field.type;
if (fieldType.isArrayType()) {
fieldType = fieldType.toBaseType(appInfo.dexItemFactory);
}
if (fieldType.isClassType()) {
markTypeAsLive(fieldType);
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Adding instance field `%s` to live set.", field.field);
}
liveFields.add(field, reason);
collectProguardCompatibilityRule(reason);
// Add all dependent members to the workqueue.
enqueueRootItems(rootSet.getDependentItems(field));
}
private void markInstantiated(DexType type, DexEncodedMethod method) {
if (instantiatedTypes.contains(type)) {
return;
}
DexClass clazz = appInfo.definitionFor(type);
if (clazz == null) {
reportMissingClass(type);
return;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Register new instantiation of `%s`.", clazz);
}
workList.add(Action.markInstantiated(clazz, KeepReason.instantiatedIn(method)));
}
private void markLambdaInstantiated(DexType itf, DexEncodedMethod method) {
instantiatedLambdas.add(itf, KeepReason.instantiatedIn(method));
}
private void markDirectStaticOrConstructorMethodAsLive(
DexEncodedMethod encodedMethod, KeepReason reason) {
assert encodedMethod != null;
if (!liveMethods.contains(encodedMethod)) {
markTypeAsLive(encodedMethod.method.holder);
markMethodAsTargeted(encodedMethod, reason);
// For granting inner/outer classes access to their private constructors, javac generates
// additional synthetic constructors. These constructors take a synthetic class
// as argument. As it is not possible to express a keep rule for these synthetic classes
// always keep synthetic arguments to synthetic constructors. See b/69825683.
if (encodedMethod.isInstanceInitializer() && encodedMethod.isSyntheticMethod()) {
for (DexType type : encodedMethod.method.proto.parameters.values) {
type = type.isArrayType() ? type.toBaseType(appInfo.dexItemFactory) : type;
if (type.isPrimitiveType()) {
continue;
}
DexClass clazz = appInfo.definitionFor(type);
if (clazz != null && clazz.accessFlags.isSynthetic()) {
markTypeAsLive(type);
}
}
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Method `%s` has become live due to direct invoke",
encodedMethod.method);
}
workList.add(Action.markMethodLive(encodedMethod, reason));
}
}
private void markVirtualMethodAsLive(DexEncodedMethod method, KeepReason reason) {
assert method != null;
assert !method.accessFlags.isAbstract();
if (!liveMethods.contains(method)) {
if (Log.ENABLED) {
Log.verbose(getClass(), "Adding virtual method `%s` to live set.", method.method);
}
workList.add(Action.markMethodLive(method, reason));
}
}
private boolean isInstantiatedOrHasInstantiatedSubtype(DexType type) {
return instantiatedTypes.contains(type)
|| appInfo.subtypes(type).stream().anyMatch(instantiatedTypes::contains);
}
private void markInstanceFieldAsReachable(DexField field, KeepReason reason) {
if (Log.ENABLED) {
Log.verbose(getClass(), "Marking instance field `%s` as reachable.", field);
}
DexEncodedField encodedField = appInfo.resolveFieldOn(field.clazz, field);
if (encodedField == null) {
reportMissingField(field);
return;
}
// We might have a instance field access that is dispatched to a static field. In such case,
// we have to keep the static field, so that the dispatch fails at runtime in the same way that
// it did before. We have to keep the field even if the receiver has no live inhabitants, as
// field resolution happens before the receiver is inspected.
if (encodedField.accessFlags.isStatic()) {
markStaticFieldAsLive(encodedField.field, reason);
} else {
SetWithReason<DexEncodedField> reachable = reachableInstanceFields
.computeIfAbsent(encodedField.field.clazz, ignore -> new SetWithReason<>());
if (reachable.add(encodedField, reason) && isInstantiatedOrHasInstantiatedSubtype(
encodedField.field.clazz)) {
// We have at least one live subtype, so mark it as live.
markInstanceFieldAsLive(encodedField, reason);
}
}
}
private void markVirtualMethodAsReachable(DexMethod method, boolean interfaceInvoke,
KeepReason reason) {
if (!virtualTargetsMarkedAsReachable.add(method)) {
return;
}
if (Log.ENABLED) {
Log.verbose(getClass(), "Marking virtual method `%s` as reachable.", method);
}
if (method.holder.isArrayType()) {
// This is an array type, so the actual class will be generated at runtime. We treat this
// like an invoke on a direct subtype of java.lang.Object that has no further subtypes.
// As it has no subtypes, it cannot affect liveness of the program we are processing.
// Ergo, we can ignore it. We need to make sure that the element type is available, though.
DexType baseType = method.holder.toBaseType(appInfo.dexItemFactory);
if (baseType.isClassType()) {
markTypeAsLive(baseType);
}
return;
}
DexClass holder = appInfo.definitionFor(method.holder);
if (holder == null) {
reportMissingClass(method.holder);
return;
}
DexEncodedMethod topTarget = interfaceInvoke
? appInfo.resolveMethodOnInterface(method.holder, method).asResultOfResolve()
: appInfo.resolveMethodOnClass(method.holder, method).asResultOfResolve();
if (topTarget == null) {
reportMissingMethod(method);
return;
}
// We have to mark this as targeted, as even if this specific instance never becomes live, we
// need at least an abstract version of it so that we have a target for the corresponding
// invoke.
markMethodAsTargeted(topTarget, reason);
Set<DexEncodedMethod> targets = interfaceInvoke
? appInfo.lookupInterfaceTargets(method)
: appInfo.lookupVirtualTargets(method);
for (DexEncodedMethod encodedMethod : targets) {
SetWithReason<DexEncodedMethod> reachable = reachableVirtualMethods
.computeIfAbsent(encodedMethod.method.holder, (ignore) -> new SetWithReason<>());
if (reachable.add(encodedMethod, reason)) {
// Abstract methods cannot be live.
if (!encodedMethod.accessFlags.isAbstract()) {
// If the holder type is instantiated, the method is live. Otherwise check whether we find
// a subtype that does not shadow this methods but is instantiated.
// Note that library classes are always considered instantiated, as we do not know where
// they are instantiated.
if (isInstantiatedOrHasInstantiatedSubtype(encodedMethod.method.holder)) {
if (instantiatedTypes.contains(encodedMethod.method.holder)) {
markVirtualMethodAsLive(encodedMethod,
KeepReason.reachableFromLiveType(encodedMethod.method.holder));
} else {
Deque<DexType> worklist = new ArrayDeque<>();
fillWorkList(worklist, encodedMethod.method.holder);
while (!worklist.isEmpty()) {
DexType current = worklist.pollFirst();
DexClass currentHolder = appInfo.definitionFor(current);
// If this class shadows the virtual, abort the search. Note, according to JVM spec,
// shadowing is independent of whether a method is public or private.
if (currentHolder == null
|| currentHolder.lookupMethod(encodedMethod.method) != null) {
continue;
}
if (instantiatedTypes.contains(current)) {
markVirtualMethodAsLive(encodedMethod, KeepReason.reachableFromLiveType(current));
break;
}
fillWorkList(worklist, current);
}
}
}
}
}
}
}
private static void fillWorkList(Deque<DexType> worklist, DexType type) {
if (type.isInterface()) {
// We need to check if the method is shadowed by a class that directly implements
// the interface and go recursively down to the sub interfaces to reach class
// implementing the interface
type.forAllImplementsSubtypes(worklist::addLast);
type.forAllExtendsSubtypes(worklist::addLast);
} else {
type.forAllExtendsSubtypes(worklist::addLast);
}
}
private void markSuperMethodAsReachable(DexMethod method, DexEncodedMethod from) {
// We have to mark the immediate target of the descriptor as targeted, as otherwise
// the invoke super will fail in the resolution step with a NSM error.
// See <a
// href="https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokespecial">
// the JVM spec for invoke-special.
DexEncodedMethod resolutionTarget = appInfo.resolveMethod(method.holder, method)
.asResultOfResolve();
if (resolutionTarget == null) {
reportMissingMethod(method);
return;
}
markMethodAsTargeted(resolutionTarget, KeepReason.targetedBySuperFrom(from));
// Now we need to compute the actual target in the context.
DexEncodedMethod target = appInfo.lookupSuperTarget(method, from.method.holder);
if (target == null) {
// The actual implementation in the super class is missing.
reportMissingMethod(method);
return;
}
assert !superInvokeDependencies.containsKey(from) || !superInvokeDependencies.get(from)
.contains(target);
if (Log.ENABLED) {
Log.verbose(getClass(), "Adding super constraint from `%s` to `%s`", from.method,
target.method);
}
superInvokeDependencies.computeIfAbsent(from, ignore -> Sets.newIdentityHashSet()).add(target);
if (liveMethods.contains(from)) {
markMethodAsTargeted(target, KeepReason.invokedViaSuperFrom(from));
if (!target.accessFlags.isAbstract()) {
markVirtualMethodAsLive(target, KeepReason.invokedViaSuperFrom(from));
}
}
}
public ReasonPrinter getReasonPrinter(Set<DexItem> queriedItems) {
// If no reason was asked, just return a no-op printer to avoid computing the information.
// This is the common path.
if (queriedItems.isEmpty()) {
return ReasonPrinter.getNoOpPrinter();
}
Map<DexItem, KeepReason> reachability = new HashMap<>();
for (SetWithReason<DexEncodedMethod> mappings : reachableVirtualMethods.values()) {
reachability.putAll(mappings.getReasons());
}
for (SetWithReason<DexEncodedField> mappings : reachableInstanceFields.values()) {
reachability.putAll(mappings.getReasons());
}
return new ReasonPrinter(queriedItems, liveFields.getReasons(), liveMethods.getReasons(),
reachability, instantiatedTypes.getReasons());
}
public AppInfoWithLiveness traceMainDex(
RootSet rootSet, ExecutorService executorService, Timing timing) throws ExecutionException {
this.tracingMainDex = true;
this.rootSet = rootSet;
// Translate the result of root-set computation into enqueuer actions.
enqueueRootItems(rootSet.noShrinking);
AppInfoWithLiveness appInfo = trace(executorService, timing);
options.reporter.failIfPendingErrors();
return appInfo;
}
public AppInfoWithLiveness traceApplication(
RootSet rootSet, ExecutorService executorService, Timing timing) throws ExecutionException {
this.rootSet = rootSet;
// Translate the result of root-set computation into enqueuer actions.
enqueueRootItems(rootSet.noShrinking);
appInfo.libraryClasses().forEach(this::markAllLibraryVirtualMethodsReachable);
AppInfoWithLiveness result = trace(executorService, timing);
options.reporter.failIfPendingErrors();
return result;
}
private AppInfoWithLiveness trace(
ExecutorService executorService, Timing timing) throws ExecutionException {
timing.begin("Grow the tree.");
try {
while (true) {
long numOfLiveItems = (long) liveTypes.size();
numOfLiveItems += (long) liveMethods.items.size();
numOfLiveItems += (long) liveFields.items.size();
while (!workList.isEmpty()) {
Action action = workList.poll();
switch (action.kind) {
case MARK_INSTANTIATED:
processNewlyInstantiatedClass((DexClass) action.target, action.reason);
break;
case MARK_REACHABLE_FIELD:
markInstanceFieldAsReachable((DexField) action.target, action.reason);
break;
case MARK_REACHABLE_VIRTUAL:
markVirtualMethodAsReachable((DexMethod) action.target, false, action.reason);
break;
case MARK_REACHABLE_INTERFACE:
markVirtualMethodAsReachable((DexMethod) action.target, true, action.reason);
break;
case MARK_REACHABLE_SUPER:
markSuperMethodAsReachable((DexMethod) action.target,
(DexEncodedMethod) action.context);
break;
case MARK_METHOD_KEPT:
markMethodAsKept((DexEncodedMethod) action.target, action.reason);
break;
case MARK_FIELD_KEPT:
markFieldAsKept((DexEncodedField) action.target, action.reason);
break;
case MARK_METHOD_LIVE:
processNewlyLiveMethod(((DexEncodedMethod) action.target), action.reason);
break;
default:
throw new IllegalArgumentException(action.kind.toString());
}
}
// Continue fix-point processing if -if rules are enabled by items that newly became live.
long numOfLiveItemsAfterProcessing = (long) liveTypes.size();
numOfLiveItemsAfterProcessing += (long) liveMethods.items.size();
numOfLiveItemsAfterProcessing += (long) liveFields.items.size();
if (numOfLiveItemsAfterProcessing > numOfLiveItems) {
RootSetBuilder consequentSetBuilder =
new RootSetBuilder(appInfo, rootSet.ifRules, options);
ConsequentRootSet consequentRootSet = consequentSetBuilder.runForIfRules(
executorService, liveTypes, liveMethods.getItems(), liveFields.getItems());
enqueueRootItems(consequentRootSet.noShrinking);
rootSet.noOptimization.addAll(consequentRootSet.noOptimization);
rootSet.noObfuscation.addAll(consequentRootSet.noObfuscation);
if (!workList.isEmpty()) {
continue;
}
}
// Continue fix-point processing while there are additional work items to ensure
// Proguard compatibility.
if (proguardCompatibilityWorkList.isEmpty()
&& pendingProguardReflectiveCompatibility.isEmpty()) {
break;
}
pendingProguardReflectiveCompatibility.forEach(this::handleProguardReflectiveBehavior);
workList.addAll(proguardCompatibilityWorkList);
proguardCompatibilityWorkList.clear();
pendingProguardReflectiveCompatibility.clear();
}
if (Log.ENABLED) {
Set<DexEncodedMethod> allLive = Sets.newIdentityHashSet();
for (Entry<DexType, SetWithReason<DexEncodedMethod>> entry : reachableVirtualMethods
.entrySet()) {
allLive.addAll(entry.getValue().getItems());
}
Set<DexEncodedMethod> reachableNotLive = Sets.difference(allLive, liveMethods.getItems());
Log.debug(getClass(), "%s methods are reachable but not live", reachableNotLive.size());
Log.info(getClass(), "Only reachable: %s", reachableNotLive);
Set<DexType> liveButNotInstantiated =
Sets.difference(liveTypes, instantiatedTypes.getItems());
Log.debug(getClass(), "%s classes are live but not instantiated",
liveButNotInstantiated.size());
Log.info(getClass(), "Live but not instantiated: %s", liveButNotInstantiated);
SetView<DexEncodedMethod> targetedButNotLive = Sets
.difference(targetedMethods.getItems(), liveMethods.getItems());
Log.debug(getClass(), "%s methods are targeted but not live", targetedButNotLive.size());
Log.info(getClass(), "Targeted but not live: %s", targetedButNotLive);
}
assert liveTypes.stream().allMatch(DexType::isClassType);
assert instantiatedTypes.getItems().stream().allMatch(DexType::isClassType);
} finally {
timing.end();
}
return new AppInfoWithLiveness(appInfo, this);
}
private void markMethodAsKept(DexEncodedMethod target, KeepReason reason) {
DexClass holder = appInfo.definitionFor(target.method.holder);
// If this method no longer has a corresponding class then we have shaken it away before.
if (holder == null) {
return;
}
if (target.isVirtualMethod()) {
// A virtual method. Mark it as reachable so that subclasses, if instantiated, keep
// their overrides. However, we don't mark it live, as a keep rule might not imply that
// the corresponding class is live.
markVirtualMethodAsReachable(target.method, holder.accessFlags.isInterface(), reason);
// Reachability for default methods is based on live subtypes in general. For keep rules,
// we need special handling as we essentially might have live subtypes that are outside of
// our reach. Do this here, as we still know that this is due to a keep rule.
if (holder.isInterface() && target.isNonAbstractVirtualMethod()) {
markVirtualMethodAsLive(target, reason);
}
} else {
markDirectStaticOrConstructorMethodAsLive(target, reason);
}
}
private void markFieldAsKept(DexEncodedField target, KeepReason reason) {
// If this field no longer has a corresponding class, then we have shaken it away before.
if (appInfo.definitionFor(target.field.clazz) == null) {
return;
}
if (target.accessFlags.isStatic()) {
markStaticFieldAsLive(target.field, reason);
} else {
markInstanceFieldAsReachable(target.field, reason);
}
}
private void markAllLibraryVirtualMethodsReachable(DexClass clazz) {
assert clazz.isLibraryClass();
if (Log.ENABLED) {
Log.verbose(getClass(), "Marking all methods of library class `%s` as reachable.",
clazz.type);
}
for (DexEncodedMethod encodedMethod : clazz.virtualMethods()) {
markMethodAsTargeted(encodedMethod, KeepReason.isLibraryMethod());
markVirtualMethodAsReachable(encodedMethod.method, clazz.isInterface(),
KeepReason.isLibraryMethod());
}
}
private void processNewlyLiveMethod(DexEncodedMethod method, KeepReason reason) {
if (liveMethods.add(method, reason)) {
collectProguardCompatibilityRule(reason);
DexClass holder = appInfo.definitionFor(method.method.holder);
assert holder != null;
if (holder.isLibraryClass()) {
// We do not process library classes.
return;
}
Set<DexEncodedMethod> superCallTargets = superInvokeDependencies.get(method);
if (superCallTargets != null) {
for (DexEncodedMethod superCallTarget : superCallTargets) {
if (Log.ENABLED) {
Log.verbose(getClass(), "Found super invoke constraint on `%s`.",
superCallTarget.method);
}
markMethodAsTargeted(superCallTarget, KeepReason.invokedViaSuperFrom(method));
markVirtualMethodAsLive(superCallTarget, KeepReason.invokedViaSuperFrom(method));
}
}
for (DexType parameterType : method.method.proto.parameters.values) {
if (parameterType.isArrayType()) {
parameterType = parameterType.toBaseType(appInfo.dexItemFactory);
}
if (parameterType.isClassType()) {
markTypeAsLive(parameterType);
}
}
processAnnotations(method.annotations.annotations);
method.parameterAnnotationsList.forEachAnnotation(this::processAnnotation);
if (protoLiteExtension != null && protoLiteExtension.appliesTo(method)) {
protoLiteExtension.processMethod(method, new UseRegistry(method), protoLiteFields);
} else {
method.registerCodeReferences(new UseRegistry(method));
}
// Add all dependent members to the workqueue.
enqueueRootItems(rootSet.getDependentItems(method));
}
}
private void collectProguardCompatibilityRule(KeepReason reason) {
if (reason.isDueToProguardCompatibility() && compatibility != null) {
compatibility.addRule(reason.getProguardKeepRule());
}
}
private Map<DexField, Set<DexEncodedMethod>> collectFields(
Map<DexType, Set<TargetWithContext<DexField>>> map) {
Map<DexField, Set<DexEncodedMethod>> result = new IdentityHashMap<>();
for (Map.Entry<DexType, Set<TargetWithContext<DexField>>> entry : map.entrySet()) {
for (TargetWithContext<DexField> fieldWithContext : entry.getValue()) {
DexField field = fieldWithContext.getTarget();
DexEncodedMethod context = fieldWithContext.getContext();
result.computeIfAbsent(field, k -> Sets.newIdentityHashSet())
.add(context);
}
}
return result;
}
Map<DexField, Set<DexEncodedMethod>> collectInstanceFieldsRead() {
return Collections.unmodifiableMap(collectFields(instanceFieldsRead));
}
Map<DexField, Set<DexEncodedMethod>> collectInstanceFieldsWritten() {
return Collections.unmodifiableMap(collectFields(instanceFieldsWritten));
}
Map<DexField, Set<DexEncodedMethod>> collectStaticFieldsRead() {
return Collections.unmodifiableMap(collectFields(staticFieldsRead));
}
Map<DexField, Set<DexEncodedMethod>> collectStaticFieldsWritten() {
return Collections.unmodifiableMap(collectFields(staticFieldsWritten));
}
private Set<DexField> collectReachedFields(
Set<DexField> set, Function<DexField, DexField> lookup) {
return set.stream()
.map(lookup)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(Sets::newIdentityHashSet));
}
private DexField tryLookupInstanceField(DexField field) {
DexEncodedField target = appInfo.lookupInstanceTarget(field.clazz, field);
return target == null ? null : target.field;
}
private DexField tryLookupStaticField(DexField field) {
DexEncodedField target = appInfo.lookupStaticTarget(field.clazz, field);
return target == null ? null : target.field;
}
SortedSet<DexField> mergeFieldAccesses(Set<DexField> instanceFields, Set<DexField> staticFields) {
return ImmutableSortedSet.copyOf(PresortedComparable<DexField>::slowCompareTo,
Sets.union(
collectReachedFields(instanceFields, this::tryLookupInstanceField),
collectReachedFields(staticFields, this::tryLookupStaticField)));
}
private void markClassAsInstantiatedWithCompatRule(DexClass clazz) {
ProguardKeepRule rule =
ProguardConfigurationUtils.buildDefaultInitializerKeepRule(clazz);
proguardCompatibilityWorkList.add(
Action.markInstantiated(clazz, KeepReason.dueToProguardCompatibilityKeepRule(rule)));
if (clazz.hasDefaultInitializer()) {
proguardCompatibilityWorkList.add(
Action.markMethodLive(
clazz.getDefaultInitializer(), KeepReason.dueToProguardCompatibilityKeepRule(rule)));
}
}
private void markFieldAsKeptWithCompatRule(DexEncodedField field) {
DexClass holderClass = appInfo.definitionFor(field.field.getHolder());
ProguardKeepRule rule =
ProguardConfigurationUtils.buildFieldKeepRule(holderClass, field);
proguardCompatibilityWorkList.add(
Action.markFieldKept(field, KeepReason.dueToProguardCompatibilityKeepRule(rule)));
}
private void markMethodAsKeptWithCompatRule(DexEncodedMethod method) {
DexClass holderClass = appInfo.definitionFor(method.method.getHolder());
ProguardKeepRule rule =
ProguardConfigurationUtils.buildMethodKeepRule(holderClass, method);
proguardCompatibilityWorkList.add(
Action.markMethodLive(method, KeepReason.dueToProguardCompatibilityKeepRule(rule)));
}
private void handleProguardReflectiveBehavior(DexEncodedMethod method) {
IRCode code = method.buildIR(appInfo, options, appInfo.originFor(method.method.holder));
code.instructionIterator().forEachRemaining(this::handleProguardReflectiveBehavior);
}
private void handleProguardReflectiveBehavior(Instruction instruction) {
if (!instruction.isInvokeMethod()) {
return;
}
InvokeMethod invoke = instruction.asInvokeMethod();
DexMethod invokedMethod = invoke.getInvokedMethod();
if (!isReflectionMethod(appInfo.dexItemFactory, invokedMethod)) {
return;
}
DexItemBasedString itemBasedString = identifyIdentiferNameString(appInfo, invoke);
if (itemBasedString == null) {
return;
}
if (itemBasedString.basedOn instanceof DexType) {
DexClass clazz = appInfo.definitionFor((DexType) itemBasedString.basedOn);
if (clazz != null) {
markClassAsInstantiatedWithCompatRule(clazz);
}
} else if (itemBasedString.basedOn instanceof DexField) {
DexEncodedField encodedField = appInfo.definitionFor((DexField) itemBasedString.basedOn);
if (encodedField != null) {
markFieldAsKeptWithCompatRule(encodedField);
}
} else {
assert itemBasedString.basedOn instanceof DexMethod;
DexEncodedMethod encodedMethod = appInfo.definitionFor((DexMethod) itemBasedString.basedOn);
if (encodedMethod != null) {
markMethodAsKeptWithCompatRule(encodedMethod);
}
}
}
private static class Action {
final Kind kind;
final DexItem target;
final DexItem context;
final KeepReason reason;
private Action(Kind kind, DexItem target, DexItem context, KeepReason reason) {
this.kind = kind;
this.target = target;
this.context = context;
this.reason = reason;
}
public static Action markReachableVirtual(DexMethod method, KeepReason reason) {
return new Action(Kind.MARK_REACHABLE_VIRTUAL, method, null, reason);
}
public static Action markReachableInterface(DexMethod method, KeepReason reason) {
return new Action(Kind.MARK_REACHABLE_INTERFACE, method, null, reason);
}
public static Action markReachableSuper(DexMethod method, DexEncodedMethod from) {
return new Action(Kind.MARK_REACHABLE_SUPER, method, from, null);
}
public static Action markReachableField(DexField field, KeepReason reason) {
return new Action(Kind.MARK_REACHABLE_FIELD, field, null, reason);
}
public static Action markInstantiated(DexClass clazz, KeepReason reason) {
return new Action(Kind.MARK_INSTANTIATED, clazz, null, reason);
}
public static Action markMethodLive(DexEncodedMethod method, KeepReason reason) {
return new Action(Kind.MARK_METHOD_LIVE, method, null, reason);
}
public static Action markMethodKept(DexEncodedMethod method, KeepReason reason) {
return new Action(Kind.MARK_METHOD_KEPT, method, null, reason);
}
public static Action markFieldKept(DexEncodedField field, KeepReason reason) {
return new Action(Kind.MARK_FIELD_KEPT, field, null, reason);
}
private enum Kind {
MARK_REACHABLE_VIRTUAL,
MARK_REACHABLE_INTERFACE,
MARK_REACHABLE_SUPER,
MARK_REACHABLE_FIELD,
MARK_INSTANTIATED,
MARK_METHOD_LIVE,
MARK_METHOD_KEPT,
MARK_FIELD_KEPT
}
}
/**
* Encapsulates liveness and reachability information for an application.
*/
public static class AppInfoWithLiveness extends AppInfoWithSubtyping {
/**
* Set of types that are mentioned in the program. We at least need an empty abstract classitem
* for these.
*/
public final SortedSet<DexType> liveTypes;
/**
* Set of types that are actually instantiated. These cannot be abstract.
*/
final SortedSet<DexType> instantiatedTypes;
/**
* Set of methods that are the immediate target of an invoke. They might not actually be live
* but are required so that invokes can find the method. If such a method is not live (i.e. not
* contained in {@link #liveMethods}, it may be marked as abstract and its implementation may be
* removed.
*/
final SortedSet<DexMethod> targetedMethods;
/**
* Set of methods that belong to live classes and can be reached by invokes. These need to be
* kept.
*/
final SortedSet<DexMethod> liveMethods;
/**
* Set of fields that belong to live classes and can be reached by invokes. These need to be
* kept.
*/
public final SortedSet<DexField> liveFields;
/**
* Set of all fields which may be touched by a get operation. This is actual field definitions.
*/
public final SortedSet<DexField> fieldsRead;
/**
* Set of all fields which may be touched by a put operation. This is actual field definitions.
*/
public final SortedSet<DexField> fieldsWritten;
/**
* Set of all field ids used in instance field reads, along with access context.
*/
public final Map<DexField, Set<DexEncodedMethod>> instanceFieldReads;
/**
* Set of all field ids used in instance field writes, along with access context.
*/
public final Map<DexField, Set<DexEncodedMethod>> instanceFieldWrites;
/**
* Set of all field ids used in static field reads, along with access context.
*/
public final Map<DexField, Set<DexEncodedMethod>> staticFieldReads;
/**
* Set of all field ids used in static field writes, along with access context.
*/
public final Map<DexField, Set<DexEncodedMethod>> staticFieldWrites;
/**
* Set of all methods referenced in virtual invokes;
*/
public final SortedSet<DexMethod> virtualInvokes;
/**
* Set of all methods referenced in interface invokes;
*/
public final SortedSet<DexMethod> interfaceInvokes;
/**
* Set of all methods referenced in super invokes;
*/
public final SortedSet<DexMethod> superInvokes;
/**
* Set of all methods referenced in direct invokes;
*/
public final SortedSet<DexMethod> directInvokes;
/**
* Set of all methods referenced in static invokes;
*/
public final SortedSet<DexMethod> staticInvokes;
/**
* Set of all items that have to be kept independent of whether they are used.
*/
final Set<DexItem> pinnedItems;
/**
* All items with assumenosideeffects rule.
*/
public final Map<DexItem, ProguardMemberRule> noSideEffects;
/**
* All items with assumevalues rule.
*/
public final Map<DexItem, ProguardMemberRule> assumedValues;
/**
* All methods that have to be inlined due to a configuration directive.
*/
public final Set<DexItem> alwaysInline;
/**
* All items with -identifiernamestring rule.
*/
public final Set<DexItem> identifierNameStrings;
/**
* Set of fields that have been identified as proto-lite fields by the
* {@link ProtoLiteExtension}.
*/
final Set<DexField> protoLiteFields;
/**
* A set of types that have been removed by the {@link TreePruner}.
*/
final Set<DexType> prunedTypes;
/**
* A map from switchmap class types to their corresponding switchmaps.
*/
final Map<DexField, Int2ReferenceMap<DexField>> switchMaps;
/**
* A map from enum types to their ordinal values.
*/
final Map<DexType, Reference2IntMap<DexField>> ordinalsMaps;
final ImmutableSortedSet<DexType> instantiatedLambdas;
private AppInfoWithLiveness(AppInfoWithSubtyping appInfo, Enqueuer enqueuer) {
super(appInfo);
this.liveTypes = ImmutableSortedSet.copyOf(
PresortedComparable<DexType>::slowCompareTo, enqueuer.liveTypes);
this.instantiatedTypes = ImmutableSortedSet.copyOf(
PresortedComparable<DexType>::slowCompareTo, enqueuer.instantiatedTypes.getItems());
this.instantiatedLambdas =
ImmutableSortedSet.copyOf(
PresortedComparable<DexType>::slowCompareTo, enqueuer.instantiatedLambdas.getItems());
this.targetedMethods = toSortedDescriptorSet(enqueuer.targetedMethods.getItems());
this.liveMethods = toSortedDescriptorSet(enqueuer.liveMethods.getItems());
this.liveFields = toSortedDescriptorSet(enqueuer.liveFields.getItems());
this.instanceFieldReads = enqueuer.collectInstanceFieldsRead();
this.instanceFieldWrites = enqueuer.collectInstanceFieldsWritten();
this.staticFieldReads = enqueuer.collectStaticFieldsRead();
this.staticFieldWrites = enqueuer.collectStaticFieldsWritten();
this.fieldsRead = enqueuer.mergeFieldAccesses(
instanceFieldReads.keySet(), staticFieldReads.keySet());
this.fieldsWritten = enqueuer.mergeFieldAccesses(
instanceFieldWrites.keySet(), staticFieldWrites.keySet());
this.pinnedItems = rewritePinnedItemsToDescriptors(enqueuer.pinnedItems);
this.virtualInvokes = joinInvokedMethods(enqueuer.virtualInvokes);
this.interfaceInvokes = joinInvokedMethods(enqueuer.interfaceInvokes);
this.superInvokes = joinInvokedMethods(enqueuer.superInvokes, TargetWithContext::getTarget);
this.directInvokes = joinInvokedMethods(enqueuer.directInvokes);
this.staticInvokes = joinInvokedMethods(enqueuer.staticInvokes);
this.noSideEffects = enqueuer.rootSet.noSideEffects;
this.assumedValues = enqueuer.rootSet.assumedValues;
this.alwaysInline = enqueuer.rootSet.alwaysInline;
this.identifierNameStrings =
Sets.union(enqueuer.rootSet.identifierNameStrings, enqueuer.identifierNameStrings);
this.protoLiteFields = enqueuer.protoLiteFields;
this.prunedTypes = Collections.emptySet();
this.switchMaps = Collections.emptyMap();
this.ordinalsMaps = Collections.emptyMap();
assert Sets.intersection(instanceFieldReads.keySet(), staticFieldReads.keySet()).isEmpty();
assert Sets.intersection(instanceFieldWrites.keySet(), staticFieldWrites.keySet()).isEmpty();
}
private AppInfoWithLiveness(AppInfoWithLiveness previous, DexApplication application,
Collection<DexType> removedClasses) {
super(application);
this.liveTypes = previous.liveTypes;
this.instantiatedTypes = previous.instantiatedTypes;
this.instantiatedLambdas = previous.instantiatedLambdas;
this.targetedMethods = previous.targetedMethods;
this.liveMethods = previous.liveMethods;
this.liveFields = previous.liveFields;
this.instanceFieldReads = previous.instanceFieldReads;
this.instanceFieldWrites = previous.instanceFieldWrites;
this.staticFieldReads = previous.staticFieldReads;
this.staticFieldWrites = previous.staticFieldWrites;
this.fieldsRead = previous.fieldsRead;
// TODO(herhut): We remove fields that are only written, so maybe update this.
this.fieldsWritten = previous.fieldsWritten;
assert assertNoItemRemoved(previous.pinnedItems, removedClasses);
this.pinnedItems = previous.pinnedItems;
this.noSideEffects = previous.noSideEffects;
this.assumedValues = previous.assumedValues;
this.virtualInvokes = previous.virtualInvokes;
this.interfaceInvokes = previous.interfaceInvokes;
this.superInvokes = previous.superInvokes;
this.directInvokes = previous.directInvokes;
this.staticInvokes = previous.staticInvokes;
this.protoLiteFields = previous.protoLiteFields;
this.alwaysInline = previous.alwaysInline;
this.identifierNameStrings = previous.identifierNameStrings;
this.prunedTypes = mergeSets(previous.prunedTypes, removedClasses);
this.switchMaps = previous.switchMaps;
this.ordinalsMaps = previous.ordinalsMaps;
assert Sets.intersection(instanceFieldReads.keySet(), staticFieldReads.keySet()).isEmpty();
assert Sets.intersection(instanceFieldWrites.keySet(), staticFieldWrites.keySet()).isEmpty();
}
private AppInfoWithLiveness(AppInfoWithLiveness previous,
DirectMappedDexApplication application,
GraphLense lense) {
super(application, lense);
this.liveTypes = rewriteItems(previous.liveTypes, lense::lookupType);
this.instantiatedTypes = rewriteItems(previous.instantiatedTypes, lense::lookupType);
this.instantiatedLambdas = rewriteItems(previous.instantiatedLambdas, lense::lookupType);
this.targetedMethods = rewriteMethodsConservatively(previous.targetedMethods, lense);
this.liveMethods = rewriteMethodsConservatively(previous.liveMethods, lense);
this.liveFields = rewriteItems(previous.liveFields, lense::lookupField);
this.instanceFieldReads =
rewriteKeysWhileMergingValues(previous.instanceFieldReads, lense::lookupField);
this.instanceFieldWrites =
rewriteKeysWhileMergingValues(previous.instanceFieldWrites, lense::lookupField);
this.staticFieldReads =
rewriteKeysWhileMergingValues(previous.staticFieldReads, lense::lookupField);
this.staticFieldWrites =
rewriteKeysWhileMergingValues(previous.staticFieldWrites, lense::lookupField);
this.fieldsRead = rewriteItems(previous.fieldsRead, lense::lookupField);
this.fieldsWritten = rewriteItems(previous.fieldsWritten, lense::lookupField);
this.pinnedItems = rewriteMixedItems(previous.pinnedItems, lense);
this.virtualInvokes = rewriteMethodsConservatively(previous.virtualInvokes, lense);
this.interfaceInvokes = rewriteMethodsConservatively(previous.interfaceInvokes, lense);
this.superInvokes = rewriteMethodsConservatively(previous.superInvokes, lense);
this.directInvokes = rewriteMethodsConservatively(previous.directInvokes, lense);
this.staticInvokes = rewriteMethodsConservatively(previous.staticInvokes, lense);
this.prunedTypes = rewriteItems(previous.prunedTypes, lense::lookupType);
// TODO(herhut): Migrate these to Descriptors, as well.
assert assertNotModifiedByLense(previous.noSideEffects.keySet(), lense);
this.noSideEffects = previous.noSideEffects;
assert assertNotModifiedByLense(previous.assumedValues.keySet(), lense);
this.assumedValues = previous.assumedValues;
assert assertNotModifiedByLense(previous.alwaysInline, lense);
this.alwaysInline = previous.alwaysInline;
this.identifierNameStrings = rewriteMixedItems(previous.identifierNameStrings, lense);
// Switchmap classes should never be affected by renaming.
assert assertNotModifiedByLense(
previous.switchMaps.keySet().stream().map(this::definitionFor).filter(Objects::nonNull)
.collect(Collectors.toList()), lense);
this.switchMaps = previous.switchMaps;
this.ordinalsMaps = rewriteKeys(previous.ordinalsMaps, lense::lookupType);
this.protoLiteFields = previous.protoLiteFields;
// Sanity check sets after rewriting.
assert Sets.intersection(instanceFieldReads.keySet(), staticFieldReads.keySet()).isEmpty();
assert Sets.intersection(instanceFieldWrites.keySet(), staticFieldWrites.keySet()).isEmpty();
}
public AppInfoWithLiveness(AppInfoWithLiveness previous,
Map<DexField, Int2ReferenceMap<DexField>> switchMaps,
Map<DexType, Reference2IntMap<DexField>> ordinalsMaps) {
super(previous);
this.liveTypes = previous.liveTypes;
this.instantiatedTypes = previous.instantiatedTypes;
this.instantiatedLambdas = previous.instantiatedLambdas;
this.targetedMethods = previous.targetedMethods;
this.liveMethods = previous.liveMethods;
this.liveFields = previous.liveFields;
this.instanceFieldReads = previous.instanceFieldReads;
this.instanceFieldWrites = previous.instanceFieldWrites;
this.staticFieldReads = previous.staticFieldReads;
this.staticFieldWrites = previous.staticFieldWrites;
this.fieldsRead = previous.fieldsRead;
this.fieldsWritten = previous.fieldsWritten;
this.pinnedItems = previous.pinnedItems;
this.noSideEffects = previous.noSideEffects;
this.assumedValues = previous.assumedValues;
this.virtualInvokes = previous.virtualInvokes;
this.interfaceInvokes = previous.interfaceInvokes;
this.superInvokes = previous.superInvokes;
this.directInvokes = previous.directInvokes;
this.staticInvokes = previous.staticInvokes;
this.protoLiteFields = previous.protoLiteFields;
this.alwaysInline = previous.alwaysInline;
this.identifierNameStrings = previous.identifierNameStrings;
this.prunedTypes = previous.prunedTypes;
this.switchMaps = switchMaps;
this.ordinalsMaps = ordinalsMaps;
}
public Reference2IntMap<DexField> getOrdinalsMapFor(DexType enumClass) {
return ordinalsMaps.get(enumClass);
}
public Int2ReferenceMap<DexField> getSwitchMapFor(DexField field) {
return switchMaps.get(field);
}
private boolean assertNoItemRemoved(Collection<DexItem> items, Collection<DexType> types) {
Set<DexType> typeSet = ImmutableSet.copyOf(types);
for (DexItem item : items) {
if (item instanceof DexType) {
assert !typeSet.contains(item);
} else if (item instanceof DexMethod) {
assert !typeSet.contains(((DexMethod) item).getHolder());
} else if (item instanceof DexField) {
assert !typeSet.contains(((DexField) item).getHolder());
} else {
assert false;
}
}
return true;
}
private boolean assertNotModifiedByLense(Iterable<DexItem> items, GraphLense lense) {
for (DexItem item : items) {
if (item instanceof DexClass) {
DexType type = ((DexClass) item).type;
assert lense.lookupType(type) == type;
} else if (item instanceof DexEncodedMethod) {
DexEncodedMethod method = (DexEncodedMethod) item;
// We only allow changes to bridge methods, as these get retargeted even if they
// are kept.
assert method.accessFlags.isBridge()
|| lense.lookupMethod(method.method) == method.method;
} else if (item instanceof DexEncodedField) {
DexField field = ((DexEncodedField) item).field;
assert lense.lookupField(field) == field;
} else {
assert false;
}
}
return true;
}
private SortedSet<DexMethod> joinInvokedMethods(Map<DexType, Set<DexMethod>> invokes) {
return joinInvokedMethods(invokes, Function.identity());
}
private <T> SortedSet<DexMethod> joinInvokedMethods(Map<DexType, Set<T>> invokes,
Function<T, DexMethod> getter) {
return invokes.values().stream().flatMap(Set::stream).map(getter)
.collect(ImmutableSortedSet.toImmutableSortedSet(PresortedComparable::slowCompare));
}
private <T extends PresortedComparable<T>> SortedSet<T> toSortedDescriptorSet(
Set<? extends KeyedDexItem<T>> set) {
ImmutableSortedSet.Builder<T> builder =
new ImmutableSortedSet.Builder<>(PresortedComparable<T>::slowCompareTo);
for (KeyedDexItem<T> item : set) {
builder.add(item.getKey());
}
return builder.build();
}
private ImmutableSet<DexItem> rewritePinnedItemsToDescriptors(Collection<DexItem> source) {
ImmutableSet.Builder<DexItem> builder = ImmutableSet.builder();
for (DexItem item : source) {
// TODO(b/67934123) There should be a common interface to extract this information.
if (item instanceof DexClass) {
builder.add(((DexClass) item).type);
} else if (item instanceof DexEncodedMethod) {
builder.add(((DexEncodedMethod) item).method);
} else if (item instanceof DexEncodedField) {
builder.add(((DexEncodedField) item).field);
} else {
throw new Unreachable();
}
}
return builder.build();
}
private static ImmutableSortedSet<DexMethod> rewriteMethodsConservatively(
Set<DexMethod> original, GraphLense lense) {
ImmutableSortedSet.Builder<DexMethod> builder =
new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompare);
if (lense.isContextFreeForMethods()) {
for (DexMethod item : original) {
builder.add(lense.lookupMethod(item));
}
} else {
for (DexMethod item : original) {
// Avoid using lookupMethodInAllContexts when possible.
if (lense.isContextFreeForMethod(item)) {
builder.add(lense.lookupMethod(item));
} else {
// The lense is context sensitive, but we do not have the context here. Therefore, we
// conservatively look up the method in all contexts.
builder.addAll(lense.lookupMethodInAllContexts(item));
}
}
}
return builder.build();
}
private static <T extends PresortedComparable<T>> ImmutableSortedSet<T> rewriteItems(
Set<T> original, Function<T, T> rewrite) {
ImmutableSortedSet.Builder<T> builder =
new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompare);
for (T item : original) {
builder.add(rewrite.apply(item));
}
return builder.build();
}
private static <T extends PresortedComparable<T>, S> ImmutableMap<T, S> rewriteKeys(
Map<T, S> original, Function<T, T> rewrite) {
ImmutableMap.Builder<T, S> builder = new ImmutableMap.Builder<>();
for (T item : original.keySet()) {
builder.put(rewrite.apply(item), original.get(item));
}
return builder.build();
}
private static <T extends PresortedComparable<T>, S>
Map<T, Set<S>> rewriteKeysWhileMergingValues(
Map<T, Set<S>> original, Function<T, T> rewrite) {
Map<T, Set<S>> result = new IdentityHashMap<>();
for (T item : original.keySet()) {
T rewrittenKey = rewrite.apply(item);
result.computeIfAbsent(rewrittenKey, k -> Sets.newIdentityHashSet())
.addAll(original.get(item));
}
return Collections.unmodifiableMap(result);
}
private static ImmutableSet<DexItem> rewriteMixedItems(
Set<DexItem> original, GraphLense lense) {
ImmutableSet.Builder<DexItem> builder = ImmutableSet.builder();
for (DexItem item : original) {
// TODO(b/67934123) There should be a common interface to perform the dispatch.
if (item instanceof DexType) {
builder.add(lense.lookupType((DexType) item));
} else if (item instanceof DexMethod) {
builder.add(lense.lookupMethod((DexMethod) item));
} else if (item instanceof DexField) {
builder.add(lense.lookupField((DexField) item));
} else {
throw new Unreachable();
}
}
return builder.build();
}
private static <T> Set<T> mergeSets(Collection<T> first, Collection<T> second) {
ImmutableSet.Builder<T> builder = ImmutableSet.builder();
builder.addAll(first);
builder.addAll(second);
return builder.build();
}
@Override
public boolean hasLiveness() {
return true;
}
@Override
public AppInfoWithLiveness withLiveness() {
return this;
}
// TODO(b/67934123) Unify into one method,
public boolean isPinned(DexType item) {
return pinnedItems.contains(item);
}
// TODO(b/67934123) Unify into one method,
public boolean isPinned(DexMethod item) {
return pinnedItems.contains(item);
}
// TODO(b/67934123) Unify into one method,
public boolean isPinned(DexField item) {
return pinnedItems.contains(item);
}
public boolean isProtoLiteField(DexField field) {
return protoLiteFields.contains(field);
}
public Iterable<DexItem> getPinnedItems() {
return pinnedItems;
}
/**
* Returns a copy of this AppInfoWithLiveness where the set of classes is pruned using the given
* DexApplication object.
*/
public AppInfoWithLiveness prunedCopyFrom(DexApplication application,
Collection<DexType> removedClasses) {
return new AppInfoWithLiveness(this, application, removedClasses);
}
public AppInfoWithLiveness rewrittenWithLense(DirectMappedDexApplication application,
GraphLense lense) {
return new AppInfoWithLiveness(this, application, lense);
}
/**
* Returns true if the given type was part of the original program but has been removed during
* tree shaking.
*/
public boolean wasPruned(DexType type) {
return prunedTypes.contains(type);
}
public DexEncodedMethod lookup(Type type, DexMethod target, DexType invocationContext) {
DexType holder = target.getHolder();
if (!holder.isClassType()) {
return null;
}
switch (type) {
case VIRTUAL:
return lookupSingleVirtualTarget(target);
case INTERFACE:
return lookupSingleInterfaceTarget(target);
case DIRECT:
return lookupDirectTarget(target);
case STATIC:
return lookupStaticTarget(target);
case SUPER:
return lookupSuperTarget(target, invocationContext);
default:
return null;
}
}
/**
* For mapping invoke virtual instruction to single target method.
*/
public DexEncodedMethod lookupSingleVirtualTarget(DexMethod method) {
return lookupSingleVirtualTarget(method, method.holder);
}
public DexEncodedMethod lookupSingleVirtualTarget(
DexMethod method, DexType refinedReceiverType) {
// This implements the logic from
// https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html#jvms-6.5.invokevirtual
assert method != null;
assert refinedReceiverType.isSubtypeOf(method.holder, this);
DexClass holder = definitionFor(method.holder);
if (holder == null || holder.isLibraryClass() || holder.isInterface()) {
return null;
}
boolean refinedReceiverIsStrictSubType = refinedReceiverType != method.holder;
DexClass refinedHolder =
refinedReceiverIsStrictSubType ? definitionFor(refinedReceiverType) : holder;
assert refinedHolder != null;
assert !refinedHolder.isLibraryClass();
if (method.isSingleVirtualMethodCached(refinedReceiverType)) {
return method.getSingleVirtualMethodCache(refinedReceiverType);
}
// For kept types we cannot ensure a single target.
if (pinnedItems.contains(method.holder)) {
method.setSingleVirtualMethodCache(refinedReceiverType, null);
return null;
}
// First get the target for the holder type.
ResolutionResult topMethod = resolveMethod(method.holder, method);
// We might hit none or multiple targets. Both make this fail at runtime.
if (!topMethod.hasSingleTarget() || !topMethod.asSingleTarget().isVirtualMethod()) {
method.setSingleVirtualMethodCache(refinedReceiverType, null);
return null;
}
// Now, resolve the target with the refined receiver type.
if (refinedReceiverIsStrictSubType) {
topMethod = resolveMethod(refinedReceiverType, method);
}
DexEncodedMethod topSingleTarget = topMethod.asSingleTarget();
DexClass topHolder = definitionFor(topSingleTarget.method.holder);
// We need to know whether the top method is from an interface, as that would allow it to be
// shadowed by a default method from an interface further down.
boolean topIsFromInterface = topHolder.isInterface();
// Now look at all subtypes and search for overrides.
DexEncodedMethod result = findSingleTargetFromSubtypes(refinedReceiverType, method,
topSingleTarget, !refinedHolder.accessFlags.isAbstract(), topIsFromInterface);
// Map the failure case of SENTINEL to null.
result = result == DexEncodedMethod.SENTINEL ? null : result;
method.setSingleVirtualMethodCache(refinedReceiverType, result);
return result;
}
/**
* Computes which methods overriding <code>method</code> are visible for the subtypes of type.
* <p>
* <code>candidate</code> is the definition further up the hierarchy that is visible from the
* subtypes. If <code>candidateIsReachable</code> is true, the provided candidate is already a
* target for a type further up the chain, so anything found in subtypes is a conflict. If it is
* false, the target exists but is not reachable from a live type.
* <p>
* Returns <code>null</code> if the given type has no subtypes or all subtypes are abstract.
* Returns {@link DexEncodedMethod#SENTINEL} if multiple live overrides were found. Returns the
* single virtual target otherwise.
*/
private DexEncodedMethod findSingleTargetFromSubtypes(DexType type, DexMethod method,
DexEncodedMethod candidate,
boolean candidateIsReachable, boolean checkForInterfaceConflicts) {
// If the candidate is reachable, we already have a previous result.
DexEncodedMethod result = candidateIsReachable ? candidate : null;
if (pinnedItems.contains(type)) {
// For kept types we do not know all subtypes, so abort.
return DexEncodedMethod.SENTINEL;
}
for (DexType subtype : type.allExtendsSubtypes()) {
DexClass clazz = definitionFor(subtype);
DexEncodedMethod target = clazz.lookupMethod(method);
if (target != null) {
// We found a method on this class. If this class is not abstract it is a runtime
// reachable override and hence a conflict.
if (!clazz.accessFlags.isAbstract()) {
if (result != null && result != target) {
// We found a new target on this subtype that does not match the previous one. Fail.
return DexEncodedMethod.SENTINEL;
}
// Add the first or matching target.
result = target;
}
}
if (checkForInterfaceConflicts) {
// We have to check whether there are any default methods in implemented interfaces.
if (interfacesMayHaveDefaultFor(clazz.interfaces, method)) {
return DexEncodedMethod.SENTINEL;
}
}
DexEncodedMethod newCandidate = target == null ? candidate : target;
// If we have a new target and did not fail, it is not an override of a reachable method.
// Whether the target is actually reachable depends on whether this class is abstract.
// If we did not find a new target, the candidate is reachable if it was before, or if this
// class is not abstract.
boolean newCandidateIsReachable =
!clazz.accessFlags.isAbstract() || ((target == null) && candidateIsReachable);
DexEncodedMethod subtypeTarget = findSingleTargetFromSubtypes(subtype, method,
newCandidate,
newCandidateIsReachable, checkForInterfaceConflicts);
if (subtypeTarget != null) {
// We found a target in the subclasses. If we already have a different result, fail.
if (result != null && result != subtypeTarget) {
return DexEncodedMethod.SENTINEL;
}
// Remember this new result.
result = subtypeTarget;
}
}
return result;
}
/**
* Checks whether any interface in the given list or their super interfaces implement a default
* method.
* <p>
* This method is conservative for unknown interfaces and interfaces from the library.
*/
private boolean interfacesMayHaveDefaultFor(DexTypeList ifaces, DexMethod method) {
for (DexType iface : ifaces.values) {
DexClass clazz = definitionFor(iface);
if (clazz == null || clazz.isLibraryClass()) {
return true;
}
DexEncodedMethod candidate = clazz.lookupMethod(method);
if (candidate != null && !candidate.accessFlags.isAbstract()) {
return true;
}
if (interfacesMayHaveDefaultFor(clazz.interfaces, method)) {
return true;
}
}
return false;
}
public DexEncodedMethod lookupSingleInterfaceTarget(DexMethod method) {
return lookupSingleInterfaceTarget(method, method.holder);
}
public DexEncodedMethod lookupSingleInterfaceTarget(
DexMethod method, DexType refinedReceiverType) {
if (instantiatedLambdas.contains(method.holder)) {
return null;
}
DexClass holder = definitionFor(method.holder);
if ((holder == null) || holder.isLibraryClass() || !holder.accessFlags.isInterface()) {
return null;
}
// First check that there is a target for this invoke-interface to hit. If there is none,
// this will fail at runtime.
ResolutionResult topTarget = resolveMethodOnInterface(method.holder, method);
if (topTarget.asResultOfResolve() == null) {
return null;
}
// For kept types we cannot ensure a single target.
if (pinnedItems.contains(method.holder)) {
return null;
}
DexEncodedMethod result = null;
// The loop will ignore abstract classes that are not kept as they should not be a target
// at runtime.
Iterable<DexType> subTypesToExplore =
refinedReceiverType == method.holder
? subtypes(method.holder)
: Iterables.concat(
ImmutableList.of(refinedReceiverType), subtypes(refinedReceiverType));
for (DexType type : subTypesToExplore) {
if (pinnedItems.contains(type)) {
// For kept classes we cannot ensure a single target.
return null;
}
DexClass clazz = definitionFor(type);
if (clazz.isInterface()) {
// Default methods are looked up when looking at a specific subtype that does not
// override them, so we ignore interface methods here. Otherwise, we would look up
// default methods that are factually never used.
} else if (!clazz.accessFlags.isAbstract()) {
ResolutionResult resolutionResult = resolveMethodOnClass(type, method);
if (resolutionResult.hasSingleTarget()) {
if ((result != null) && (result != resolutionResult.asSingleTarget())) {
return null;
} else {
result = resolutionResult.asSingleTarget();
}
} else {
// This will fail at runtime.
return null;
}
}
}
return result == null || !result.isVirtualMethod() ? null : result;
}
public AppInfoWithLiveness addSwitchMaps(Map<DexField, Int2ReferenceMap<DexField>> switchMaps) {
assert this.switchMaps.isEmpty();
return new AppInfoWithLiveness(this, switchMaps, ordinalsMaps);
}
public AppInfoWithLiveness addEnumOrdinalMaps(
Map<DexType, Reference2IntMap<DexField>> ordinalsMaps) {
assert this.ordinalsMaps.isEmpty();
return new AppInfoWithLiveness(this, switchMaps, ordinalsMaps);
}
}
private static class SetWithReason<T> {
private final Set<T> items = Sets.newIdentityHashSet();
private final Map<T, KeepReason> reasons = Maps.newIdentityHashMap();
boolean add(T item, KeepReason reason) {
if (items.add(item)) {
reasons.put(item, reason);
return true;
}
return false;
}
boolean contains(T item) {
return items.contains(item);
}
Set<T> getItems() {
return ImmutableSet.copyOf(items);
}
Map<T, KeepReason> getReasons() {
return ImmutableMap.copyOf(reasons);
}
}
private static final class TargetWithContext<T extends Descriptor<?, T>> {
private final T target;
private final DexEncodedMethod context;
private TargetWithContext(T target, DexEncodedMethod context) {
this.target = target;
this.context = context;
}
public T getTarget() {
return target;
}
public DexEncodedMethod getContext() {
return context;
}
@Override
public int hashCode() {
return target.hashCode() * 31 + context.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TargetWithContext)) {
return false;
}
TargetWithContext other = (TargetWithContext) obj;
return (this.target == other.target) && (this.context == other.context);
}
}
private class AnnotationReferenceMarker implements IndexedItemCollection {
private final DexItem annotationHolder;
private final DexItemFactory dexItemFactory;
private AnnotationReferenceMarker(DexItem annotationHolder, DexItemFactory dexItemFactory) {
this.annotationHolder = annotationHolder;
this.dexItemFactory = dexItemFactory;
}
@Override
public boolean addClass(DexProgramClass dexProgramClass) {
return false;
}
@Override
public boolean addField(DexField field) {
DexClass holder = appInfo.definitionFor(field.clazz);
if (holder == null) {
return false;
}
DexEncodedField target = holder.lookupStaticField(field);
if (target != null) {
// There is no dispatch on annotations, so only keep what is directly referenced.
if (target.field == field) {
markStaticFieldAsLive(field, KeepReason.referencedInAnnotation(annotationHolder));
}
} else {
target = holder.lookupInstanceField(field);
// There is no dispatch on annotations, so only keep what is directly referenced.
if (target != null && target.field != field) {
markInstanceFieldAsReachable(field, KeepReason.referencedInAnnotation(annotationHolder));
}
}
return false;
}
@Override
public boolean addMethod(DexMethod method) {
DexClass holder = appInfo.definitionFor(method.holder);
if (holder == null) {
return false;
}
DexEncodedMethod target = holder.lookupDirectMethod(method);
if (target != null) {
// There is no dispatch on annotations, so only keep what is directly referenced.
if (target.method == method) {
markDirectStaticOrConstructorMethodAsLive(
target, KeepReason.referencedInAnnotation(annotationHolder));
}
} else {
target = holder.lookupVirtualMethod(method);
// There is no dispatch on annotations, so only keep what is directly referenced.
if (target != null && target.method == method) {
markMethodAsTargeted(target, KeepReason.referencedInAnnotation(annotationHolder));
}
}
return false;
}
@Override
public boolean addString(DexString string) {
return false;
}
@Override
public boolean addProto(DexProto proto) {
return false;
}
@Override
public boolean addCallSite(DexCallSite callSite) {
return false;
}
@Override
public boolean addMethodHandle(DexMethodHandle methodHandle) {
return false;
}
@Override
public boolean addType(DexType type) {
// Annotations can also contain the void type, which is not a class type, so filter it out
// here.
if (type != dexItemFactory.voidType) {
markTypeAsLive(type);
}
return false;
}
}
}