| // 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.graph.DexProgramClass.asProgramClassOrNull; |
| import static com.android.tools.r8.graph.FieldAccessInfoImpl.MISSING_FIELD_ACCESS_INFO; |
| import static com.android.tools.r8.naming.IdentifierNameStringUtils.identifyIdentifier; |
| import static com.android.tools.r8.naming.IdentifierNameStringUtils.isReflectionMethod; |
| import static com.android.tools.r8.shaking.AnnotationRemover.shouldKeepAnnotation; |
| import static com.android.tools.r8.shaking.EnqueuerUtils.toImmutableSortedMap; |
| |
| import com.android.tools.r8.Diagnostic; |
| import com.android.tools.r8.cf.code.CfFieldInstruction; |
| import com.android.tools.r8.cf.code.CfInstruction; |
| import com.android.tools.r8.cf.code.CfInvoke; |
| import com.android.tools.r8.cf.code.CfInvokeDynamic; |
| import com.android.tools.r8.cf.code.CfLoad; |
| import com.android.tools.r8.cf.code.CfNew; |
| import com.android.tools.r8.cf.code.CfStackInstruction; |
| import com.android.tools.r8.cf.code.CfStackInstruction.Opcode; |
| import com.android.tools.r8.cf.code.CfStore; |
| import com.android.tools.r8.dex.IndexedItemCollection; |
| import com.android.tools.r8.errors.Unreachable; |
| import com.android.tools.r8.experimental.graphinfo.GraphConsumer; |
| import com.android.tools.r8.graph.AppInfoWithClassHierarchy; |
| import com.android.tools.r8.graph.AppInfoWithSubtyping; |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.CfCode; |
| import com.android.tools.r8.graph.DexAnnotation; |
| import com.android.tools.r8.graph.DexAnnotationSet; |
| 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.DexClassAndMethod; |
| import com.android.tools.r8.graph.DexClasspathClass; |
| import com.android.tools.r8.graph.DexDefinition; |
| import com.android.tools.r8.graph.DexEncodedField; |
| import com.android.tools.r8.graph.DexEncodedMember; |
| 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.DexItemFactory; |
| import com.android.tools.r8.graph.DexLibraryClass; |
| import com.android.tools.r8.graph.DexMember; |
| 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.DexReference; |
| import com.android.tools.r8.graph.DexString; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.DirectMappedDexApplication; |
| import com.android.tools.r8.graph.DirectMappedDexApplication.Builder; |
| import com.android.tools.r8.graph.EnumValueInfoMapCollection; |
| import com.android.tools.r8.graph.FieldAccessInfoCollectionImpl; |
| import com.android.tools.r8.graph.FieldAccessInfoImpl; |
| import com.android.tools.r8.graph.InnerClassAttribute; |
| import com.android.tools.r8.graph.LookupLambdaTarget; |
| import com.android.tools.r8.graph.LookupTarget; |
| import com.android.tools.r8.graph.ObjectAllocationInfoCollectionImpl; |
| import com.android.tools.r8.graph.PresortedComparable; |
| import com.android.tools.r8.graph.ProgramMethod; |
| import com.android.tools.r8.graph.ResolutionResult; |
| import com.android.tools.r8.graph.ResolutionResult.FailedResolutionResult; |
| import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult; |
| import com.android.tools.r8.graph.UseRegistry.MethodHandleUse; |
| import com.android.tools.r8.graph.analysis.DesugaredLibraryConversionWrapperAnalysis; |
| import com.android.tools.r8.graph.analysis.EnqueuerAnalysis; |
| import com.android.tools.r8.graph.analysis.EnqueuerInvokeAnalysis; |
| import com.android.tools.r8.ir.analysis.proto.ProtoEnqueuerUseRegistry; |
| import com.android.tools.r8.ir.analysis.proto.schema.ProtoEnqueuerExtension; |
| import com.android.tools.r8.ir.code.ArrayPut; |
| import com.android.tools.r8.ir.code.ConstantValueUtils; |
| import com.android.tools.r8.ir.code.IRCode; |
| import com.android.tools.r8.ir.code.Instruction; |
| import com.android.tools.r8.ir.code.InstructionIterator; |
| import com.android.tools.r8.ir.code.InvokeMethod; |
| import com.android.tools.r8.ir.code.InvokeVirtual; |
| import com.android.tools.r8.ir.code.Value; |
| import com.android.tools.r8.ir.code.ValueType; |
| import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter; |
| import com.android.tools.r8.ir.desugar.LambdaClass; |
| import com.android.tools.r8.ir.desugar.LambdaDescriptor; |
| import com.android.tools.r8.ir.desugar.LambdaRewriter; |
| import com.android.tools.r8.logging.Log; |
| import com.android.tools.r8.origin.Origin; |
| import com.android.tools.r8.shaking.DelayedRootSetActionItem.InterfaceMethodSyntheticBridgeAction; |
| import com.android.tools.r8.shaking.EnqueuerWorklist.EnqueuerAction; |
| import com.android.tools.r8.shaking.GraphReporter.KeepReasonWitness; |
| import com.android.tools.r8.shaking.RootSetBuilder.ConsequentRootSet; |
| import com.android.tools.r8.shaking.RootSetBuilder.RootSet; |
| import com.android.tools.r8.shaking.ScopedDexMethodSet.AddMethodIfMoreVisibleResult; |
| import com.android.tools.r8.utils.Action; |
| import com.android.tools.r8.utils.DequeUtils; |
| import com.android.tools.r8.utils.InternalOptions; |
| import com.android.tools.r8.utils.InternalOptions.DesugarState; |
| import com.android.tools.r8.utils.MethodSignatureEquivalence; |
| import com.android.tools.r8.utils.OptionalBool; |
| import com.android.tools.r8.utils.Pair; |
| import com.android.tools.r8.utils.SetUtils; |
| import com.android.tools.r8.utils.StringDiagnostic; |
| import com.android.tools.r8.utils.Timing; |
| import com.android.tools.r8.utils.Visibility; |
| import com.android.tools.r8.utils.WorkList; |
| import com.google.common.base.Equivalence.Wrapper; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.google.common.collect.Sets.SetView; |
| import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap; |
| import it.unimi.dsi.fastutil.objects.Object2BooleanMap; |
| import java.lang.reflect.InvocationHandler; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Deque; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.SortedSet; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.function.BiConsumer; |
| import java.util.function.Function; |
| import java.util.function.Predicate; |
| import org.objectweb.asm.Opcodes; |
| |
| /** |
| * 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 { |
| |
| public enum Mode { |
| INITIAL_TREE_SHAKING, |
| FINAL_TREE_SHAKING, |
| MAIN_DEX_TRACING, |
| WHY_ARE_YOU_KEEPING; |
| |
| public boolean isInitialTreeShaking() { |
| return this == INITIAL_TREE_SHAKING; |
| } |
| |
| public boolean isFinalTreeShaking() { |
| return this == FINAL_TREE_SHAKING; |
| } |
| |
| public boolean isInitialOrFinalTreeShaking() { |
| return isInitialTreeShaking() || isFinalTreeShaking(); |
| } |
| |
| public boolean isTracingMainDex() { |
| return this == MAIN_DEX_TRACING; |
| } |
| } |
| |
| private final boolean forceProguardCompatibility; |
| private final Mode mode; |
| |
| private Set<EnqueuerAnalysis> analyses = Sets.newIdentityHashSet(); |
| private Set<EnqueuerInvokeAnalysis> invokeAnalyses = Sets.newIdentityHashSet(); |
| |
| // Don't hold a direct pointer to app info (use appView). |
| private AppInfoWithSubtyping appInfo; |
| private final AppView<AppInfoWithSubtyping> appView; |
| private final InternalOptions options; |
| private RootSet rootSet; |
| private ProguardClassFilter dontWarnPatterns; |
| private final EnqueuerUseRegistryFactory useRegistryFactory; |
| private AnnotationRemover.Builder annotationRemoverBuilder; |
| |
| private final Map<DexMethod, Set<DexEncodedMethod>> virtualInvokes = new IdentityHashMap<>(); |
| private final Map<DexMethod, Set<DexEncodedMethod>> interfaceInvokes = new IdentityHashMap<>(); |
| private final Map<DexMethod, Set<DexEncodedMethod>> superInvokes = new IdentityHashMap<>(); |
| private final Map<DexMethod, Set<DexEncodedMethod>> directInvokes = new IdentityHashMap<>(); |
| private final Map<DexMethod, Set<DexEncodedMethod>> staticInvokes = new IdentityHashMap<>(); |
| private final FieldAccessInfoCollectionImpl fieldAccessInfoCollection = |
| new FieldAccessInfoCollectionImpl(); |
| private final ObjectAllocationInfoCollectionImpl.Builder objectAllocationInfoCollection; |
| private final Set<DexCallSite> callSites = Sets.newIdentityHashSet(); |
| |
| private final Set<DexReference> identifierNameStrings = Sets.newIdentityHashSet(); |
| |
| /** |
| * 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<DexProgramClass, 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 SetWithReportedReason<DexProgramClass> liveTypes; |
| |
| /** Set of types whose class initializer may execute. */ |
| private final SetWithReportedReason<DexProgramClass> initializedTypes; |
| |
| /** |
| * Set of live types defined in the library and classpath. |
| * |
| * <p>Used to build a new app of just referenced types and avoid duplicate tracing. |
| */ |
| private final Set<DexClass> liveNonProgramTypes = Sets.newIdentityHashSet(); |
| |
| /** Set of reachable proto types that will be dead code eliminated. */ |
| private final Set<DexProgramClass> deadProtoTypeCandidates = Sets.newIdentityHashSet(); |
| |
| /** Set of missing types. */ |
| private final Set<DexType> missingTypes = Sets.newIdentityHashSet(); |
| |
| /** Mapping from each unused interface to the set of live types that implements the interface. */ |
| private final Map<DexProgramClass, Set<DexProgramClass>> unusedInterfaceTypes = |
| new IdentityHashMap<>(); |
| |
| /** |
| * 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; |
| |
| /** Set of methods that have invalid resolutions or lookups. */ |
| private final Set<DexMethod> failedResolutionTargets; |
| |
| /** |
| * Set of program methods that are used as the bootstrap method for an invoke-dynamic instruction. |
| */ |
| private final Set<DexMethod> bootstrapMethods = Sets.newIdentityHashSet(); |
| /** |
| * Set of direct methods that are the immediate target of an invoke-dynamic. |
| */ |
| private final Set<DexMethod> methodsTargetedByInvokeDynamic = Sets.newIdentityHashSet(); |
| /** Set of direct lambda implementation methods that have been desugared, thus they may move. */ |
| private final Set<DexMethod> desugaredLambdaImplementationMethods = Sets.newIdentityHashSet(); |
| /** |
| * Set of virtual methods that are the immediate target of an invoke-direct. |
| */ |
| private final Set<DexMethod> virtualMethodsTargetedByInvokeDirect = Sets.newIdentityHashSet(); |
| /** |
| * Set of methods that belong to live classes and can be reached by invokes. These need to be |
| * kept. |
| */ |
| private final LiveMethodsSet liveMethods; |
| |
| /** |
| * Set of fields that belong to live classes and can be reached by invokes. These need to be kept. |
| */ |
| private final SetWithReason<DexEncodedField> liveFields; |
| |
| /** |
| * Set of service types (from META-INF/services/) that may have been instantiated reflectively via |
| * ServiceLoader.load() or ServiceLoader.loadInstalled(). |
| */ |
| private final Set<DexType> instantiatedAppServices = Sets.newIdentityHashSet(); |
| |
| /** A queue of items that need processing. Different items trigger different actions. */ |
| private final EnqueuerWorklist workList; |
| |
| /** |
| * A set of methods that need code inspection for Java reflection in use. |
| */ |
| private final Set<DexEncodedMethod> pendingReflectiveUses = Sets.newLinkedHashSet(); |
| |
| /** Mapping of types to the methods reachable at that type. */ |
| private final Map<DexProgramClass, Set<DexMethod>> reachableVirtualTargets = |
| new IdentityHashMap<>(); |
| |
| /** |
| * A set of references we have reported missing to dedupe warnings. |
| */ |
| private final Set<DexReference> reportedMissing = Sets.newIdentityHashSet(); |
| |
| /** |
| * A set of references that we are keeping due to keep rules. This may differ from the root set |
| * due to dependent keep rules. |
| */ |
| private final Set<DexReference> pinnedItems = Sets.newIdentityHashSet(); |
| |
| /** |
| * A set of seen const-class references that both serve as an initial lock-candidate set and will |
| * prevent statically merging the classes referenced. |
| */ |
| private final Set<DexType> constClassReferences = Sets.newIdentityHashSet(); |
| |
| /** |
| * A map from seen init-class references to the minimum required visibility of the corresponding |
| * static field. |
| */ |
| private final Map<DexType, Visibility> initClassReferences = new IdentityHashMap<>(); |
| |
| /** |
| * A map from annotation classes to annotations that need to be processed should the classes ever |
| * become live. |
| */ |
| private final Map<DexType, Set<DexAnnotation>> deferredAnnotations = new IdentityHashMap<>(); |
| |
| /** Map of active if rules to speed up aapt2 generated keep rules. */ |
| private Map<Wrapper<ProguardIfRule>, Set<ProguardIfRule>> activeIfRules; |
| |
| /** |
| * A cache of ScopedDexMethodSet for each live type used for determining that virtual methods that |
| * cannot be removed because they are widening access for another virtual method defined earlier |
| * in the type hierarchy. See b/136698023 for more information. |
| */ |
| private final Map<DexType, ScopedDexMethodSet> scopedMethodsForLiveTypes = |
| new IdentityHashMap<>(); |
| |
| private final GraphReporter graphReporter; |
| |
| private final LambdaRewriter lambdaRewriter; |
| private final DesugaredLibraryConversionWrapperAnalysis desugaredLibraryWrapperAnalysis; |
| private final Map<DexType, Pair<LambdaClass, DexEncodedMethod>> lambdaClasses = |
| new IdentityHashMap<>(); |
| private final Map<DexEncodedMethod, Map<DexCallSite, LambdaClass>> lambdaCallSites = |
| new IdentityHashMap<>(); |
| private final Set<DexProgramClass> classesWithSerializableLambdas = Sets.newIdentityHashSet(); |
| |
| Enqueuer( |
| AppView<? extends AppInfoWithSubtyping> appView, |
| GraphConsumer keptGraphConsumer, |
| Mode mode) { |
| assert appView.appServices() != null; |
| InternalOptions options = appView.options(); |
| this.appInfo = appView.appInfo(); |
| this.appView = appView.withSubtyping(); |
| this.forceProguardCompatibility = options.forceProguardCompatibility; |
| this.graphReporter = new GraphReporter(appView, keptGraphConsumer); |
| this.mode = mode; |
| this.options = options; |
| this.useRegistryFactory = createUseRegistryFactory(); |
| this.workList = EnqueuerWorklist.createWorklist(appView); |
| |
| if (options.protoShrinking().enableGeneratedMessageLiteShrinking |
| && mode.isInitialOrFinalTreeShaking()) { |
| registerAnalysis(new ProtoEnqueuerExtension(appView)); |
| } |
| |
| liveTypes = new SetWithReportedReason<>(); |
| initializedTypes = new SetWithReportedReason<>(); |
| targetedMethods = new SetWithReason<>(graphReporter::registerMethod); |
| // This set is only populated in edge cases due to multiple default interface methods. |
| // The set is generally expected to be empty and in the unlikely chance it is not, it will |
| // likely contain two methods. Thus the default capacity of 2. |
| failedResolutionTargets = SetUtils.newIdentityHashSet(2); |
| liveMethods = new LiveMethodsSet(graphReporter::registerMethod); |
| liveFields = new SetWithReason<>(graphReporter::registerField); |
| lambdaRewriter = options.desugarState == DesugarState.ON ? new LambdaRewriter(appView) : null; |
| |
| objectAllocationInfoCollection = |
| ObjectAllocationInfoCollectionImpl.builder(mode.isInitialTreeShaking(), graphReporter); |
| |
| if (appView.rewritePrefix.isRewriting() && mode.isInitialTreeShaking()) { |
| desugaredLibraryWrapperAnalysis = new DesugaredLibraryConversionWrapperAnalysis(appView); |
| registerAnalysis(desugaredLibraryWrapperAnalysis); |
| registerInvokeAnalysis(desugaredLibraryWrapperAnalysis); |
| } else { |
| desugaredLibraryWrapperAnalysis = null; |
| } |
| } |
| |
| private AppInfoWithClassHierarchy appInfo() { |
| return appView.appInfo(); |
| } |
| |
| public Mode getMode() { |
| return mode; |
| } |
| |
| public GraphReporter getGraphReporter() { |
| return graphReporter; |
| } |
| |
| private EnqueuerUseRegistryFactory createUseRegistryFactory() { |
| if (mode.isFinalTreeShaking()) { |
| return appView.withGeneratedMessageLiteShrinker( |
| ignore -> ProtoEnqueuerUseRegistry.getFactory(), DefaultEnqueuerUseRegistry::new); |
| } |
| return DefaultEnqueuerUseRegistry::new; |
| } |
| |
| public EnqueuerUseRegistryFactory getUseRegistryFactory() { |
| return useRegistryFactory; |
| } |
| |
| public Enqueuer registerAnalysis(EnqueuerAnalysis analysis) { |
| analyses.add(analysis); |
| return this; |
| } |
| |
| private Enqueuer registerInvokeAnalysis(EnqueuerInvokeAnalysis analysis) { |
| invokeAnalyses.add(analysis); |
| return this; |
| } |
| |
| public void setAnnotationRemoverBuilder(AnnotationRemover.Builder annotationRemoverBuilder) { |
| this.annotationRemoverBuilder = annotationRemoverBuilder; |
| } |
| |
| public void addDeadProtoTypeCandidate(DexType type) { |
| assert type.isProgramType(appView); |
| addDeadProtoTypeCandidate(appView.definitionFor(type).asProgramClass()); |
| } |
| |
| public void addDeadProtoTypeCandidate(DexProgramClass clazz) { |
| deadProtoTypeCandidates.add(clazz); |
| } |
| |
| private boolean isProgramClass(DexType type) { |
| return getProgramClassOrNull(type) != null; |
| } |
| |
| private void recordReference(DexReference r) { |
| if (r.isDexType()) { |
| recordTypeReference(r.asDexType()); |
| } else if (r.isDexField()) { |
| recordFieldReference(r.asDexField()); |
| } else { |
| assert r.isDexMethod(); |
| recordMethodReference(r.asDexMethod()); |
| } |
| } |
| |
| private void recordTypeReference(DexType type) { |
| if (type == null) { |
| return; |
| } |
| if (type.isArrayType()) { |
| type = type.toBaseType(appView.dexItemFactory()); |
| } |
| if (!type.isClassType()) { |
| return; |
| } |
| // Lookup the definition, ignoring the result. This populates the missing and referenced sets. |
| definitionFor(type); |
| } |
| |
| private void recordMethodReference(DexMethod method) { |
| recordTypeReference(method.holder); |
| recordTypeReference(method.proto.returnType); |
| for (DexType type : method.proto.parameters.values) { |
| recordTypeReference(type); |
| } |
| } |
| |
| private void recordFieldReference(DexField field) { |
| recordTypeReference(field.holder); |
| recordTypeReference(field.type); |
| } |
| |
| private DexClass definitionFor(DexType type) { |
| DexClass clazz = appView.definitionFor(type); |
| if (clazz == null) { |
| reportMissingClass(type); |
| return null; |
| } |
| if (clazz.isNotProgramClass()) { |
| addLiveNonProgramType(clazz); |
| } |
| return clazz; |
| } |
| |
| private void addLiveNonProgramType(DexClass clazz) { |
| assert clazz.isNotProgramClass(); |
| // Fast path to avoid the worklist when the class is already seen. |
| if (!liveNonProgramTypes.add(clazz)) { |
| return; |
| } |
| Deque<DexClass> worklist = new ArrayDeque<>(); |
| worklist.addLast(clazz); |
| while (!worklist.isEmpty()) { |
| DexClass definition = worklist.removeFirst(); |
| processNewLiveNonProgramType(definition, worklist); |
| } |
| } |
| |
| private void processNewLiveNonProgramType(DexClass clazz, Deque<DexClass> worklist) { |
| assert clazz.isNotProgramClass(); |
| if (clazz.isLibraryClass()) { |
| // TODO(b/149201735): This likely needs to apply to classpath too. |
| ensureMethodsContinueToWidenAccess(clazz); |
| // Only libraries must not derive program. Classpath classes can, assuming correct keep rules. |
| warnIfLibraryTypeInheritsFromProgramType(clazz.asLibraryClass()); |
| } |
| for (DexEncodedField field : clazz.fields()) { |
| addNonProgramClassToWorklist(field.field.type, worklist); |
| } |
| for (DexEncodedMethod method : clazz.methods()) { |
| addNonProgramClassToWorklist(method.method.proto.returnType, worklist); |
| for (DexType param : method.method.proto.parameters.values) { |
| addNonProgramClassToWorklist(param, worklist); |
| } |
| } |
| for (DexType supertype : clazz.allImmediateSupertypes()) { |
| addNonProgramClassToWorklist(supertype, worklist); |
| } |
| } |
| |
| private void addNonProgramClassToWorklist(DexType type, Deque<DexClass> worklist) { |
| if (type.isArrayType()) { |
| type = type.toBaseType(appView.dexItemFactory()); |
| } |
| if (!type.isClassType()) { |
| return; |
| } |
| DexClass definition = appView.definitionFor(type); |
| if (definition == null) { |
| reportMissingClass(type); |
| return; |
| } |
| if (definition.isProgramClass() || !liveNonProgramTypes.add(definition)) { |
| return; |
| } |
| worklist.addLast(definition); |
| } |
| |
| private DexProgramClass getProgramClassOrNull(DexType type) { |
| DexClass clazz = definitionFor(type); |
| return clazz != null && clazz.isProgramClass() ? clazz.asProgramClass() : null; |
| } |
| |
| private void warnIfLibraryTypeInheritsFromProgramType(DexLibraryClass clazz) { |
| if (clazz.superType != null) { |
| ensureFromLibraryOrThrow(clazz.superType, clazz); |
| } |
| for (DexType iface : clazz.interfaces.values) { |
| ensureFromLibraryOrThrow(iface, clazz); |
| } |
| } |
| |
| private static <T> SetWithReason<T> newSetWithoutReasonReporter() { |
| return new SetWithReason<>((f, r) -> {}); |
| } |
| |
| private void enqueueRootItems(Map<DexReference, Set<ProguardKeepRuleBase>> items) { |
| items.entrySet().forEach(this::enqueueRootItem); |
| } |
| |
| private void enqueueRootItem(Entry<DexReference, Set<ProguardKeepRuleBase>> root) { |
| DexDefinition item = appView.definitionFor(root.getKey()); |
| if (item != null) { |
| enqueueRootItem(item, root.getValue()); |
| } else { |
| // TODO(b/123923324): Verify that root items are present. |
| // assert false : "Expected root item `" + root.getKey().toSourceString() + "` to be present"; |
| } |
| } |
| |
| private void enqueueRootItem(DexDefinition item, Set<ProguardKeepRuleBase> rules) { |
| internalEnqueueRootItem(item, rules, null); |
| } |
| |
| private void internalEnqueueRootItem( |
| DexDefinition item, Set<ProguardKeepRuleBase> rules, DexDefinition precondition) { |
| if (item.isDexClass()) { |
| DexProgramClass clazz = item.asDexClass().asProgramClass(); |
| KeepReasonWitness witness = graphReporter.reportKeepClass(precondition, rules, clazz); |
| if (clazz.isAnnotation()) { |
| workList.enqueueMarkAnnotationInstantiatedAction(clazz, witness); |
| } else if (clazz.isInterface()) { |
| workList.enqueueMarkInterfaceInstantiatedAction(clazz, witness); |
| } else { |
| workList.enqueueMarkInstantiatedAction(clazz, null, InstantiationReason.KEEP_RULE, witness); |
| if (clazz.hasDefaultInitializer()) { |
| DexEncodedMethod defaultInitializer = clazz.getDefaultInitializer(); |
| if (forceProguardCompatibility) { |
| workList.enqueueMarkMethodKeptAction( |
| clazz, |
| defaultInitializer, |
| graphReporter.reportCompatKeepDefaultInitializer(clazz, defaultInitializer)); |
| } |
| if (clazz.isExternalizable(appView)) { |
| enqueueMarkMethodLiveAction(clazz, defaultInitializer, witness); |
| } |
| } |
| } |
| } else if (item.isDexEncodedField()) { |
| DexEncodedField dexEncodedField = item.asDexEncodedField(); |
| DexProgramClass holder = getProgramClassOrNull(dexEncodedField.holder()); |
| if (holder != null) { |
| workList.enqueueMarkFieldKeptAction( |
| holder, |
| dexEncodedField, |
| graphReporter.reportKeepField(precondition, rules, dexEncodedField)); |
| } |
| } else if (item.isDexEncodedMethod()) { |
| DexEncodedMethod encodedMethod = item.asDexEncodedMethod(); |
| DexProgramClass holder = getProgramClassOrNull(encodedMethod.holder()); |
| if (holder != null) { |
| workList.enqueueMarkMethodKeptAction( |
| holder, |
| encodedMethod, |
| graphReporter.reportKeepMethod(precondition, rules, encodedMethod)); |
| } |
| } else { |
| throw new IllegalArgumentException(item.toString()); |
| } |
| pinnedItems.add(item.toReference()); |
| } |
| |
| private void enqueueFirstNonSerializableClassInitializer( |
| DexProgramClass clazz, KeepReason reason) { |
| assert clazz.isSerializable(appView); |
| // Climb up the class hierarchy. Break out if the definition is not found, or hit the library |
| // classes which are kept by definition, or encounter the first non-serializable class. |
| while (clazz.isSerializable(appView)) { |
| DexProgramClass superClass = getProgramClassOrNull(clazz.superType); |
| if (superClass == null) { |
| return; |
| } |
| clazz = superClass; |
| } |
| if (clazz.hasDefaultInitializer()) { |
| enqueueMarkMethodLiveAction(clazz, clazz.getDefaultInitializer(), reason); |
| } |
| } |
| |
| // Utility to avoid adding to the worklist if already live. |
| private boolean enqueueMarkMethodLiveAction( |
| DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) { |
| assert method.holder() == clazz.type; |
| if (liveMethods.add(clazz, method, reason)) { |
| workList.enqueueMarkMethodLiveAction(clazz, method, reason); |
| return true; |
| } |
| return false; |
| } |
| |
| private void compatEnqueueHolderIfDependentNonStaticMember( |
| DexClass holder, Set<ProguardKeepRuleBase> compatRules) { |
| if (!forceProguardCompatibility || compatRules == null) { |
| return; |
| } |
| enqueueRootItem(holder, compatRules); |
| } |
| |
| // |
| // Things to do with registering events. This is essentially the interface for byte-code |
| // traversals. |
| // |
| |
| private boolean registerMethodWithTargetAndContext( |
| Map<DexMethod, Set<DexEncodedMethod>> seen, DexMethod method, DexEncodedMethod context) { |
| DexType baseHolder = method.holder.toBaseType(appView.dexItemFactory()); |
| if (baseHolder.isClassType()) { |
| markTypeAsLive(baseHolder, clazz -> graphReporter.reportClassReferencedFrom(clazz, context)); |
| return seen.computeIfAbsent(method, ignore -> Sets.newIdentityHashSet()).add(context); |
| } |
| return false; |
| } |
| |
| public boolean registerFieldRead(DexField field, DexEncodedMethod context) { |
| return registerFieldAccess(field, context, true, false); |
| } |
| |
| public boolean registerReflectiveFieldRead(DexField field, DexEncodedMethod context) { |
| return registerFieldAccess(field, context, true, true); |
| } |
| |
| public boolean registerFieldWrite(DexField field, DexEncodedMethod context) { |
| return registerFieldAccess(field, context, false, false); |
| } |
| |
| public boolean registerReflectiveFieldWrite(DexField field, DexEncodedMethod context) { |
| return registerFieldAccess(field, context, false, true); |
| } |
| |
| public boolean registerReflectiveFieldAccess(DexField field, DexEncodedMethod context) { |
| boolean changed = registerFieldAccess(field, context, true, true); |
| changed |= registerFieldAccess(field, context, false, true); |
| return changed; |
| } |
| |
| private boolean registerFieldAccess( |
| DexField field, DexEncodedMethod context, boolean isRead, boolean isReflective) { |
| FieldAccessInfoImpl info = fieldAccessInfoCollection.get(field); |
| if (info == null) { |
| DexEncodedField encodedField = resolveField(field); |
| |
| // If the field does not exist, then record this in the mapping, such that we don't have to |
| // resolve the field the next time. |
| if (encodedField == null) { |
| fieldAccessInfoCollection.extend(field, MISSING_FIELD_ACCESS_INFO); |
| return true; |
| } |
| |
| // Check if we have previously created a FieldAccessInfo object for the field definition. |
| info = fieldAccessInfoCollection.get(encodedField.field); |
| |
| // If not, we must create one. |
| if (info == null) { |
| info = new FieldAccessInfoImpl(encodedField.field); |
| fieldAccessInfoCollection.extend(encodedField.field, info); |
| } |
| |
| // If `field` is an indirect reference, then create a mapping for it, such that we don't have |
| // to resolve the field the next time we see the reference. |
| if (field != encodedField.field) { |
| fieldAccessInfoCollection.extend(field, info); |
| } |
| } else if (info == MISSING_FIELD_ACCESS_INFO) { |
| return false; |
| } |
| if (isReflective) { |
| info.setHasReflectiveAccess(); |
| } |
| return isRead ? info.recordRead(field, context) : info.recordWrite(field, context); |
| } |
| |
| void traceCallSite(DexCallSite callSite, ProgramMethod context) { |
| DexProgramClass bootstrapClass = |
| getProgramClassOrNull(callSite.bootstrapMethod.asMethod().holder); |
| if (bootstrapClass != null) { |
| bootstrapMethods.add(callSite.bootstrapMethod.asMethod()); |
| } |
| |
| DexProgramClass contextHolder = context.getHolder(); |
| LambdaDescriptor descriptor = LambdaDescriptor.tryInfer(callSite, appInfo, contextHolder); |
| if (descriptor == null) { |
| if (!appInfo.isStringConcat(callSite.bootstrapMethod)) { |
| if (options.reporter != null) { |
| Diagnostic message = |
| new StringDiagnostic( |
| "Unknown bootstrap method " + callSite.bootstrapMethod, contextHolder.origin); |
| options.reporter.warning(message); |
| } |
| } |
| |
| callSites.add(callSite); |
| return; |
| } |
| |
| DexEncodedMethod contextMethod = context.getMethod(); |
| if (lambdaRewriter != null) { |
| assert contextMethod.getCode().isCfCode() : "Unexpected input type with lambdas"; |
| CfCode code = contextMethod.getCode().asCfCode(); |
| if (code != null) { |
| LambdaClass lambdaClass = |
| lambdaRewriter.getOrCreateLambdaClass(descriptor, contextMethod.holder()); |
| lambdaClasses.put(lambdaClass.type, new Pair<>(lambdaClass, contextMethod)); |
| lambdaCallSites |
| .computeIfAbsent(contextMethod, k -> new IdentityHashMap<>()) |
| .put(callSite, lambdaClass); |
| if (lambdaClass.descriptor.interfaces.contains(appView.dexItemFactory().serializableType)) { |
| classesWithSerializableLambdas.add(contextHolder); |
| } |
| } |
| if (descriptor.delegatesToLambdaImplMethod()) { |
| desugaredLambdaImplementationMethods.add(descriptor.implHandle.asMethod()); |
| } |
| } else { |
| markLambdaAsInstantiated(descriptor, contextMethod); |
| transitionMethodsForInstantiatedLambda(descriptor); |
| callSites.add(callSite); |
| } |
| |
| // For call sites representing a lambda, we link the targeted method |
| // or field as if it were referenced from the current method. |
| |
| DexMethodHandle implHandle = descriptor.implHandle; |
| assert implHandle != null; |
| |
| DexMethod method = implHandle.asMethod(); |
| if (!methodsTargetedByInvokeDynamic.add(method)) { |
| return; |
| } |
| |
| switch (implHandle.type) { |
| case INVOKE_STATIC: |
| traceInvokeStaticFromLambda(method, context); |
| break; |
| case INVOKE_INTERFACE: |
| traceInvokeInterfaceFromLambda(method, context); |
| break; |
| case INVOKE_INSTANCE: |
| traceInvokeVirtualFromLambda(method, context); |
| break; |
| case INVOKE_DIRECT: |
| traceInvokeDirectFromLambda(method, context); |
| break; |
| case INVOKE_CONSTRUCTOR: |
| traceNewInstanceFromLambda(method.holder, context); |
| break; |
| default: |
| throw new Unreachable(); |
| } |
| } |
| |
| boolean traceCheckCast(DexType type, DexEncodedMethod currentMethod) { |
| return traceConstClassOrCheckCast(type, currentMethod); |
| } |
| |
| boolean traceConstClass(DexType type, DexEncodedMethod currentMethod) { |
| // We conservatively group T.class and T[].class to ensure that we do not merge T with S if |
| // potential locks on T[].class and S[].class exists. |
| DexType baseType = type.toBaseType(appView.dexItemFactory()); |
| if (baseType.isClassType()) { |
| DexProgramClass baseClass = getProgramClassOrNull(baseType); |
| if (baseClass != null) { |
| constClassReferences.add(baseType); |
| } |
| } |
| return traceConstClassOrCheckCast(type, currentMethod); |
| } |
| |
| private boolean traceConstClassOrCheckCast(DexType type, DexEncodedMethod currentMethod) { |
| if (!forceProguardCompatibility) { |
| return traceTypeReference(type, currentMethod); |
| } |
| DexType baseType = type.toBaseType(appView.dexItemFactory()); |
| if (baseType.isClassType()) { |
| DexProgramClass baseClass = getProgramClassOrNull(baseType); |
| if (baseClass != null) { |
| // Don't require any constructor, see b/112386012. |
| markClassAsInstantiatedWithCompatRule( |
| baseClass, graphReporter.reportCompatInstantiated(baseClass, currentMethod)); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| boolean traceInitClass(DexType type, ProgramMethod currentMethod) { |
| assert type.isClassType(); |
| |
| Visibility oldMinimumRequiredVisibility = initClassReferences.get(type); |
| if (oldMinimumRequiredVisibility == null) { |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz == null) { |
| assert false; |
| return false; |
| } |
| |
| initClassReferences.put( |
| type, computeMinimumRequiredVisibilityForInitClassField(type, currentMethod.getHolder())); |
| |
| markTypeAsLive(type, classReferencedFromReporter(currentMethod.getMethod())); |
| markDirectAndIndirectClassInitializersAsLive(clazz); |
| return true; |
| } |
| |
| if (oldMinimumRequiredVisibility.isPublic()) { |
| return false; |
| } |
| |
| Visibility minimumRequiredVisibilityForCurrentMethod = |
| computeMinimumRequiredVisibilityForInitClassField(type, currentMethod.getHolder()); |
| |
| // There should never be a need to have an InitClass instruction for the enclosing class. |
| assert !minimumRequiredVisibilityForCurrentMethod.isPrivate(); |
| |
| if (minimumRequiredVisibilityForCurrentMethod.isPublic()) { |
| initClassReferences.put(type, minimumRequiredVisibilityForCurrentMethod); |
| return true; |
| } |
| |
| if (oldMinimumRequiredVisibility.isProtected()) { |
| return false; |
| } |
| |
| if (minimumRequiredVisibilityForCurrentMethod.isProtected()) { |
| initClassReferences.put(type, minimumRequiredVisibilityForCurrentMethod); |
| return true; |
| } |
| |
| assert oldMinimumRequiredVisibility.isPackagePrivate(); |
| assert minimumRequiredVisibilityForCurrentMethod.isPackagePrivate(); |
| return false; |
| } |
| |
| private Visibility computeMinimumRequiredVisibilityForInitClassField( |
| DexType clazz, DexProgramClass context) { |
| if (clazz.isSamePackage(context.type)) { |
| return Visibility.PACKAGE_PRIVATE; |
| } |
| if (appInfo.isStrictSubtypeOf(context.type, clazz)) { |
| return Visibility.PROTECTED; |
| } |
| return Visibility.PUBLIC; |
| } |
| |
| void traceMethodHandle( |
| DexMethodHandle methodHandle, MethodHandleUse use, DexEncodedMethod currentMethod) { |
| // If a method handle is not an argument to a lambda metafactory it could flow to a |
| // MethodHandle.invokeExact invocation. For that to work, the receiver type cannot have |
| // changed and therefore we cannot perform member rebinding. For these handles, we maintain |
| // the receiver for the method handle. Therefore, we have to make sure that the receiver |
| // stays in the output (and is not class merged). To ensure that we treat the receiver |
| // as instantiated. |
| if (methodHandle.isMethodHandle() && use != MethodHandleUse.ARGUMENT_TO_LAMBDA_METAFACTORY) { |
| DexType type = methodHandle.asMethod().holder; |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz != null) { |
| KeepReason reason = KeepReason.methodHandleReferencedIn(currentMethod); |
| if (clazz.isAnnotation()) { |
| markTypeAsLive(clazz, graphReporter.registerClass(clazz, reason)); |
| } else if (clazz.isInterface()) { |
| markInterfaceAsInstantiated(clazz, graphReporter.registerInterface(clazz, reason)); |
| } else { |
| workList.enqueueMarkInstantiatedAction( |
| clazz, null, InstantiationReason.REFERENCED_IN_METHOD_HANDLE, reason); |
| } |
| } |
| } |
| } |
| |
| boolean traceTypeReference(DexType type, DexEncodedMethod currentMethod) { |
| markTypeAsLive(type, classReferencedFromReporter(currentMethod)); |
| return true; |
| } |
| |
| boolean traceInvokeDirect(DexMethod invokedMethod, ProgramMethod context) { |
| DexProgramClass currentHolder = context.getHolder(); |
| DexEncodedMethod currentMethod = context.getMethod(); |
| boolean skipTracing = |
| registerDeferredActionForDeadProtoBuilder( |
| invokedMethod.holder, |
| currentMethod, |
| () -> |
| workList.enqueueTraceInvokeDirectAction( |
| invokedMethod, currentHolder, currentMethod)); |
| if (skipTracing) { |
| addDeadProtoTypeCandidate(invokedMethod.holder); |
| return false; |
| } |
| |
| return traceInvokeDirect( |
| invokedMethod, context, KeepReason.invokedFrom(currentHolder, currentMethod)); |
| } |
| |
| /** Returns true if a deferred action was registered. */ |
| private boolean registerDeferredActionForDeadProtoBuilder( |
| DexType type, DexEncodedMethod currentMethod, Action action) { |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz != null) { |
| return appView.withGeneratedMessageLiteBuilderShrinker( |
| shrinker -> |
| shrinker.deferDeadProtoBuilders( |
| clazz, |
| currentMethod, |
| () -> liveTypes.registerDeferredAction(clazz, action), |
| this), |
| false); |
| } |
| return false; |
| } |
| |
| boolean traceInvokeDirectFromLambda(DexMethod invokedMethod, ProgramMethod context) { |
| return traceInvokeDirect( |
| invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context.getMethod())); |
| } |
| |
| private boolean traceInvokeDirect( |
| DexMethod invokedMethod, ProgramMethod context, KeepReason reason) { |
| DexEncodedMethod currentMethod = context.getMethod(); |
| if (!registerMethodWithTargetAndContext(directInvokes, invokedMethod, currentMethod)) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeDirect `%s`.", invokedMethod); |
| } |
| handleInvokeOfDirectTarget(invokedMethod, reason); |
| invokeAnalyses.forEach(analysis -> analysis.traceInvokeDirect(invokedMethod, context)); |
| return true; |
| } |
| |
| boolean traceInvokeInterface(DexMethod invokedMethod, ProgramMethod context) { |
| return traceInvokeInterface(invokedMethod, context, KeepReason.invokedFrom(context)); |
| } |
| |
| boolean traceInvokeInterfaceFromLambda(DexMethod invokedMethod, ProgramMethod context) { |
| return traceInvokeInterface( |
| invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context.getMethod())); |
| } |
| |
| private boolean traceInvokeInterface( |
| DexMethod method, ProgramMethod context, KeepReason keepReason) { |
| DexEncodedMethod currentMethod = context.getMethod(); |
| if (!registerMethodWithTargetAndContext(interfaceInvokes, method, currentMethod)) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeInterface `%s`.", method); |
| } |
| markVirtualMethodAsReachable(method, true, context, keepReason); |
| invokeAnalyses.forEach(analysis -> analysis.traceInvokeInterface(method, context)); |
| return true; |
| } |
| |
| boolean traceInvokeStatic(DexMethod invokedMethod, ProgramMethod context) { |
| return traceInvokeStatic(invokedMethod, context, KeepReason.invokedFrom(context)); |
| } |
| |
| boolean traceInvokeStaticFromLambda(DexMethod invokedMethod, ProgramMethod context) { |
| return traceInvokeStatic( |
| invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context.getMethod())); |
| } |
| |
| private boolean traceInvokeStatic( |
| DexMethod invokedMethod, ProgramMethod context, KeepReason reason) { |
| DexEncodedMethod currentMethod = context.getMethod(); |
| DexItemFactory dexItemFactory = appView.dexItemFactory(); |
| if (dexItemFactory.classMethods.isReflectiveClassLookup(invokedMethod) |
| || dexItemFactory.atomicFieldUpdaterMethods.isFieldUpdater(invokedMethod)) { |
| // Implicitly add -identifiernamestring rule for the Java reflection in use. |
| identifierNameStrings.add(invokedMethod); |
| // Revisit the current method to implicitly add -keep rule for items with reflective access. |
| pendingReflectiveUses.add(currentMethod); |
| } |
| // See comment in handleJavaLangEnumValueOf. |
| if (invokedMethod == dexItemFactory.enumMethods.valueOf) { |
| pendingReflectiveUses.add(currentMethod); |
| } |
| // Handling of application services. |
| if (dexItemFactory.serviceLoaderMethods.isLoadMethod(invokedMethod)) { |
| pendingReflectiveUses.add(currentMethod); |
| } |
| if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) { |
| pendingReflectiveUses.add(currentMethod); |
| } |
| if (!registerMethodWithTargetAndContext(staticInvokes, invokedMethod, currentMethod)) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeStatic `%s`.", invokedMethod); |
| } |
| handleInvokeOfStaticTarget(invokedMethod, reason); |
| invokeAnalyses.forEach(analysis -> analysis.traceInvokeStatic(invokedMethod, context)); |
| return true; |
| } |
| |
| boolean traceInvokeSuper(DexMethod invokedMethod, ProgramMethod context) { |
| DexEncodedMethod currentMethod = context.getMethod(); |
| // 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(invokedMethod, currentMethod); |
| if (!registerMethodWithTargetAndContext(superInvokes, invokedMethod, currentMethod)) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeSuper `%s`.", actualTarget); |
| } |
| workList.enqueueMarkReachableSuperAction(invokedMethod, currentMethod); |
| invokeAnalyses.forEach(analysis -> analysis.traceInvokeSuper(invokedMethod, context)); |
| return true; |
| } |
| |
| boolean traceInvokeVirtual(DexMethod invokedMethod, ProgramMethod context) { |
| return traceInvokeVirtual(invokedMethod, context, KeepReason.invokedFrom(context)); |
| } |
| |
| boolean traceInvokeVirtualFromLambda(DexMethod invokedMethod, ProgramMethod context) { |
| return traceInvokeVirtual( |
| invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context.getMethod())); |
| } |
| |
| private boolean traceInvokeVirtual( |
| DexMethod invokedMethod, ProgramMethod context, KeepReason reason) { |
| if (invokedMethod == appView.dexItemFactory().classMethods.newInstance |
| || invokedMethod == appView.dexItemFactory().constructorMethods.newInstance) { |
| pendingReflectiveUses.add(context.getMethod()); |
| } else if (appView.dexItemFactory().classMethods.isReflectiveMemberLookup(invokedMethod)) { |
| // Implicitly add -identifiernamestring rule for the Java reflection in use. |
| identifierNameStrings.add(invokedMethod); |
| // Revisit the current method to implicitly add -keep rule for items with reflective access. |
| pendingReflectiveUses.add(context.getMethod()); |
| } |
| if (!registerMethodWithTargetAndContext(virtualInvokes, invokedMethod, context.getMethod())) { |
| return false; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register invokeVirtual `%s`.", invokedMethod); |
| } |
| markVirtualMethodAsReachable(invokedMethod, false, context, reason); |
| invokeAnalyses.forEach(analysis -> analysis.traceInvokeVirtual(invokedMethod, context)); |
| return true; |
| } |
| |
| boolean traceNewInstance(DexType type, ProgramMethod context) { |
| DexEncodedMethod currentMethod = context.getMethod(); |
| boolean skipTracing = |
| registerDeferredActionForDeadProtoBuilder( |
| type, currentMethod, () -> workList.enqueueTraceNewInstanceAction(type, context)); |
| if (skipTracing) { |
| addDeadProtoTypeCandidate(type); |
| return false; |
| } |
| |
| return traceNewInstance( |
| type, |
| context, |
| InstantiationReason.NEW_INSTANCE_INSTRUCTION, |
| KeepReason.instantiatedIn(currentMethod)); |
| } |
| |
| boolean traceNewInstanceFromLambda(DexType type, ProgramMethod context) { |
| return traceNewInstance( |
| type, |
| context, |
| InstantiationReason.LAMBDA, |
| KeepReason.invokedFromLambdaCreatedIn(context.getMethod())); |
| } |
| |
| private boolean traceNewInstance( |
| DexType type, |
| ProgramMethod context, |
| InstantiationReason instantiationReason, |
| KeepReason keepReason) { |
| DexEncodedMethod currentMethod = context.getMethod(); |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz != null) { |
| if (clazz.isAnnotation() || clazz.isInterface()) { |
| markTypeAsLive(clazz, graphReporter.registerClass(clazz, keepReason)); |
| } else { |
| workList.enqueueMarkInstantiatedAction( |
| clazz, currentMethod, instantiationReason, keepReason); |
| } |
| } |
| return true; |
| } |
| |
| boolean traceInstanceFieldRead(DexField field, DexEncodedMethod currentMethod) { |
| return traceInstanceFieldRead(field, currentMethod, false); |
| } |
| |
| boolean traceInstanceFieldReadFromMethodHandle(DexField field, DexEncodedMethod currentMethod) { |
| return traceInstanceFieldRead(field, currentMethod, true); |
| } |
| |
| private boolean traceInstanceFieldRead( |
| DexField field, DexEncodedMethod currentMethod, boolean fromMethodHandle) { |
| if (!registerFieldRead(field, currentMethod)) { |
| return false; |
| } |
| |
| // Must mark the field as targeted even if it does not exist. |
| markFieldAsTargeted(field, currentMethod); |
| |
| DexEncodedField encodedField = resolveField(field); |
| if (encodedField == null) { |
| return false; |
| } |
| |
| if (fromMethodHandle) { |
| fieldAccessInfoCollection.get(encodedField.field).setReadFromMethodHandle(); |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(encodedField.holder()); |
| if (clazz == null) { |
| return false; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register Iget `%s`.", field); |
| } |
| |
| // If unused interface removal is enabled, then we won't necessarily mark the actual holder of |
| // the field as live, if the holder is an interface. |
| if (appView.options().enableUnusedInterfaceRemoval) { |
| if (encodedField.field != field) { |
| markTypeAsLive(clazz, graphReporter.reportClassReferencedFrom(clazz, currentMethod)); |
| markTypeAsLive(encodedField.field.type, classReferencedFromReporter(currentMethod)); |
| } |
| } |
| |
| workList.enqueueMarkReachableFieldAction( |
| clazz, encodedField, KeepReason.fieldReferencedIn(currentMethod)); |
| return true; |
| } |
| |
| boolean traceInstanceFieldWrite(DexField field, DexEncodedMethod currentMethod) { |
| return traceInstanceFieldWrite(field, currentMethod, false); |
| } |
| |
| boolean traceInstanceFieldWriteFromMethodHandle(DexField field, DexEncodedMethod currentMethod) { |
| return traceInstanceFieldWrite(field, currentMethod, true); |
| } |
| |
| private boolean traceInstanceFieldWrite( |
| DexField field, DexEncodedMethod currentMethod, boolean fromMethodHandle) { |
| if (!registerFieldWrite(field, currentMethod)) { |
| return false; |
| } |
| |
| // Must mark the field as targeted even if it does not exist. |
| markFieldAsTargeted(field, currentMethod); |
| |
| DexEncodedField encodedField = resolveField(field); |
| if (encodedField == null) { |
| return false; |
| } |
| |
| if (fromMethodHandle) { |
| fieldAccessInfoCollection.get(encodedField.field).setWrittenFromMethodHandle(); |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(encodedField.holder()); |
| if (clazz == null) { |
| return false; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register Iput `%s`.", field); |
| } |
| |
| // If unused interface removal is enabled, then we won't necessarily mark the actual holder of |
| // the field as live, if the holder is an interface. |
| if (appView.options().enableUnusedInterfaceRemoval) { |
| if (encodedField.field != field) { |
| markTypeAsLive(clazz, graphReporter.reportClassReferencedFrom(clazz, currentMethod)); |
| markTypeAsLive(encodedField.field.type, classReferencedFromReporter(currentMethod)); |
| } |
| } |
| |
| KeepReason reason = KeepReason.fieldReferencedIn(currentMethod); |
| workList.enqueueMarkReachableFieldAction(clazz, encodedField, reason); |
| return true; |
| } |
| |
| boolean traceStaticFieldRead(DexField field, DexEncodedMethod currentMethod) { |
| return traceStaticFieldRead(field, currentMethod, false); |
| } |
| |
| boolean traceStaticFieldReadFromMethodHandle(DexField field, DexEncodedMethod currentMethod) { |
| return traceStaticFieldRead(field, currentMethod, true); |
| } |
| |
| private boolean traceStaticFieldRead( |
| DexField field, DexEncodedMethod currentMethod, boolean fromMethodHandle) { |
| if (!registerFieldRead(field, currentMethod)) { |
| return false; |
| } |
| |
| DexEncodedField encodedField = resolveField(field); |
| if (encodedField == null) { |
| // Must mark the field as targeted even if it does not exist. |
| markFieldAsTargeted(field, currentMethod); |
| return false; |
| } |
| |
| if (fromMethodHandle) { |
| fieldAccessInfoCollection.get(encodedField.field).setReadFromMethodHandle(); |
| } |
| |
| DexProgramClass holder = getProgramClassOrNull(encodedField.holder()); |
| if (holder == null) { |
| // No need to trace into the non-program code. |
| return false; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register Sget `%s`.", field); |
| } |
| |
| if (appView.options().protoShrinking().enableGeneratedExtensionRegistryShrinking) { |
| // If it is a dead proto extension field, don't trace onwards. |
| boolean skipTracing = |
| appView.withGeneratedExtensionRegistryShrinker( |
| shrinker -> |
| shrinker.isDeadProtoExtensionField( |
| encodedField, fieldAccessInfoCollection, pinnedItems), |
| false); |
| if (skipTracing) { |
| addDeadProtoTypeCandidate(holder); |
| return false; |
| } |
| } |
| |
| if (encodedField.field != field) { |
| // Mark the non-rebound field access as targeted. Note that this should only be done if the |
| // field is not a dead proto field (in which case we bail-out above). |
| markFieldAsTargeted(field, currentMethod); |
| } |
| |
| markStaticFieldAsLive(encodedField, KeepReason.fieldReferencedIn(currentMethod)); |
| return true; |
| } |
| |
| boolean traceStaticFieldWrite(DexField field, DexEncodedMethod currentMethod) { |
| return traceStaticFieldWrite(field, currentMethod, false); |
| } |
| |
| boolean traceStaticFieldWriteFromMethodHandle(DexField field, DexEncodedMethod currentMethod) { |
| return traceStaticFieldWrite(field, currentMethod, true); |
| } |
| |
| private boolean traceStaticFieldWrite( |
| DexField field, DexEncodedMethod currentMethod, boolean fromMethodHandle) { |
| if (!registerFieldWrite(field, currentMethod)) { |
| return false; |
| } |
| |
| DexEncodedField encodedField = resolveField(field); |
| if (encodedField == null) { |
| // Must mark the field as targeted even if it does not exist. |
| markFieldAsTargeted(field, currentMethod); |
| return false; |
| } |
| |
| if (fromMethodHandle) { |
| fieldAccessInfoCollection.get(encodedField.field).setWrittenFromMethodHandle(); |
| } |
| |
| DexProgramClass holder = getProgramClassOrNull(encodedField.holder()); |
| if (holder == null) { |
| // No need to trace into the non-program code. |
| return false; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Register Sput `%s`.", field); |
| } |
| |
| if (appView.options().protoShrinking().enableGeneratedExtensionRegistryShrinking) { |
| // If it is a dead proto extension field, don't trace onwards. |
| boolean skipTracing = |
| appView.withGeneratedExtensionRegistryShrinker( |
| shrinker -> |
| shrinker.isDeadProtoExtensionField( |
| encodedField, fieldAccessInfoCollection, pinnedItems), |
| false); |
| if (skipTracing) { |
| addDeadProtoTypeCandidate(holder); |
| return false; |
| } |
| } |
| |
| if (encodedField.field != field) { |
| // Mark the non-rebound field access as targeted. Note that this should only be done if the |
| // field is not a dead proto field (in which case we bail-out above). |
| markFieldAsTargeted(field, currentMethod); |
| } |
| |
| markStaticFieldAsLive(encodedField, KeepReason.fieldReferencedIn(currentMethod)); |
| return true; |
| } |
| |
| private Function<DexProgramClass, KeepReasonWitness> classReferencedFromReporter( |
| DexEncodedMethod currentMethod) { |
| return clazz -> graphReporter.reportClassReferencedFrom(clazz, currentMethod); |
| } |
| |
| private DexMethod getInvokeSuperTarget(DexMethod method, DexEncodedMethod currentMethod) { |
| DexClass methodHolderClass = appView.definitionFor(method.holder); |
| if (methodHolderClass != null && methodHolderClass.isInterface()) { |
| return method; |
| } |
| DexClass holderClass = appView.definitionFor(currentMethod.holder()); |
| if (holderClass == null || holderClass.superType == null || holderClass.isInterface()) { |
| // We do not know better or this call is made from an interface. |
| return method; |
| } |
| // Return the invoked method on the supertype. |
| return appView.dexItemFactory().createMethod(holderClass.superType, method.proto, method.name); |
| } |
| |
| // |
| // Actual actions performed. |
| // |
| |
| private boolean verifyMethodIsTargeted(DexEncodedMethod method) { |
| assert !method.isClassInitializer() : "Class initializers are never targeted"; |
| assert targetedMethods.contains(method); |
| return true; |
| } |
| |
| private boolean verifyTypeIsLive(DexProgramClass clazz) { |
| assert liveTypes.contains(clazz); |
| return true; |
| } |
| |
| private void markTypeAsLive(DexType type, KeepReason reason) { |
| if (type.isArrayType()) { |
| markTypeAsLive(type.toBaseType(appView.dexItemFactory()), reason); |
| return; |
| } |
| if (!type.isClassType()) { |
| // Ignore primitive types. |
| return; |
| } |
| DexProgramClass holder = getProgramClassOrNull(type); |
| if (holder == null) { |
| return; |
| } |
| markTypeAsLive( |
| holder, |
| scopedMethodsForLiveTypes.computeIfAbsent(type, ignore -> new ScopedDexMethodSet()), |
| graphReporter.registerClass(holder, reason)); |
| } |
| |
| private void markTypeAsLive(DexType type, Function<DexProgramClass, KeepReasonWitness> reason) { |
| if (type.isArrayType()) { |
| markTypeAsLive(type.toBaseType(appView.dexItemFactory()), reason); |
| return; |
| } |
| if (!type.isClassType()) { |
| // Ignore primitive types. |
| return; |
| } |
| DexProgramClass holder = getProgramClassOrNull(type); |
| if (holder == null) { |
| return; |
| } |
| markTypeAsLive( |
| holder, |
| scopedMethodsForLiveTypes.computeIfAbsent(type, ignore -> new ScopedDexMethodSet()), |
| reason.apply(holder)); |
| } |
| |
| void markTypeAsLive(DexProgramClass clazz, KeepReasonWitness witness) { |
| markTypeAsLive( |
| clazz, |
| scopedMethodsForLiveTypes.computeIfAbsent(clazz.type, ignore -> new ScopedDexMethodSet()), |
| witness); |
| } |
| |
| private void markTypeAsLive( |
| DexProgramClass holder, ScopedDexMethodSet seen, KeepReasonWitness witness) { |
| if (!liveTypes.add(holder, witness)) { |
| return; |
| } |
| |
| // Mark types in inner-class attributes referenced. |
| for (InnerClassAttribute innerClassAttribute : holder.getInnerClasses()) { |
| recordTypeReference(innerClassAttribute.getInner()); |
| recordTypeReference(innerClassAttribute.getOuter()); |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Type `%s` has become live.", holder.type); |
| } |
| |
| KeepReason reason = KeepReason.reachableFromLiveType(holder.type); |
| |
| for (DexType iface : holder.interfaces.values) { |
| markInterfaceTypeAsLiveViaInheritanceClause(iface, holder); |
| } |
| |
| if (holder.superType != null) { |
| ScopedDexMethodSet seenForSuper = |
| scopedMethodsForLiveTypes.computeIfAbsent( |
| holder.superType, ignore -> new ScopedDexMethodSet()); |
| seen.setParent(seenForSuper); |
| markTypeAsLive(holder.superType, reason); |
| } |
| |
| // If this is an interface that has just become live, then report previously seen but unreported |
| // implemented-by edges. |
| transitionUnusedInterfaceToLive(holder); |
| |
| // We cannot remove virtual methods defined earlier in the type hierarchy if it is widening |
| // access and is defined in an interface: |
| // |
| // public interface I { |
| // void clone(); |
| // } |
| // |
| // class Model implements I { |
| // public void clone() { ... } <-- this cannot be removed |
| // } |
| // |
| // Any class loading of Model with Model.clone() removed will result in an illegal access |
| // error because their exists an existing implementation (here it is Object.clone()). This is |
| // only a problem in the DEX VM. We have to make this check no matter the output because |
| // CF libraries can be used by Android apps. See b/136698023 for more information. |
| ensureMethodsContinueToWidenAccess(holder, seen, reason); |
| |
| if (holder.isSerializable(appView)) { |
| enqueueFirstNonSerializableClassInitializer(holder, reason); |
| } |
| |
| processAnnotations(holder, holder); |
| |
| // If this type has deferred annotations, we have to process those now, too. |
| if (holder.isAnnotation()) { |
| Set<DexAnnotation> annotations = deferredAnnotations.remove(holder.type); |
| if (annotations != null && !annotations.isEmpty()) { |
| assert annotations.stream().allMatch(a -> a.annotation.type == holder.type); |
| annotations.forEach(annotation -> processAnnotation(holder, holder, annotation)); |
| } |
| } |
| |
| rootSet.forEachDependentInstanceConstructor( |
| holder, appView, this::enqueueHolderWithDependentInstanceConstructor); |
| rootSet.forEachDependentStaticMember(holder, appView, this::enqueueDependentItem); |
| compatEnqueueHolderIfDependentNonStaticMember( |
| holder, rootSet.getDependentKeepClassCompatRule(holder.getType())); |
| |
| analyses.forEach(analysis -> analysis.processNewlyLiveClass(holder, workList)); |
| } |
| |
| private void ensureMethodsContinueToWidenAccess(DexClass clazz) { |
| assert !clazz.isProgramClass(); |
| ScopedDexMethodSet seen = |
| scopedMethodsForLiveTypes.computeIfAbsent(clazz.type, ignore -> new ScopedDexMethodSet()); |
| clazz.virtualMethods().forEach(seen::addMethodIfMoreVisible); |
| } |
| |
| private void ensureMethodsContinueToWidenAccess( |
| DexProgramClass clazz, ScopedDexMethodSet seen, KeepReason reason) { |
| for (DexEncodedMethod method : clazz.virtualMethods()) { |
| if (seen.addMethodIfMoreVisible(method) == AddMethodIfMoreVisibleResult.ADDED_MORE_VISIBLE |
| && clazz.isProgramClass() |
| && appView.appInfo().methodDefinedInInterfaces(method, clazz.type)) { |
| markMethodAsTargeted(clazz, method, reason); |
| } |
| } |
| } |
| |
| private void markInterfaceTypeAsLiveViaInheritanceClause( |
| DexType type, DexProgramClass implementer) { |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz == null) { |
| return; |
| } |
| |
| if (!appView.options().enableUnusedInterfaceRemoval || mode.isTracingMainDex()) { |
| markTypeAsLive(clazz, graphReporter.reportClassReferencedFrom(clazz, implementer)); |
| } else { |
| if (liveTypes.contains(clazz)) { |
| // The interface is already live, so make sure to report this implements-edge. |
| graphReporter.reportClassReferencedFrom(clazz, implementer); |
| } else { |
| // No need to mark the type as live. If an interface type is only reachable via the |
| // inheritance clause of another type it can simply be removed from the inheritance clause. |
| // The interface is needed if it has a live default interface method or field, though. |
| // Therefore, we record that this implemented-by edge has not been reported, such that we |
| // can report it in the future if one its members becomes live. |
| WorkList<DexProgramClass> worklist = WorkList.newIdentityWorkList(); |
| worklist.addIfNotSeen(clazz); |
| while (worklist.hasNext()) { |
| DexProgramClass current = worklist.next(); |
| if (liveTypes.contains(current)) { |
| continue; |
| } |
| Set<DexProgramClass> implementors = |
| unusedInterfaceTypes.computeIfAbsent(current, ignore -> Sets.newIdentityHashSet()); |
| if (implementors.add(implementer)) { |
| for (DexType iface : current.interfaces.values) { |
| DexProgramClass definition = getProgramClassOrNull(iface); |
| if (definition != null) { |
| worklist.addIfNotSeen(definition); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private void enqueueDependentItem( |
| DexDefinition precondition, DexDefinition consequent, Set<ProguardKeepRuleBase> reasons) { |
| internalEnqueueRootItem(consequent, reasons, precondition); |
| } |
| |
| private void enqueueHolderWithDependentInstanceConstructor( |
| DexProgramClass clazz, |
| DexEncodedMethod instanceInitializer, |
| Set<ProguardKeepRuleBase> reasons) { |
| enqueueRootItem(clazz, reasons); |
| } |
| |
| private void processAnnotations(DexProgramClass holder, DexDefinition annotatedItem) { |
| processAnnotations(holder, annotatedItem, annotatedItem.annotations()); |
| } |
| |
| private void processAnnotations( |
| DexProgramClass holder, DexDefinition annotatedItem, DexAnnotationSet annotations) { |
| processAnnotations(holder, annotatedItem, annotations.annotations); |
| } |
| |
| private void processAnnotations( |
| DexProgramClass holder, DexDefinition annotatedItem, DexAnnotation[] annotations) { |
| for (DexAnnotation annotation : annotations) { |
| processAnnotation(holder, annotatedItem, annotation); |
| } |
| } |
| |
| private void processAnnotation( |
| DexProgramClass holder, DexDefinition annotatedItem, DexAnnotation annotation) { |
| assert annotatedItem == holder |
| || annotatedItem.asDexEncodedMember().toReference().holder == holder.type; |
| assert !holder.isDexClass() || holder.asDexClass().isProgramClass(); |
| DexType type = annotation.annotation.type; |
| recordTypeReference(type); |
| DexClass clazz = appView.definitionFor(type); |
| boolean annotationTypeIsLibraryClass = clazz == null || clazz.isNotProgramClass(); |
| boolean isLive = annotationTypeIsLibraryClass || liveTypes.contains(clazz.asProgramClass()); |
| if (!shouldKeepAnnotation(appView, annotatedItem, annotation, isLive)) { |
| // Remember this annotation for later. |
| if (!annotationTypeIsLibraryClass) { |
| deferredAnnotations.computeIfAbsent(type, ignore -> new HashSet<>()).add(annotation); |
| } |
| return; |
| } |
| KeepReason reason = KeepReason.annotatedOn(annotatedItem); |
| graphReporter.registerAnnotation(annotation, reason); |
| AnnotationReferenceMarker referenceMarker = |
| new AnnotationReferenceMarker(annotation.annotation.type, appView.dexItemFactory(), reason); |
| annotation.annotation.collectIndexedItems(referenceMarker); |
| } |
| |
| private DexEncodedField resolveField(DexField field) { |
| // Record the references in case they are not program types. |
| recordTypeReference(field.holder); |
| recordTypeReference(field.type); |
| DexEncodedField encodedField = appInfo.resolveField(field); |
| if (encodedField == null) { |
| reportMissingField(field); |
| return null; |
| } |
| return encodedField; |
| } |
| |
| private SingleResolutionResult resolveMethod(DexMethod method, KeepReason reason) { |
| // Record the references in case they are not program types. |
| recordMethodReference(method); |
| ResolutionResult resolutionResult = appInfo.resolveMethod(method.holder, method); |
| if (resolutionResult.isFailedResolution()) { |
| reportMissingMethod(method); |
| markFailedResolutionTargets(method, resolutionResult.asFailedResolution(), reason); |
| } |
| return resolutionResult.asSingleResolution(); |
| } |
| |
| private SingleResolutionResult resolveMethod( |
| DexMethod method, KeepReason reason, boolean interfaceInvoke) { |
| // Record the references in case they are not program types. |
| recordMethodReference(method); |
| ResolutionResult resolutionResult = |
| appInfo.resolveMethod(method.holder, method, interfaceInvoke); |
| if (resolutionResult.isFailedResolution()) { |
| reportMissingMethod(method); |
| markFailedResolutionTargets(method, resolutionResult.asFailedResolution(), reason); |
| } |
| return resolutionResult.asSingleResolution(); |
| } |
| |
| private void handleInvokeOfStaticTarget(DexMethod method, KeepReason reason) { |
| SingleResolutionResult resolution = resolveMethod(method, reason); |
| if (resolution == null || resolution.getResolvedHolder().isNotProgramClass()) { |
| return; |
| } |
| DexProgramClass clazz = resolution.getResolvedHolder().asProgramClass(); |
| DexEncodedMethod encodedMethod = resolution.getResolvedMethod(); |
| |
| // 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. |
| markMethodAsTargeted(clazz, encodedMethod, reason); |
| |
| // Only mark methods for which invocation will succeed at runtime live. |
| if (encodedMethod.isStatic()) { |
| markDirectAndIndirectClassInitializersAsLive(clazz); |
| markDirectStaticOrConstructorMethodAsLive(clazz, encodedMethod, reason); |
| } |
| } |
| |
| private void markDirectAndIndirectClassInitializersAsLive(DexProgramClass clazz) { |
| Deque<DexProgramClass> worklist = DequeUtils.newArrayDeque(clazz); |
| Set<DexProgramClass> visited = SetUtils.newIdentityHashSet(clazz); |
| while (!worklist.isEmpty()) { |
| DexProgramClass current = worklist.removeFirst(); |
| assert visited.contains(current); |
| |
| if (!markDirectClassInitializerAsLive(current)) { |
| continue; |
| } |
| |
| // Mark all class initializers in all super types as live. |
| for (DexType superType : clazz.allImmediateSupertypes()) { |
| DexProgramClass superClass = getProgramClassOrNull(superType); |
| if (superClass != null && visited.add(superClass)) { |
| worklist.add(superClass); |
| } |
| } |
| } |
| } |
| |
| /** Returns true if the class initializer became live for the first time. */ |
| private boolean markDirectClassInitializerAsLive(DexProgramClass clazz) { |
| DexEncodedMethod clinit = clazz.getClassInitializer(); |
| KeepReasonWitness witness = graphReporter.reportReachableClassInitializer(clazz, clinit); |
| if (!initializedTypes.add(clazz, witness)) { |
| return false; |
| } |
| if (clinit != null && clinit.getOptimizationInfo().mayHaveSideEffects()) { |
| markDirectStaticOrConstructorMethodAsLive(clazz, clinit, witness); |
| } |
| return true; |
| } |
| |
| // Package protected due to entry point from worklist. |
| void markNonStaticDirectMethodAsReachable(DexMethod method, KeepReason reason) { |
| handleInvokeOfDirectTarget(method, reason); |
| } |
| |
| private void handleInvokeOfDirectTarget(DexMethod method, KeepReason reason) { |
| DexType holder = method.holder; |
| DexProgramClass clazz = getProgramClassOrNull(holder); |
| if (clazz == null) { |
| recordMethodReference(method); |
| return; |
| } |
| // TODO(zerny): Is it ok that we lookup in both the direct and virtual pool here? |
| DexEncodedMethod encodedMethod = clazz.lookupMethod(method); |
| if (encodedMethod == null) { |
| reportMissingMethod(method); |
| return; |
| } |
| |
| // 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. |
| markMethodAsTargeted(clazz, encodedMethod, reason); |
| |
| // Only mark methods for which invocation will succeed at runtime live. |
| if (encodedMethod.isStatic()) { |
| return; |
| } |
| |
| markDirectStaticOrConstructorMethodAsLive(clazz, encodedMethod, reason); |
| |
| // It is valid to have an invoke-direct instruction in a default interface method that |
| // targets another default method in the same interface (see testInvokeSpecialToDefault- |
| // Method). In a class, that would lead to a verification error. |
| if (encodedMethod.isNonPrivateVirtualMethod() |
| && virtualMethodsTargetedByInvokeDirect.add(encodedMethod.method)) { |
| enqueueMarkMethodLiveAction(clazz, encodedMethod, reason); |
| } |
| } |
| |
| private void ensureFromLibraryOrThrow(DexType type, DexClass context) { |
| if (!mode.isInitialTreeShaking()) { |
| // 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 = appView.definitionFor(type); |
| if (holder != null && !holder.isLibraryClass()) { |
| if (forceProguardCompatibility) { |
| // To ensure that the program works correctly we have to pin all super types and members |
| // in the tree. |
| appInfo.forEachSuperType( |
| holder, |
| (dexType, ignored) -> { |
| if (holder.isProgramClass()) { |
| DexReference holderReference = holder.toReference(); |
| pinnedItems.add(holderReference); |
| rootSet.shouldNotBeMinified(holderReference); |
| for (DexEncodedMember<?, ?> member : holder.members()) { |
| DexMember<?, ?> memberReference = member.toReference(); |
| pinnedItems.add(memberReference); |
| rootSet.shouldNotBeMinified(memberReference); |
| } |
| } |
| }); |
| } |
| if (dontWarnPatterns.matches(context.type)) { |
| // Ignore. |
| return; |
| } |
| Diagnostic message = |
| new StringDiagnostic( |
| "Library class " |
| + context.type.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) { |
| boolean newReport = missingTypes.add(clazz); |
| if (Log.ENABLED && newReport) { |
| 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( |
| DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) { |
| assert method.holder() == clazz.type; |
| if (!targetedMethods.add(method, reason)) { |
| // Already targeted. |
| return; |
| } |
| markReferencedTypesAsLive(method); |
| processAnnotations(clazz, method); |
| method.parameterAnnotationsList.forEachAnnotation( |
| annotation -> processAnnotation(clazz, method, annotation)); |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Method `%s` is targeted.", method.method); |
| } |
| 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. |
| if (!method.accessFlags.isAbstract() && clazz.isInterface()) { |
| markMethodAsLiveWithCompatRule(clazz, method); |
| } |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| // Package protected due to entry point from worklist. |
| void processNewlyInstantiatedClass( |
| DexProgramClass clazz, |
| DexEncodedMethod context, |
| InstantiationReason instantiationReason, |
| KeepReason keepReason) { |
| assert !clazz.isAnnotation(); |
| assert !clazz.isInterface(); |
| |
| // Notify analyses. This is done even if `clazz` has already been marked as instantiated, |
| // because each analysis may depend on seeing all the (clazz, reason) pairs. Thus, not doing so |
| // could lead to nondeterminism. |
| analyses.forEach( |
| analysis -> analysis.processNewlyInstantiatedClass(clazz.asProgramClass(), context)); |
| |
| if (!markInstantiatedClass(clazz, context, instantiationReason, keepReason)) { |
| return; |
| } |
| |
| 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, graphReporter.registerClass(clazz, keepReason)); |
| // Instantiation triggers class initialization. |
| markDirectAndIndirectClassInitializersAsLive(clazz); |
| // 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); |
| // For all instance fields visible from the class, mark them live if we have seen a read. |
| transitionFieldsForInstantiatedClass(clazz); |
| // Add all dependent instance members to the workqueue. |
| transitionDependentItemsForInstantiatedClass(clazz); |
| } |
| |
| // TODO(b/146016987): Make this the single instantiation entry rather than the worklist action. |
| private boolean markInstantiatedClass( |
| DexProgramClass clazz, |
| DexEncodedMethod context, |
| InstantiationReason instantiationReason, |
| KeepReason keepReason) { |
| assert !clazz.isInterface(); |
| return objectAllocationInfoCollection.recordDirectAllocationSite( |
| clazz, context, instantiationReason, keepReason, appInfo); |
| } |
| |
| void markInterfaceAsInstantiated(DexProgramClass clazz, KeepReasonWitness witness) { |
| assert !clazz.isAnnotation(); |
| assert clazz.isInterface(); |
| if (!objectAllocationInfoCollection.recordInstantiatedInterface(clazz, appInfo)) { |
| return; |
| } |
| markTypeAsLive(clazz, witness); |
| transitionDependentItemsForInstantiatedInterface(clazz); |
| } |
| |
| private void markLambdaAsInstantiated(LambdaDescriptor descriptor, DexEncodedMethod context) { |
| // Each descriptor is unique, so there is no check for already marking the lambda. |
| for (DexType iface : descriptor.interfaces) { |
| checkLambdaInterface(iface, context); |
| objectAllocationInfoCollection.recordInstantiatedLambdaInterface(iface, descriptor, appInfo); |
| // TODO(b/150277553): Lambdas should be accurately traced and thus not be added here. |
| assert lambdaRewriter == null; |
| DexProgramClass clazz = getProgramClassOrNull(iface); |
| if (clazz != null) { |
| objectAllocationInfoCollection.recordInstantiatedInterface(clazz, appInfo); |
| } |
| } |
| } |
| |
| private void checkLambdaInterface(DexType itf, DexEncodedMethod context) { |
| DexClass clazz = definitionFor(itf); |
| if (clazz == null) { |
| StringDiagnostic message = |
| new StringDiagnostic( |
| "Lambda expression implements missing interface `" + itf.toSourceString() + "`", |
| appInfo.originFor(context.holder())); |
| options.reporter.warning(message); |
| } else if (!clazz.isInterface()) { |
| StringDiagnostic message = |
| new StringDiagnostic( |
| "Lambda expression expected to implement an interface, but found " |
| + "`" |
| + itf.toSourceString() |
| + "`", |
| appInfo.originFor(context.holder())); |
| options.reporter.warning(message); |
| } |
| } |
| |
| private void transitionMethodsForInstantiatedLambda(LambdaDescriptor lambda) { |
| transitionMethodsForInstantiatedObject( |
| InstantiatedObject.of(lambda), |
| definitionFor(appInfo.dexItemFactory().objectType), |
| lambda.interfaces); |
| } |
| |
| private void transitionMethodsForInstantiatedClass(DexProgramClass clazz) { |
| assert !clazz.isAnnotation(); |
| assert !clazz.isInterface(); |
| transitionMethodsForInstantiatedObject( |
| InstantiatedObject.of(clazz), clazz, Collections.emptyList()); |
| } |
| |
| /** |
| * Marks all methods live that are overrides of reachable methods for a given instantiation. |
| * |
| * <p>Only reachable methods in the hierarchy of the given instantiation and above are considered, |
| * and only the lowest such reachable target (ie, mirroring resolution). All library and classpath |
| * methods are considered reachable. |
| */ |
| private void transitionMethodsForInstantiatedObject( |
| InstantiatedObject instantiation, DexClass clazz, List<DexType> interfaces) { |
| Set<Wrapper<DexMethod>> seen = new HashSet<>(); |
| WorkList<DexType> worklist = WorkList.newIdentityWorkList(); |
| worklist.addIfNotSeen(interfaces); |
| // First we lookup and mark all targets on the instantiated class for each reachable method in |
| // the super chain (inclusive). |
| while (clazz != null) { |
| if (clazz.isProgramClass()) { |
| markProgramMethodOverridesAsLive(instantiation, clazz.asProgramClass(), seen); |
| } else { |
| markLibraryAndClasspathMethodOverridesAsLive(instantiation, clazz); |
| } |
| worklist.addIfNotSeen(Arrays.asList(clazz.interfaces.values)); |
| clazz = clazz.superType != null ? definitionFor(clazz.superType) : null; |
| } |
| // The targets for methods on the type and its supertype that are reachable are now marked. |
| // In a second step, we look at interfaces. We order the search this way such that a |
| // method reachable on a class takes precedence when reporting edges. That order mirrors JVM |
| // resolution/dispatch. |
| while (worklist.hasNext()) { |
| DexType type = worklist.next(); |
| DexClass iface = definitionFor(type); |
| if (iface == null) { |
| continue; |
| } |
| assert iface.superType == appInfo.dexItemFactory().objectType; |
| if (iface.isNotProgramClass()) { |
| markLibraryAndClasspathMethodOverridesAsLive(instantiation, iface); |
| } else { |
| markProgramMethodOverridesAsLive(instantiation, iface.asProgramClass(), seen); |
| } |
| worklist.addIfNotSeen(Arrays.asList(iface.interfaces.values)); |
| } |
| } |
| |
| private Set<DexMethod> getReachableVirtualTargets(DexProgramClass clazz) { |
| return reachableVirtualTargets.getOrDefault(clazz, Collections.emptySet()); |
| } |
| |
| private void markProgramMethodOverridesAsLive( |
| InstantiatedObject instantiation, |
| DexProgramClass superClass, |
| Set<Wrapper<DexMethod>> seenMethods) { |
| for (DexMethod method : getReachableVirtualTargets(superClass)) { |
| assert method.holder == superClass.type; |
| if (seenMethods.add(MethodSignatureEquivalence.get().wrap(method))) { |
| SingleResolutionResult resolution = |
| appInfo.resolveMethod(superClass, method).asSingleResolution(); |
| assert resolution != null; |
| assert resolution.getResolvedHolder().isProgramClass(); |
| if (resolution != null && resolution.getResolvedHolder().isProgramClass()) { |
| markLiveOverrides( |
| instantiation, superClass, resolution.getResolutionPair().asProgramMethod()); |
| } |
| } |
| } |
| } |
| |
| private void markLiveOverrides( |
| InstantiatedObject instantiation, |
| DexProgramClass initialHolder, |
| ProgramMethod resolutionMethod) { |
| // The validity of the reachable method is checked at the point it becomes "reachable" and is |
| // resolved. If the method is private, then the dispatch is not "virtual" and the method is |
| // simply marked live on its holder. |
| if (resolutionMethod.getMethod().isPrivateMethod()) { |
| markVirtualMethodAsLive( |
| resolutionMethod.getHolder(), |
| resolutionMethod.getMethod(), |
| graphReporter.reportReachableMethodAsLive( |
| resolutionMethod.getMethod().method, resolutionMethod)); |
| return; |
| } |
| // Otherwise, we set the initial holder type to be the holder of the reachable method, which |
| // ensures that access will be generally valid. |
| SingleResolutionResult resolution = |
| new SingleResolutionResult( |
| initialHolder, resolutionMethod.getHolder(), resolutionMethod.getMethod()); |
| LookupTarget lookup = resolution.lookupVirtualDispatchTarget(instantiation, appInfo); |
| if (lookup != null) { |
| markVirtualDispatchTargetAsLive( |
| lookup, |
| programMethod -> |
| graphReporter.reportReachableMethodAsLive( |
| resolutionMethod.getMethod().method, programMethod)); |
| } |
| } |
| |
| private void markLibraryAndClasspathMethodOverridesAsLive( |
| InstantiatedObject instantiation, DexClass libraryClass) { |
| assert libraryClass.isNotProgramClass(); |
| if (mode.isTracingMainDex()) { |
| // Library roots must be specified for tracing of library methods. For classpath the expected |
| // use case is that the classes will be classloaded, thus they should have no bearing on the |
| // content of the main dex file. |
| return; |
| } |
| for (DexEncodedMethod method : libraryClass.virtualMethods()) { |
| assert !method.isPrivateMethod(); |
| // Note: It would be reasonable to not process methods already seen during the marking of |
| // program usages, but that would cause the methods to not be marked as library overrides. |
| markLibraryOrClasspathOverrideLive( |
| instantiation, libraryClass, appInfo.resolveMethod(libraryClass, method.method)); |
| |
| // Due to API conversion, some overrides can be hidden since they will be rewritten. See |
| // class comment of DesugaredLibraryAPIConverter and vivifiedType logic. |
| // In the first enqueuer phase, the signature has not been desugared, so firstResolution |
| // maintains the library override. In the second enqueuer phase, the signature has been |
| // desugared, and the second resolution maintains the the library override. |
| if (instantiation.isClass() |
| && appView.rewritePrefix.hasRewrittenTypeInSignature(method.method.proto, appView)) { |
| DexMethod methodToResolve = |
| DesugaredLibraryAPIConverter.methodWithVivifiedTypeInSignature( |
| method.method, method.holder(), appView); |
| assert methodToResolve != method.method; |
| markLibraryOrClasspathOverrideLive( |
| instantiation, |
| libraryClass, |
| appInfo.resolveMethod(instantiation.asClass(), methodToResolve)); |
| } |
| } |
| } |
| |
| private void markLibraryOrClasspathOverrideLive( |
| InstantiatedObject instantiation, |
| DexClass libraryOrClasspathClass, |
| ResolutionResult resolution) { |
| LookupTarget lookup = resolution.lookupVirtualDispatchTarget(instantiation, appInfo); |
| if (lookup == null) { |
| return; |
| } |
| if (!shouldMarkLibraryMethodOverrideAsReachable(lookup)) { |
| return; |
| } |
| markVirtualDispatchTargetAsLive( |
| lookup, |
| method -> |
| graphReporter.reportLibraryMethodAsLive( |
| instantiation, method, libraryOrClasspathClass)); |
| if (instantiation.isClass()) { |
| // TODO(b/149976493): We need to mark these for lambdas too! |
| markOverridesAsLibraryMethodOverrides( |
| instantiation.asClass(), lookup.asMethodTarget().getMethod().method); |
| } |
| } |
| |
| private void markOverridesAsLibraryMethodOverrides( |
| DexProgramClass instantiatedClass, DexMethod libraryMethodOverride) { |
| WorkList<DexType> worklist = WorkList.newIdentityWorkList(); |
| worklist.addIfNotSeen(instantiatedClass.type); |
| while (worklist.hasNext()) { |
| DexType type = worklist.next(); |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz == null) { |
| continue; |
| } |
| DexEncodedMethod override = clazz.lookupVirtualMethod(libraryMethodOverride); |
| if (override != null) { |
| if (override.isLibraryMethodOverride().isTrue()) { |
| continue; |
| } |
| override.setLibraryMethodOverride(OptionalBool.TRUE); |
| } |
| clazz.forEachImmediateSupertype(worklist::addIfNotSeen); |
| } |
| } |
| |
| /** |
| * 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(DexProgramClass clazz) { |
| do { |
| SetWithReason<DexEncodedField> reachableFields = reachableInstanceFields.get(clazz); |
| if (reachableFields != null) { |
| for (DexEncodedField field : reachableFields.getItems()) { |
| // TODO(b/120959039): Should the reason this field is reachable come from the set? |
| markInstanceFieldAsLive(clazz, field, KeepReason.reachableFromLiveType(clazz.type)); |
| } |
| } |
| clazz = getProgramClassOrNull(clazz.superType); |
| } while (clazz != null && !objectAllocationInfoCollection.isInstantiatedDirectly(clazz)); |
| } |
| |
| private void transitionDependentItemsForInstantiatedClass(DexProgramClass clazz) { |
| assert !clazz.isAnnotation(); |
| assert !clazz.isInterface(); |
| transitionDependentItemsForInstantiatedItem(clazz); |
| } |
| |
| private void transitionDependentItemsForInstantiatedInterface(DexProgramClass clazz) { |
| assert clazz.isInterface(); |
| transitionDependentItemsForInstantiatedItem(clazz); |
| } |
| |
| private void transitionDependentItemsForInstantiatedItem(DexProgramClass clazz) { |
| do { |
| // Handle keep rules that are dependent on the class being instantiated. |
| rootSet.forEachDependentNonStaticMember(clazz, appView, this::enqueueDependentItem); |
| |
| // Visit the super type. |
| clazz = |
| clazz.superType != null |
| ? asProgramClassOrNull(appView.definitionFor(clazz.superType)) |
| : null; |
| } while (clazz != null && !objectAllocationInfoCollection.isInstantiatedDirectly(clazz)); |
| } |
| |
| private void transitionUnusedInterfaceToLive(DexProgramClass clazz) { |
| if (clazz.isInterface()) { |
| Set<DexProgramClass> implementedBy = unusedInterfaceTypes.remove(clazz); |
| if (implementedBy != null) { |
| for (DexProgramClass implementer : implementedBy) { |
| markTypeAsLive(clazz, graphReporter.reportClassReferencedFrom(clazz, implementer)); |
| } |
| } |
| } else { |
| assert !unusedInterfaceTypes.containsKey(clazz); |
| } |
| } |
| |
| private void markFieldAsTargeted(DexField field, DexEncodedMethod context) { |
| markTypeAsLive(field.type, clazz -> graphReporter.reportClassReferencedFrom(clazz, context)); |
| markTypeAsLive(field.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, context)); |
| } |
| |
| private void markStaticFieldAsLive(DexEncodedField encodedField, KeepReason reason) { |
| // Mark the type live here, so that the class exists at runtime. |
| DexField field = encodedField.field; |
| markTypeAsLive( |
| field.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, encodedField)); |
| markTypeAsLive( |
| field.type, clazz -> graphReporter.reportClassReferencedFrom(clazz, encodedField)); |
| |
| DexProgramClass clazz = getProgramClassOrNull(field.holder); |
| if (clazz == null) { |
| return; |
| } |
| |
| markDirectAndIndirectClassInitializersAsLive(clazz); |
| |
| // 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); |
| } |
| } |
| processAnnotations(clazz, encodedField); |
| liveFields.add(encodedField, reason); |
| |
| // Add all dependent members to the workqueue. |
| enqueueRootItems(rootSet.getDependentItems(encodedField)); |
| |
| // Notify analyses. |
| analyses.forEach(analysis -> analysis.processNewlyLiveField(encodedField)); |
| } |
| |
| private void markInstanceFieldAsLive( |
| DexProgramClass holder, DexEncodedField field, KeepReason reason) { |
| assert field != null; |
| assert field.isProgramField(appView); |
| markTypeAsLive(field.holder(), reason); |
| markTypeAsLive(field.field.type, reason); |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Adding instance field `%s` to live set.", field.field); |
| } |
| processAnnotations(holder, field); |
| liveFields.add(field, reason); |
| |
| // Add all dependent members to the workqueue. |
| enqueueRootItems(rootSet.getDependentItems(field)); |
| |
| // Notify analyses. |
| analyses.forEach(analysis -> analysis.processNewlyLiveField(field)); |
| } |
| |
| private void markDirectStaticOrConstructorMethodAsLive( |
| DexProgramClass clazz, DexEncodedMethod encodedMethod, KeepReason reason) { |
| assert encodedMethod.holder() == clazz.type; |
| |
| if (!enqueueMarkMethodLiveAction(clazz, encodedMethod, reason)) { |
| // Already marked live. |
| return; |
| } |
| // Should already have marked the type live previously. |
| DexMethod method = encodedMethod.method; |
| assert encodedMethod.isClassInitializer() || verifyMethodIsTargeted(encodedMethod); |
| assert verifyTypeIsLive(clazz); |
| if (Log.ENABLED) { |
| Log.verbose( |
| getClass(), "Method `%s` has become live due to direct invoke", encodedMethod.method); |
| } |
| } |
| |
| private void markVirtualMethodAsLive( |
| DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) { |
| assert method != null; |
| // Only explicit keep rules or reflective use should make abstract methods live. |
| assert !method.accessFlags.isAbstract() |
| || reason.isDueToKeepRule() |
| || reason.isDueToReflectiveUse(); |
| if (enqueueMarkMethodLiveAction(clazz, method, reason)) { |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Adding virtual method `%s` to live set.", method.method); |
| } |
| } |
| } |
| |
| public boolean isFieldReferenced(DexEncodedField field) { |
| FieldAccessInfoImpl info = fieldAccessInfoCollection.get(field.field); |
| return info != null; |
| } |
| |
| public boolean isFieldLive(DexEncodedField field) { |
| return liveFields.contains(field); |
| } |
| |
| public boolean isFieldRead(DexEncodedField field) { |
| FieldAccessInfoImpl info = fieldAccessInfoCollection.get(field.field); |
| return info != null && info.isRead(); |
| } |
| |
| public boolean isFieldWrittenInMethodSatisfying( |
| DexEncodedField field, Predicate<DexEncodedMethod> predicate) { |
| FieldAccessInfoImpl info = fieldAccessInfoCollection.get(field.field); |
| return info != null && info.isWrittenInMethodSatisfying(predicate); |
| } |
| |
| public boolean isFieldWrittenOutsideDefaultConstructor(DexEncodedField field) { |
| FieldAccessInfoImpl info = fieldAccessInfoCollection.get(field.field); |
| if (info == null) { |
| return false; |
| } |
| DexClass clazz = appView.definitionFor(field.holder()); |
| DexEncodedMethod defaultInitializer = clazz.getDefaultInitializer(); |
| return defaultInitializer != null |
| ? info.isWrittenOutside(defaultInitializer) |
| : info.isWritten(); |
| } |
| |
| public boolean isMethodLive(DexEncodedMethod method) { |
| return liveMethods.contains(method); |
| } |
| |
| public boolean isMethodTargeted(DexEncodedMethod method) { |
| return targetedMethods.contains(method); |
| } |
| |
| public boolean isTypeLive(DexClass clazz) { |
| return clazz.isProgramClass() |
| ? isTypeLive(clazz.asProgramClass()) |
| : isNonProgramTypeLive(clazz); |
| } |
| |
| public boolean isTypeLive(DexProgramClass clazz) { |
| return liveTypes.contains(clazz); |
| } |
| |
| public boolean isNonProgramTypeLive(DexClass clazz) { |
| assert !clazz.isProgramClass(); |
| return liveNonProgramTypes.contains(clazz); |
| } |
| |
| // Package protected due to entry point from worklist. |
| void markInstanceFieldAsReachable(DexEncodedField encodedField, KeepReason reason) { |
| DexField field = encodedField.field; |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Marking instance field `%s` as reachable.", field); |
| } |
| |
| markTypeAsLive( |
| field.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, encodedField)); |
| markTypeAsLive( |
| field.type, clazz -> graphReporter.reportClassReferencedFrom(clazz, encodedField)); |
| |
| DexProgramClass clazz = getProgramClassOrNull(field.holder); |
| if (clazz == null) { |
| 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, reason); |
| } else { |
| if (objectAllocationInfoCollection.isInstantiatedDirectlyOrHasInstantiatedSubtype(clazz)) { |
| markInstanceFieldAsLive(clazz, encodedField, reason); |
| } else { |
| // Add the field to the reachable set if the type later becomes instantiated. |
| reachableInstanceFields |
| .computeIfAbsent(clazz, ignore -> newSetWithoutReasonReporter()) |
| .add(encodedField, reason); |
| } |
| } |
| } |
| |
| private void markVirtualMethodAsReachable( |
| DexMethod method, boolean interfaceInvoke, ProgramMethod contextOrNull, KeepReason reason) { |
| 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. |
| markTypeAsLive(method.holder, reason); |
| return; |
| } |
| |
| // Note that all virtual methods derived from library methods are kept regardless of being |
| // reachable, so the following only needs to consider reachable targets in the program. |
| // TODO(b/70160030): Revise this to support tree shaking library methods on non-escaping types. |
| DexProgramClass holder = getProgramClassOrNull(method.holder); |
| if (holder == null) { |
| // TODO(b/139464956): clean this. |
| // Ensure that the full proto of the targeted method is referenced. |
| recordMethodReference(method); |
| return; |
| } |
| |
| SingleResolutionResult resolution = resolveMethod(method, reason, interfaceInvoke); |
| if (resolution == null) { |
| return; |
| } |
| |
| if (resolution.getResolvedHolder().isNotProgramClass()) { |
| // TODO(b/70160030): If the resolution is on a library method, then the keep edge needs to go |
| // directly to the target method in the program. Thus this method will need to ensure that |
| // 'reason' is not already reported (eg, must be delayed / non-witness) and report that for |
| // each possible target edge below. |
| return; |
| } |
| |
| // If the method has already been marked, just report the new reason for the resolved target. |
| if (getReachableVirtualTargets(holder).contains(method)) { |
| graphReporter.registerMethod(resolution.getResolvedMethod(), reason); |
| return; |
| } |
| |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Marking virtual method `%s` as reachable.", method); |
| } |
| |
| // We have to mark the resolution targeted, even if it does not become live, we |
| // need at least an abstract version of it so that it can be targeted. |
| DexProgramClass resolvedHolder = resolution.getResolvedHolder().asProgramClass(); |
| DexEncodedMethod resolvedMethod = resolution.getResolvedMethod(); |
| markMethodAsTargeted(resolvedHolder, resolvedMethod, reason); |
| |
| DexProgramClass context = contextOrNull == null ? null : contextOrNull.getHolder(); |
| if (contextOrNull != null && !resolution.isAccessibleForVirtualDispatchFrom(context, appInfo)) { |
| // Not accessible from this context, so this call will cause a runtime exception. |
| return; |
| } |
| |
| // If the resolved method is not a virtual target, eg, is static, dispatch will fail too. |
| if (!resolvedMethod.isVirtualMethod()) { |
| // This can only happen when context is null, otherwise the access check above will fail. |
| assert context == null; |
| return; |
| } |
| |
| // The method resolved and is accessible, so currently live overrides become live. |
| reachableVirtualTargets.computeIfAbsent(holder, k -> Sets.newIdentityHashSet()).add(method); |
| |
| resolution |
| .lookupVirtualDispatchTargets( |
| context, |
| appInfo, |
| (type, subTypeConsumer, lambdaConsumer) -> |
| objectAllocationInfoCollection.forEachInstantiatedSubType( |
| type, subTypeConsumer, lambdaConsumer, appInfo), |
| pinnedItems::contains) |
| .forEach( |
| target -> |
| markVirtualDispatchTargetAsLive( |
| target, |
| programMethod -> |
| graphReporter.reportReachableMethodAsLive( |
| resolvedMethod.method, programMethod))); |
| } |
| |
| private void markVirtualDispatchTargetAsLive( |
| LookupTarget target, Function<ProgramMethod, KeepReasonWitness> reason) { |
| if (target.isMethodTarget()) { |
| markVirtualDispatchTargetAsLive(target.asMethodTarget(), reason); |
| } else { |
| assert target.isLambdaTarget(); |
| markVirtualDispatchTargetAsLive(target.asLambdaTarget(), reason); |
| } |
| } |
| |
| private void markVirtualDispatchTargetAsLive( |
| DexClassAndMethod target, Function<ProgramMethod, KeepReasonWitness> reason) { |
| ProgramMethod programMethod = target.asProgramMethod(); |
| if (programMethod != null && !programMethod.getMethod().isAbstract()) { |
| markVirtualMethodAsLive( |
| programMethod.getHolder(), programMethod.getMethod(), reason.apply(programMethod)); |
| } |
| } |
| |
| private void markVirtualDispatchTargetAsLive( |
| LookupLambdaTarget target, Function<ProgramMethod, KeepReasonWitness> reason) { |
| ProgramMethod implementationMethod = target.getImplementationMethod().asProgramMethod(); |
| if (implementationMethod != null) { |
| enqueueMarkMethodLiveAction( |
| implementationMethod.getHolder(), |
| implementationMethod.getMethod(), |
| reason.apply(implementationMethod)); |
| } |
| } |
| |
| private void markFailedResolutionTargets( |
| DexMethod symbolicMethod, FailedResolutionResult failedResolution, KeepReason reason) { |
| failedResolutionTargets.add(symbolicMethod); |
| failedResolution.forEachFailureDependency( |
| method -> { |
| DexProgramClass clazz = getProgramClassOrNull(method.holder()); |
| if (clazz != null) { |
| failedResolutionTargets.add(method.method); |
| markMethodAsTargeted(clazz, method, reason); |
| } |
| }); |
| } |
| |
| private DexMethod generatedEnumValuesMethod(DexClass enumClass) { |
| DexType arrayOfEnumClass = |
| appView |
| .dexItemFactory() |
| .createType( |
| appView.dexItemFactory().createString("[" + enumClass.type.toDescriptorString())); |
| DexProto proto = appView.dexItemFactory().createProto(arrayOfEnumClass); |
| return appView |
| .dexItemFactory() |
| .createMethod(enumClass.type, proto, appView.dexItemFactory().createString("values")); |
| } |
| |
| private void markEnumValuesAsReachable(DexProgramClass clazz, KeepReason reason) { |
| DexEncodedMethod valuesMethod = clazz.lookupMethod(generatedEnumValuesMethod(clazz)); |
| if (valuesMethod != null) { |
| // TODO(sgjesse): Does this have to be enqueued as a root item? Right now it is done as the |
| // marking for not renaming it is in the root set. |
| workList.enqueueMarkMethodKeptAction(clazz, valuesMethod, reason); |
| pinnedItems.add(valuesMethod.toReference()); |
| rootSet.shouldNotBeMinified(valuesMethod.toReference()); |
| } |
| } |
| |
| // Package protected due to entry point from worklist. |
| void markSuperMethodAsReachable(DexMethod method, DexEncodedMethod from) { |
| KeepReason reason = KeepReason.targetedBySuperFrom(from); |
| SingleResolutionResult resolution = resolveMethod(method, reason); |
| if (resolution == null) { |
| return; |
| } |
| // If the resolution is in the program, mark it targeted. |
| if (resolution.getResolvedHolder().isProgramClass()) { |
| markMethodAsTargeted( |
| resolution.getResolvedHolder().asProgramClass(), resolution.getResolvedMethod(), reason); |
| } |
| // If invoke target is invalid (inaccessible or not an instance-method) record it and stop. |
| // TODO(b/146016987): We should be passing the full program context and not looking it up again. |
| DexProgramClass fromHolder = appInfo.definitionFor(from.holder()).asProgramClass(); |
| DexEncodedMethod target = resolution.lookupInvokeSuperTarget(fromHolder, appInfo); |
| if (target == null) { |
| failedResolutionTargets.add(resolution.getResolvedMethod().method); |
| return; |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(target.holder()); |
| if (clazz == null) { |
| return; |
| } |
| if (Log.ENABLED) { |
| Log.verbose(getClass(), "Adding super constraint from `%s` to `%s`", from.method, |
| target.method); |
| } |
| if (superInvokeDependencies.computeIfAbsent( |
| from, ignore -> Sets.newIdentityHashSet()).add(target)) { |
| if (liveMethods.contains(from)) { |
| markMethodAsTargeted(clazz, target, KeepReason.invokedViaSuperFrom(from)); |
| if (!target.accessFlags.isAbstract()) { |
| markVirtualMethodAsLive(clazz, target, KeepReason.invokedViaSuperFrom(from)); |
| } |
| } |
| } |
| } |
| |
| // Returns the set of live types. |
| public Set<DexProgramClass> traceMainDex( |
| RootSet rootSet, ExecutorService executorService, Timing timing) throws ExecutionException { |
| assert analyses.isEmpty(); |
| assert mode.isTracingMainDex(); |
| this.rootSet = rootSet; |
| // Translate the result of root-set computation into enqueuer actions. |
| enqueueRootItems(rootSet.noShrinking); |
| trace(executorService, timing); |
| options.reporter.failIfPendingErrors(); |
| return liveTypes.getItems(); |
| } |
| |
| public AppInfoWithLiveness traceApplication( |
| RootSet rootSet, |
| ProguardClassFilter dontWarnPatterns, |
| ExecutorService executorService, |
| Timing timing) |
| throws ExecutionException { |
| this.rootSet = rootSet; |
| this.dontWarnPatterns = dontWarnPatterns; |
| // Translate the result of root-set computation into enqueuer actions. |
| if (appView.options().isShrinking() || appView.options().getProguardConfiguration() == null) { |
| enqueueRootItems(rootSet.noShrinking); |
| } else { |
| // Add everything if we are not shrinking. |
| assert appView.options().getProguardConfiguration().getKeepAllRule() != null; |
| ImmutableSet<ProguardKeepRuleBase> keepAllSet = |
| ImmutableSet.of(appView.options().getProguardConfiguration().getKeepAllRule()); |
| for (DexProgramClass dexProgramClass : appView.appInfo().classes()) { |
| for (DexEncodedMethod method : dexProgramClass.methods()) { |
| this.enqueueRootItem(method, keepAllSet); |
| } |
| for (DexEncodedField field : dexProgramClass.fields()) { |
| this.enqueueRootItem(field, keepAllSet); |
| } |
| this.enqueueRootItem(dexProgramClass, keepAllSet); |
| } |
| } |
| trace(executorService, timing); |
| options.reporter.failIfPendingErrors(); |
| finalizeLibraryMethodOverrideInformation(); |
| analyses.forEach(EnqueuerAnalysis::done); |
| assert verifyKeptGraph(); |
| AppInfoWithLiveness appInfoWithLiveness = createAppInfo(appInfo); |
| if (options.testing.enqueuerInspector != null) { |
| options.testing.enqueuerInspector.accept(appInfoWithLiveness, mode); |
| } |
| return appInfoWithLiveness; |
| } |
| |
| private static class SyntheticAdditions { |
| |
| Map<DexType, Pair<DexProgramClass, DexEncodedMethod>> syntheticInstantiations = |
| new IdentityHashMap<>(); |
| |
| Map<DexMethod, ProgramMethod> liveMethods = new IdentityHashMap<>(); |
| |
| Map<DexType, DexClasspathClass> syntheticClasspathClasses = new IdentityHashMap<>(); |
| |
| // Subset of live methods that need to be pinned. |
| Set<DexMethod> pinnedMethods = Sets.newIdentityHashSet(); |
| |
| // Subset of synthesized classes that need to be added to the main-dex file. |
| Set<DexType> mainDexTypes = Sets.newIdentityHashSet(); |
| |
| boolean isEmpty() { |
| boolean empty = syntheticInstantiations.isEmpty() && liveMethods.isEmpty(); |
| assert !empty || (pinnedMethods.isEmpty() && mainDexTypes.isEmpty()); |
| return empty; |
| } |
| |
| void addInstantiatedClass( |
| DexProgramClass clazz, DexEncodedMethod context, boolean isMainDexClass) { |
| assert !syntheticInstantiations.containsKey(clazz.type); |
| syntheticInstantiations.put(clazz.type, new Pair<>(clazz, context)); |
| if (isMainDexClass) { |
| mainDexTypes.add(clazz.type); |
| } |
| } |
| |
| void addClasspathClass(DexClasspathClass clazz) { |
| DexClasspathClass old = syntheticClasspathClasses.put(clazz.type, clazz); |
| assert old == null; |
| } |
| |
| void addLiveMethod(ProgramMethod method) { |
| DexMethod signature = method.getMethod().method; |
| assert !liveMethods.containsKey(signature); |
| liveMethods.put(signature, method); |
| } |
| |
| void addLiveAndPinnedMethod(ProgramMethod method) { |
| addLiveMethod(method); |
| pinnedMethods.add(method.getMethod().method); |
| } |
| |
| void amendApplication(Builder appBuilder) { |
| assert !isEmpty(); |
| for (Pair<DexProgramClass, DexEncodedMethod> clazzAndContext : |
| syntheticInstantiations.values()) { |
| appBuilder.addProgramClass(clazzAndContext.getFirst()); |
| } |
| appBuilder.addClasspathClasses(syntheticClasspathClasses.values()); |
| appBuilder.addToMainDexList(mainDexTypes); |
| } |
| |
| void enqueueWorkItems(Enqueuer enqueuer) { |
| assert !isEmpty(); |
| assert enqueuer.mode.isInitialTreeShaking(); |
| // All synthetic additions are initial tree shaking only. No need to track keep reasons. |
| KeepReasonWitness fakeReason = enqueuer.graphReporter.fakeReportShouldNotBeUsed(); |
| |
| enqueuer.pinnedItems.addAll(pinnedMethods); |
| for (Pair<DexProgramClass, DexEncodedMethod> clazzAndContext : |
| syntheticInstantiations.values()) { |
| enqueuer.workList.enqueueMarkInstantiatedAction( |
| clazzAndContext.getFirst(), |
| clazzAndContext.getSecond(), |
| InstantiationReason.SYNTHESIZED_CLASS, |
| fakeReason); |
| } |
| for (ProgramMethod liveMethod : liveMethods.values()) { |
| assert !enqueuer.targetedMethods.contains(liveMethod.getMethod()); |
| DexProgramClass holder = liveMethod.getHolder(); |
| DexEncodedMethod method = liveMethod.getMethod(); |
| enqueuer.markMethodAsTargeted(holder, method, fakeReason); |
| enqueuer.enqueueMarkMethodLiveAction(holder, method, fakeReason); |
| } |
| } |
| } |
| |
| private void synthesize() { |
| if (!mode.isInitialTreeShaking()) { |
| return; |
| } |
| // First part of synthesis is to create and register all reachable synthetic additions. |
| // In particular these additions are order independent, i.e., it does not matter which are |
| // registered first and no dependencies may exist among them. |
| SyntheticAdditions additions = new SyntheticAdditions(); |
| synthesizeInterfaceMethodBridges(additions); |
| synthesizeLambdas(additions); |
| synthesizeLibraryConversionWrappers(additions); |
| if (additions.isEmpty()) { |
| return; |
| } |
| |
| // Now all additions are computed, the application is atomically extended with those additions. |
| Builder appBuilder = appInfo.app().asDirect().builder(); |
| additions.amendApplication(appBuilder); |
| appInfo = new AppInfoWithSubtyping(appBuilder.build()); |
| appView.setAppInfo(appInfo); |
| |
| // Finally once all synthesized items "exist" it is now safe to continue tracing. The new work |
| // items are enqueued and the fixed point will continue once this subroutine returns. |
| additions.enqueueWorkItems(this); |
| } |
| |
| private void synthesizeInterfaceMethodBridges(SyntheticAdditions additions) { |
| for (ProgramMethod bridge : syntheticInterfaceMethodBridges.values()) { |
| DexProgramClass holder = bridge.getHolder(); |
| DexEncodedMethod method = bridge.getMethod(); |
| holder.addVirtualMethod(method); |
| additions.addLiveAndPinnedMethod(bridge); |
| } |
| syntheticInterfaceMethodBridges.clear(); |
| } |
| |
| private void synthesizeLambdas(SyntheticAdditions additions) { |
| if (lambdaRewriter == null || lambdaClasses.isEmpty()) { |
| assert lambdaCallSites.isEmpty(); |
| assert classesWithSerializableLambdas.isEmpty(); |
| return; |
| } |
| for (Pair<LambdaClass, DexEncodedMethod> lambdaClassAndContext : lambdaClasses.values()) { |
| // Add all desugared classes to the application, main-dex list, and mark them instantiated. |
| LambdaClass lambdaClass = lambdaClassAndContext.getFirst(); |
| DexEncodedMethod context = lambdaClassAndContext.getSecond(); |
| DexProgramClass programClass = lambdaClass.getOrCreateLambdaClass(); |
| additions.addInstantiatedClass(programClass, context, lambdaClass.addToMainDexList.get()); |
| // Mark the instance constructor targeted and live. |
| DexEncodedMethod constructor = programClass.lookupDirectMethod(lambdaClass.constructor); |
| KeepReason reason = KeepReason.instantiatedIn(context); |
| markMethodAsTargeted(programClass, constructor, reason); |
| markDirectStaticOrConstructorMethodAsLive(programClass, constructor, reason); |
| } |
| |
| // Rewrite all of the invoke-dynamic instructions to lambda class instantiations. |
| lambdaCallSites.forEach(this::rewriteLambdaCallSites); |
| |
| // Remove all '$deserializeLambda$' methods which are not supported by desugaring. |
| for (DexProgramClass clazz : classesWithSerializableLambdas) { |
| clazz.removeDirectMethod(appView.dexItemFactory().deserializeLambdaMethod); |
| } |
| |
| // Clear state before next fixed point iteration. |
| lambdaClasses.clear(); |
| lambdaCallSites.clear(); |
| classesWithSerializableLambdas.clear(); |
| } |
| |
| private void finalizeLibraryMethodOverrideInformation() { |
| for (DexProgramClass liveType : liveTypes.getItems()) { |
| for (DexEncodedMethod method : liveType.virtualMethods()) { |
| if (method.isLibraryMethodOverride().isUnknown()) { |
| method.setLibraryMethodOverride(OptionalBool.FALSE); |
| } |
| } |
| } |
| } |
| |
| private boolean verifyKeptGraph() { |
| if (appView.options().testing.verifyKeptGraphInfo) { |
| for (DexProgramClass liveType : liveTypes.getItems()) { |
| assert graphReporter.verifyRootedPath(liveType); |
| } |
| } |
| return true; |
| } |
| |
| private AppInfoWithLiveness createAppInfo(AppInfoWithSubtyping appInfo) { |
| // Once all tracing is done, we generate accessor methods for lambdas. |
| // These are assumed to be simple forwarding or access flag updates, thus no further tracing |
| // is needed. These cannot be generated as part of lambda synthesis as changing a direct method |
| // to a static method will invalidate the reachable method sets for tracing methods. |
| ensureLambdaAccessibility(); |
| |
| // Compute the set of dead proto types. |
| deadProtoTypeCandidates.removeIf(this::isTypeLive); |
| |
| // Remove the temporary mappings that have been inserted into the field access info collection |
| // and verify that the mapping is then one-to-one. |
| fieldAccessInfoCollection.removeIf( |
| (field, info) -> field != info.getField() || info == MISSING_FIELD_ACCESS_INFO); |
| assert fieldAccessInfoCollection.verifyMappingIsOneToOne(); |
| |
| // Verify all references on the input app before synthesizing definitions. |
| assert verifyReferences(appInfo.app()); |
| |
| // Prune the root set items that turned out to be dead. |
| // TODO(b/150736225): Pruning of dead root set items is still incomplete. |
| rootSet.pruneDeadItems(appView, this); |
| |
| // Ensure references from all hard coded factory items. |
| appView.dexItemFactory().forEachPossiblyCompilerSynthesizedType(this::recordTypeReference); |
| |
| // Rebuild a new app only containing referenced types. |
| Set<DexLibraryClass> libraryClasses = Sets.newIdentityHashSet(); |
| Set<DexClasspathClass> classpathClasses = Sets.newIdentityHashSet(); |
| for (DexClass clazz : liveNonProgramTypes) { |
| if (clazz.isLibraryClass()) { |
| libraryClasses.add(clazz.asLibraryClass()); |
| } else if (clazz.isClasspathClass()) { |
| classpathClasses.add(clazz.asClasspathClass()); |
| } else { |
| assert false; |
| } |
| } |
| |
| // Add just referenced non-program types. We can't replace the program classes at this point as |
| // they are needed in tree pruning. |
| Builder appBuilder = appInfo.app().asDirect().builder(); |
| appBuilder.replaceLibraryClasses(libraryClasses); |
| appBuilder.replaceClasspathClasses(classpathClasses); |
| DirectMappedDexApplication app = appBuilder.build(); |
| |
| // Verify the references on the pruned application after type synthesis. |
| assert verifyReferences(app); |
| |
| AppInfoWithLiveness appInfoWithLiveness = |
| new AppInfoWithLiveness( |
| app, |
| SetUtils.mapIdentityHashSet(deadProtoTypeCandidates, DexProgramClass::getType), |
| missingTypes, |
| SetUtils.mapIdentityHashSet(liveTypes.getItems(), DexProgramClass::getType), |
| Collections.unmodifiableSet(instantiatedAppServices), |
| Enqueuer.toSortedDescriptorSet(targetedMethods.getItems()), |
| Collections.unmodifiableSet(failedResolutionTargets), |
| ImmutableSortedSet.copyOf(DexMethod::slowCompareTo, bootstrapMethods), |
| ImmutableSortedSet.copyOf(DexMethod::slowCompareTo, methodsTargetedByInvokeDynamic), |
| ImmutableSortedSet.copyOf( |
| DexMethod::slowCompareTo, virtualMethodsTargetedByInvokeDirect), |
| toSortedDescriptorSet(liveMethods.getItems()), |
| // Filter out library fields and pinned fields, because these are read by default. |
| fieldAccessInfoCollection, |
| objectAllocationInfoCollection.build(appInfo), |
| // TODO(b/132593519): Do we require these sets to be sorted for determinism? |
| toImmutableSortedMap(virtualInvokes, PresortedComparable::slowCompare), |
| toImmutableSortedMap(interfaceInvokes, PresortedComparable::slowCompare), |
| toImmutableSortedMap(superInvokes, PresortedComparable::slowCompare), |
| toImmutableSortedMap(directInvokes, PresortedComparable::slowCompare), |
| toImmutableSortedMap(staticInvokes, PresortedComparable::slowCompare), |
| callSites, |
| pinnedItems, |
| rootSet.mayHaveSideEffects, |
| rootSet.noSideEffects, |
| rootSet.assumedValues, |
| rootSet.alwaysInline, |
| rootSet.forceInline, |
| rootSet.neverInline, |
| rootSet.whyAreYouNotInlining, |
| rootSet.keepConstantArguments, |
| rootSet.keepUnusedArguments, |
| rootSet.reprocess, |
| rootSet.neverReprocess, |
| rootSet.alwaysClassInline, |
| rootSet.neverClassInline, |
| rootSet.neverMerge, |
| rootSet.neverPropagateValue, |
| joinIdentifierNameStrings(rootSet.identifierNameStrings, identifierNameStrings), |
| Collections.emptySet(), |
| Collections.emptyMap(), |
| EnumValueInfoMapCollection.empty(), |
| constClassReferences, |
| initClassReferences); |
| appInfo.markObsolete(); |
| return appInfoWithLiveness; |
| } |
| |
| private void ensureLambdaAccessibility() { |
| if (lambdaRewriter == null) { |
| return; |
| } |
| lambdaRewriter |
| .getKnownLambdaClasses() |
| .forEach( |
| (type, lambda) -> { |
| DexProgramClass synthesizedClass = getProgramClassOrNull(type); |
| assert synthesizedClass != null; |
| assert liveTypes.contains(synthesizedClass); |
| if (synthesizedClass == null) { |
| return; |
| } |
| DexMethod method = lambda.descriptor.getMainMethod(); |
| if (!liveMethods.contains(synthesizedClass.lookupMethod(method))) { |
| return; |
| } |
| DexEncodedMethod accessor = lambda.target.ensureAccessibilityIfNeeded(false); |
| if (accessor == null) { |
| return; |
| } |
| DexProgramClass accessorClass = getProgramClassOrNull(accessor.holder()); |
| assert accessorClass != null; |
| if (accessorClass != null) { |
| liveMethods.add(accessorClass, accessor, graphReporter.fakeReportShouldNotBeUsed()); |
| } |
| }); |
| } |
| |
| private boolean verifyReferences(DexApplication app) { |
| WorkList<DexClass> worklist = WorkList.newIdentityWorkList(); |
| for (DexProgramClass clazz : liveTypes.getItems()) { |
| worklist.addIfNotSeen(clazz); |
| } |
| while (worklist.hasNext()) { |
| DexClass clazz = worklist.next(); |
| assert verifyReferencedType(clazz, worklist, app); |
| } |
| return true; |
| } |
| |
| private boolean verifyReferencedType( |
| DexType type, WorkList<DexClass> worklist, DexApplication app) { |
| if (type.isArrayType()) { |
| type = type.toBaseType(appView.dexItemFactory()); |
| } |
| if (!type.isClassType()) { |
| return true; |
| } |
| DexClass clazz = app.definitionFor(type); |
| if (clazz == null) { |
| assert missingTypes.contains(type) : "Expected type to be in missing types': " + type; |
| } else { |
| assert !missingTypes.contains(type) : "Type with definition also in missing types: " + type; |
| // Eager assert while the context is still present. |
| assert clazz.isProgramClass() || liveNonProgramTypes.contains(clazz) |
| : "Expected type to be in live non-program types: " + clazz; |
| worklist.addIfNotSeen(clazz); |
| } |
| return true; |
| } |
| |
| private boolean verifyReferencedType( |
| DexClass clazz, WorkList<DexClass> worklist, DexApplication app) { |
| for (DexType supertype : clazz.allImmediateSupertypes()) { |
| assert verifyReferencedType(supertype, worklist, app); |
| } |
| assert clazz.isProgramClass() || liveNonProgramTypes.contains(clazz) |
| : "Expected type to be in live non-program types: " + clazz; |
| for (DexEncodedField field : clazz.fields()) { |
| if (clazz.isNotProgramClass() || isFieldReferenced(field)) { |
| assert verifyReferencedType(field.field.type, worklist, app); |
| } |
| } |
| for (DexEncodedMethod method : clazz.methods()) { |
| if (clazz.isNotProgramClass() || isMethodTargeted(method)) { |
| assert verifyReferencedMethod(method, worklist, app); |
| } |
| } |
| return true; |
| } |
| |
| private boolean verifyReferencedMethod( |
| DexEncodedMethod method, WorkList<DexClass> worklist, DexApplication app) { |
| assert verifyReferencedType(method.method.proto.returnType, worklist, app); |
| for (DexType param : method.method.proto.parameters.values) { |
| assert verifyReferencedType(param, worklist, app); |
| } |
| return true; |
| } |
| |
| private void synthesizeLibraryConversionWrappers(SyntheticAdditions additions) { |
| if (desugaredLibraryWrapperAnalysis == null) { |
| return; |
| } |
| |
| // Generate first the callbacks since they may require extra wrappers. |
| List<DexEncodedMethod> callbacks = desugaredLibraryWrapperAnalysis.generateCallbackMethods(); |
| for (DexEncodedMethod callback : callbacks) { |
| DexProgramClass clazz = getProgramClassOrNull(callback.holder()); |
| additions.addLiveMethod(new ProgramMethod(clazz, callback)); |
| } |
| |
| // Generate the wrappers. |
| List<DexProgramClass> wrappers = desugaredLibraryWrapperAnalysis.generateWrappers(); |
| for (DexProgramClass wrapper : wrappers) { |
| additions.addInstantiatedClass(wrapper, null, false); |
| // Mark all methods on the wrapper as live and targeted. |
| for (DexEncodedMethod method : wrapper.methods()) { |
| additions.addLiveMethod(new ProgramMethod(wrapper, method)); |
| } |
| } |
| |
| // Add all vivified types as classpath classes. |
| // They will be available at runtime in the desugared library dex file. |
| desugaredLibraryWrapperAnalysis |
| .generateWrappersSuperTypeMock(wrappers) |
| .forEach(additions::addClasspathClass); |
| } |
| |
| private void rewriteLambdaCallSites( |
| DexEncodedMethod method, Map<DexCallSite, LambdaClass> callSites) { |
| assert !callSites.isEmpty(); |
| CfCode code = method.getCode().asCfCode(); |
| List<CfInstruction> instructions = code.instructions; |
| int replaced = 0; |
| int maxTemp = 0; |
| for (int i = 0; i < instructions.size(); i++) { |
| CfInstruction instruction = instructions.get(i); |
| if (instruction instanceof CfInvokeDynamic) { |
| LambdaClass lambdaClass = callSites.get(((CfInvokeDynamic) instruction).getCallSite()); |
| if (lambdaClass == null) { |
| continue; |
| } |
| if (lambdaClass.isStateless()) { |
| CfFieldInstruction getStaticLambdaInstance = |
| new CfFieldInstruction( |
| Opcodes.GETSTATIC, lambdaClass.lambdaField, lambdaClass.lambdaField); |
| instructions.set(i, getStaticLambdaInstance); |
| } else { |
| List<CfInstruction> replacement = new ArrayList<>(); |
| int arguments = lambdaClass.descriptor.captures.size(); |
| int temp = code.getMaxLocals(); |
| for (int j = arguments - 1; j >= 0; j--) { |
| ValueType type = ValueType.fromDexType(lambdaClass.descriptor.captures.values[j]); |
| replacement.add(new CfStore(type, temp)); |
| temp += type.requiredRegisters(); |
| } |
| maxTemp = Math.max(temp, maxTemp); |
| replacement.add(new CfNew(lambdaClass.type)); |
| replacement.add(new CfStackInstruction(Opcode.Dup)); |
| for (int j = 0; j < arguments; j++) { |
| ValueType type = ValueType.fromDexType(lambdaClass.descriptor.captures.values[j]); |
| temp -= type.requiredRegisters(); |
| replacement.add(new CfLoad(type, temp)); |
| } |
| replacement.add(new CfInvoke(Opcodes.INVOKESPECIAL, lambdaClass.constructor, false)); |
| instructions.remove(i); |
| instructions.addAll(i, replacement); |
| } |
| ++replaced; |
| } |
| } |
| if (maxTemp > 0) { |
| assert maxTemp > code.getMaxLocals(); |
| code.setMaxLocals(maxTemp); |
| } |
| assert replaced == callSites.size(); |
| } |
| |
| private static <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> |
| SortedSet<R> toSortedDescriptorSet(Set<D> set) { |
| ImmutableSortedSet.Builder<R> builder = |
| new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompareTo); |
| for (D item : set) { |
| builder.add(item.toReference()); |
| } |
| return builder.build(); |
| } |
| |
| private static Object2BooleanMap<DexReference> joinIdentifierNameStrings( |
| Set<DexReference> explicit, Set<DexReference> implicit) { |
| Object2BooleanMap<DexReference> result = new Object2BooleanArrayMap<>(); |
| for (DexReference e : explicit) { |
| result.putIfAbsent(e, true); |
| } |
| for (DexReference i : implicit) { |
| result.putIfAbsent(i, false); |
| } |
| return result; |
| } |
| |
| private void trace(ExecutorService executorService, Timing timing) throws ExecutionException { |
| timing.begin("Grow the tree."); |
| try { |
| while (true) { |
| long numberOfLiveItems = getNumberOfLiveItems(); |
| while (!workList.isEmpty()) { |
| EnqueuerAction action = workList.poll(); |
| action.run(this); |
| } |
| |
| // Continue fix-point processing if -if rules are enabled by items that newly became live. |
| long numberOfLiveItemsAfterProcessing = getNumberOfLiveItems(); |
| if (numberOfLiveItemsAfterProcessing > numberOfLiveItems) { |
| // Build the mapping of active if rules. We use a single collection of if-rules to allow |
| // removing if rules that have a constant sequent keep rule when they materialize. |
| if (activeIfRules == null) { |
| activeIfRules = new HashMap<>(); |
| IfRuleClassPartEquivalence equivalence = new IfRuleClassPartEquivalence(); |
| for (ProguardIfRule ifRule : rootSet.ifRules) { |
| Wrapper<ProguardIfRule> wrap = equivalence.wrap(ifRule); |
| activeIfRules.computeIfAbsent(wrap, ignore -> new LinkedHashSet<>()).add(ifRule); |
| } |
| } |
| ConsequentRootSetBuilder consequentSetBuilder = |
| new ConsequentRootSetBuilder(appView, this); |
| IfRuleEvaluator ifRuleEvaluator = |
| new IfRuleEvaluator( |
| appView, |
| this, |
| executorService, |
| activeIfRules, |
| consequentSetBuilder); |
| addConsequentRootSet(ifRuleEvaluator.run(), false); |
| assert getNumberOfLiveItems() == numberOfLiveItemsAfterProcessing; |
| if (!workList.isEmpty()) { |
| continue; |
| } |
| } |
| |
| // Continue fix-point processing while there are additional work items to ensure items that |
| // are passed to Java reflections are traced. |
| if (!pendingReflectiveUses.isEmpty()) { |
| pendingReflectiveUses.forEach(this::handleReflectiveBehavior); |
| pendingReflectiveUses.clear(); |
| } |
| if (!workList.isEmpty()) { |
| continue; |
| } |
| |
| // Notify each analysis that a fixpoint has been reached, and give each analysis an |
| // opportunity to add items to the worklist. |
| analyses.forEach(analysis -> analysis.notifyFixpoint(this, workList, timing)); |
| if (!workList.isEmpty()) { |
| continue; |
| } |
| |
| addConsequentRootSet(computeDelayedInterfaceMethodSyntheticBridges(), true); |
| rootSet.delayedRootSetActionItems.clear(); |
| |
| if (!workList.isEmpty()) { |
| continue; |
| } |
| |
| synthesize(); |
| if (!workList.isEmpty()) { |
| continue; |
| } |
| |
| // Reached the fixpoint. |
| break; |
| } |
| |
| if (Log.ENABLED) { |
| Set<DexEncodedMethod> allLive = Sets.newIdentityHashSet(); |
| 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); |
| 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); |
| } |
| } finally { |
| timing.end(); |
| } |
| unpinLambdaMethods(); |
| } |
| |
| private long getNumberOfLiveItems() { |
| long result = liveTypes.items.size(); |
| result += liveMethods.items.size(); |
| result += liveFields.items.size(); |
| return result; |
| } |
| |
| private void addConsequentRootSet(ConsequentRootSet consequentRootSet, boolean addNoShrinking) { |
| // TODO(b/132600955): This modifies the root set. Should the consequent be persistent? |
| rootSet.addConsequentRootSet(consequentRootSet, addNoShrinking); |
| enqueueRootItems(consequentRootSet.noShrinking); |
| // TODO(b/132828740): Seems incorrect that the precondition is not always met here. |
| consequentRootSet.dependentNoShrinking.forEach( |
| (precondition, dependentItems) -> enqueueRootItems(dependentItems)); |
| // Check for compatibility rules indicating that the holder must be implicitly kept. |
| if (forceProguardCompatibility) { |
| consequentRootSet.dependentKeepClassCompatRule.forEach( |
| (precondition, compatRules) -> { |
| assert precondition.isDexType(); |
| DexClass preconditionHolder = appView.definitionFor(precondition.asDexType()); |
| compatEnqueueHolderIfDependentNonStaticMember(preconditionHolder, compatRules); |
| }); |
| } |
| } |
| |
| private ConsequentRootSet computeDelayedInterfaceMethodSyntheticBridges() { |
| RootSetBuilder builder = new RootSetBuilder(appView); |
| for (DelayedRootSetActionItem delayedRootSetActionItem : rootSet.delayedRootSetActionItems) { |
| if (delayedRootSetActionItem.isInterfaceMethodSyntheticBridgeAction()) { |
| handleInterfaceMethodSyntheticBridgeAction( |
| delayedRootSetActionItem.asInterfaceMethodSyntheticBridgeAction(), builder); |
| } |
| } |
| return builder.buildConsequentRootSet(); |
| } |
| |
| private Map<DexMethod, ProgramMethod> syntheticInterfaceMethodBridges = new IdentityHashMap<>(); |
| |
| private void handleInterfaceMethodSyntheticBridgeAction( |
| InterfaceMethodSyntheticBridgeAction action, RootSetBuilder builder) { |
| ProgramMethod methodToKeep = action.getMethodToKeep(); |
| ProgramMethod singleTarget = action.getSingleTarget(); |
| DexEncodedMethod singleTargetMethod = singleTarget.getMethod(); |
| if (rootSet.noShrinking.containsKey(singleTargetMethod.method)) { |
| return; |
| } |
| if (methodToKeep != singleTarget) { |
| assert null == methodToKeep.getHolder().lookupMethod(methodToKeep.getMethod().method); |
| ProgramMethod old = |
| syntheticInterfaceMethodBridges.put(methodToKeep.getMethod().method, methodToKeep); |
| if (old == null) { |
| if (singleTargetMethod.isLibraryMethodOverride().isTrue()) { |
| methodToKeep.getMethod().setLibraryMethodOverride(OptionalBool.TRUE); |
| } |
| DexProgramClass singleTargetHolder = singleTarget.getHolder(); |
| assert singleTargetHolder.isInterface(); |
| markVirtualMethodAsReachable( |
| singleTargetMethod.method, |
| singleTargetHolder.isInterface(), |
| null, |
| graphReporter.fakeReportShouldNotBeUsed()); |
| enqueueMarkMethodLiveAction( |
| singleTargetHolder, singleTargetMethod, graphReporter.fakeReportShouldNotBeUsed()); |
| } |
| } |
| action.getAction().accept(builder); |
| } |
| |
| private void unpinLambdaMethods() { |
| assert desugaredLambdaImplementationMethods.isEmpty() |
| || options.desugarState == DesugarState.ON; |
| for (DexMethod method : desugaredLambdaImplementationMethods) { |
| pinnedItems.remove(method); |
| rootSet.prune(method); |
| } |
| desugaredLambdaImplementationMethods.clear(); |
| } |
| |
| void retainAnnotationForFinalTreeShaking(DexAnnotation annotation) { |
| assert mode.isInitialTreeShaking(); |
| if (annotationRemoverBuilder != null) { |
| annotationRemoverBuilder.retainAnnotation(annotation); |
| } |
| } |
| |
| // Package protected due to entry point from worklist. |
| void markMethodAsKept(DexProgramClass holder, DexEncodedMethod target, KeepReason reason) { |
| DexMethod method = target.method; |
| 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(method, holder.isInterface(), null, reason); |
| if (holder.isInterface()) { |
| // 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 |
| // the current compilation unit. Keep either the default-method or its implementation |
| // method. |
| // TODO(b/120959039): Codify the kept-graph expectations for these cases in tests. |
| if (target.isNonAbstractVirtualMethod()) { |
| markVirtualMethodAsLive(holder, target, reason); |
| } else { |
| DexEncodedMethod implementation = target.getDefaultInterfaceMethodImplementation(); |
| if (implementation != null) { |
| DexProgramClass companion = getProgramClassOrNull(implementation.holder()); |
| markTypeAsLive(companion, graphReporter.reportCompanionClass(holder, companion)); |
| markVirtualMethodAsLive( |
| companion, |
| implementation, |
| graphReporter.reportCompanionMethod(target, implementation)); |
| } |
| } |
| } |
| } else { |
| markMethodAsTargeted(holder, target, reason); |
| markDirectStaticOrConstructorMethodAsLive(holder, target, reason); |
| } |
| } |
| |
| // Package protected due to entry point from worklist. |
| void markFieldAsKept(DexProgramClass holder, DexEncodedField target, KeepReason reason) { |
| assert holder.type == target.holder(); |
| if (target.accessFlags.isStatic()) { |
| markStaticFieldAsLive(target, reason); |
| } else { |
| markInstanceFieldAsReachable(target, reason); |
| } |
| } |
| |
| private boolean shouldMarkLibraryMethodOverrideAsReachable(LookupTarget override) { |
| if (override.isLambdaTarget()) { |
| return true; |
| } |
| ProgramMethod programMethod = override.asMethodTarget().asProgramMethod(); |
| if (programMethod == null) { |
| return false; |
| } |
| DexProgramClass clazz = programMethod.getHolder(); |
| DexEncodedMethod method = programMethod.getMethod(); |
| assert method.isVirtualMethod(); |
| |
| if (method.isAbstract() || method.isPrivateMethod()) { |
| return false; |
| } |
| |
| if (appView.isClassEscapingIntoLibrary(clazz.type)) { |
| return true; |
| } |
| |
| // If there is an instantiated subtype of `clazz` that escapes into the library and does not |
| // override `method` then we need to mark the method as being reachable. |
| Set<DexProgramClass> immediateSubtypes = getImmediateSubtypesInInstantiatedHierarchy(clazz); |
| if (immediateSubtypes.isEmpty()) { |
| return false; |
| } |
| Deque<DexProgramClass> worklist = new ArrayDeque<>(immediateSubtypes); |
| Set<DexProgramClass> visited = SetUtils.newIdentityHashSet(immediateSubtypes); |
| |
| while (!worklist.isEmpty()) { |
| DexProgramClass current = worklist.removeFirst(); |
| assert visited.contains(current); |
| |
| if (current.lookupVirtualMethod(method.method) != null) { |
| continue; |
| } |
| |
| if (appView.isClassEscapingIntoLibrary(current.type)) { |
| return true; |
| } |
| |
| for (DexProgramClass subtype : getImmediateSubtypesInInstantiatedHierarchy(current)) { |
| if (visited.add(subtype)) { |
| worklist.add(subtype); |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| private Set<DexProgramClass> getImmediateSubtypesInInstantiatedHierarchy(DexProgramClass clazz) { |
| Set<DexClass> subtypes = |
| objectAllocationInfoCollection.getImmediateSubtypesInInstantiatedHierarchy(clazz.type); |
| if (subtypes == null) { |
| return Collections.emptySet(); |
| } |
| Set<DexProgramClass> programClasses = SetUtils.newIdentityHashSet(subtypes.size()); |
| for (DexClass subtype : subtypes) { |
| if (subtype.isProgramClass()) { |
| programClasses.add(subtype.asProgramClass()); |
| } |
| } |
| return programClasses; |
| } |
| |
| // Package protected due to entry point from worklist. |
| void markMethodAsLive(DexEncodedMethod method, KeepReason reason) { |
| assert liveMethods.contains(method); |
| |
| DexProgramClass clazz = getProgramClassOrNull(method.holder()); |
| if (clazz == null) { |
| return; |
| } |
| |
| if (method.isStatic()) { |
| markDirectAndIndirectClassInitializersAsLive(clazz); |
| } |
| |
| 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); |
| } |
| DexProgramClass targetClass = getProgramClassOrNull(superCallTarget.holder()); |
| assert targetClass != null; |
| if (targetClass != null) { |
| markMethodAsTargeted( |
| targetClass, superCallTarget, KeepReason.invokedViaSuperFrom(method)); |
| markVirtualMethodAsLive( |
| targetClass, superCallTarget, KeepReason.invokedViaSuperFrom(method)); |
| } |
| } |
| } |
| markParameterAndReturnTypesAsLive(method); |
| processAnnotations(clazz, method); |
| method.parameterAnnotationsList.forEachAnnotation( |
| annotation -> processAnnotation(clazz, method, annotation)); |
| method.registerCodeReferences(useRegistryFactory.create(appView, clazz, method, this)); |
| |
| // Add all dependent members to the workqueue. |
| enqueueRootItems(rootSet.getDependentItems(method)); |
| |
| // Notify analyses. |
| analyses.forEach(analysis -> analysis.processNewlyLiveMethod(method)); |
| } |
| |
| private void markReferencedTypesAsLive(DexEncodedMethod method) { |
| markTypeAsLive( |
| method.holder(), clazz -> graphReporter.reportClassReferencedFrom(clazz, method)); |
| markParameterAndReturnTypesAsLive(method); |
| } |
| |
| |
| private void markParameterAndReturnTypesAsLive(DexEncodedMethod method) { |
| for (DexType parameterType : method.method.proto.parameters.values) { |
| markTypeAsLive( |
| parameterType, clazz -> graphReporter.reportClassReferencedFrom(clazz, method)); |
| } |
| markTypeAsLive( |
| method.method.proto.returnType, |
| clazz -> graphReporter.reportClassReferencedFrom(clazz, method)); |
| } |
| |
| private void markClassAsInstantiatedWithReason(DexProgramClass clazz, KeepReason reason) { |
| workList.enqueueMarkInstantiatedAction(clazz, null, InstantiationReason.REFLECTION, reason); |
| if (clazz.hasDefaultInitializer()) { |
| workList.enqueueMarkReachableDirectAction(clazz.getDefaultInitializer().method, reason); |
| } |
| } |
| |
| private void markClassAsInstantiatedWithCompatRule( |
| DexProgramClass clazz, KeepReasonWitness witness) { |
| if (clazz.isAnnotation()) { |
| markTypeAsLive(clazz, witness); |
| } else if (clazz.isInterface()) { |
| markInterfaceAsInstantiated(clazz, witness); |
| } else { |
| workList.enqueueMarkInstantiatedAction(clazz, null, InstantiationReason.KEEP_RULE, witness); |
| if (clazz.hasDefaultInitializer()) { |
| DexEncodedMethod defaultInitializer = clazz.getDefaultInitializer(); |
| workList.enqueueMarkReachableDirectAction( |
| defaultInitializer.method, |
| graphReporter.reportCompatKeepDefaultInitializer(clazz, defaultInitializer)); |
| } |
| } |
| } |
| |
| private void markMethodAsLiveWithCompatRule(DexProgramClass clazz, DexEncodedMethod method) { |
| enqueueMarkMethodLiveAction(clazz, method, graphReporter.reportCompatKeepMethod(clazz, method)); |
| } |
| |
| private void handleReflectiveBehavior(DexEncodedMethod method) { |
| DexType originHolder = method.holder(); |
| Origin origin = appInfo.originFor(originHolder); |
| IRCode code = method.buildIR(appView, origin); |
| InstructionIterator iterator = code.instructionIterator(); |
| while (iterator.hasNext()) { |
| Instruction instruction = iterator.next(); |
| handleReflectiveBehavior(method, instruction); |
| } |
| } |
| |
| private void handleReflectiveBehavior(DexEncodedMethod method, Instruction instruction) { |
| if (!instruction.isInvokeMethod()) { |
| return; |
| } |
| InvokeMethod invoke = instruction.asInvokeMethod(); |
| DexMethod invokedMethod = invoke.getInvokedMethod(); |
| DexItemFactory dexItemFactory = appView.dexItemFactory(); |
| if (invokedMethod == dexItemFactory.classMethods.newInstance) { |
| handleJavaLangClassNewInstance(method, invoke); |
| return; |
| } |
| if (invokedMethod == dexItemFactory.constructorMethods.newInstance) { |
| handleJavaLangReflectConstructorNewInstance(method, invoke); |
| return; |
| } |
| if (invokedMethod == dexItemFactory.enumMethods.valueOf) { |
| handleJavaLangEnumValueOf(method, invoke); |
| return; |
| } |
| if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) { |
| handleJavaLangReflectProxyNewProxyInstance(method, invoke); |
| return; |
| } |
| if (dexItemFactory.serviceLoaderMethods.isLoadMethod(invokedMethod)) { |
| handleServiceLoaderInvocation(method, invoke); |
| return; |
| } |
| if (!isReflectionMethod(dexItemFactory, invokedMethod)) { |
| return; |
| } |
| DexReference identifierItem = identifyIdentifier(invoke, appView); |
| if (identifierItem == null) { |
| return; |
| } |
| if (identifierItem.isDexType()) { |
| DexProgramClass clazz = getProgramClassOrNull(identifierItem.asDexType()); |
| if (clazz == null) { |
| return; |
| } |
| if (clazz.isAnnotation() || clazz.isInterface()) { |
| markTypeAsLive(clazz.type, KeepReason.reflectiveUseIn(method)); |
| } else { |
| workList.enqueueMarkInstantiatedAction( |
| clazz, null, InstantiationReason.REFLECTION, KeepReason.reflectiveUseIn(method)); |
| if (clazz.hasDefaultInitializer()) { |
| DexEncodedMethod initializer = clazz.getDefaultInitializer(); |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| markMethodAsTargeted(clazz, initializer, reason); |
| markDirectStaticOrConstructorMethodAsLive(clazz, initializer, reason); |
| } |
| } |
| } else if (identifierItem.isDexField()) { |
| DexField field = identifierItem.asDexField(); |
| DexProgramClass clazz = getProgramClassOrNull(field.holder); |
| if (clazz == null) { |
| return; |
| } |
| DexEncodedField encodedField = appView.definitionFor(field); |
| if (encodedField == null) { |
| return; |
| } |
| // Normally, we generate a -keepclassmembers rule for the field, such that the field is only |
| // kept if it is a static field, or if the holder or one of its subtypes are instantiated. |
| // However, if the invoked method is a field updater, then we always need to keep instance |
| // fields since the creation of a field updater throws a NoSuchFieldException if the field |
| // is not present. |
| boolean keepClass = |
| !encodedField.accessFlags.isStatic() |
| && dexItemFactory.atomicFieldUpdaterMethods.isFieldUpdater(invokedMethod); |
| if (keepClass) { |
| workList.enqueueMarkInstantiatedAction( |
| clazz, null, InstantiationReason.REFLECTION, KeepReason.reflectiveUseIn(method)); |
| } |
| if (pinnedItems.add(encodedField.field)) { |
| markFieldAsKept(clazz, encodedField, KeepReason.reflectiveUseIn(method)); |
| } |
| } else { |
| assert identifierItem.isDexMethod(); |
| DexMethod targetedMethod = identifierItem.asDexMethod(); |
| DexProgramClass clazz = getProgramClassOrNull(targetedMethod.holder); |
| if (clazz == null) { |
| return; |
| } |
| DexEncodedMethod encodedMethod = appView.definitionFor(targetedMethod); |
| if (encodedMethod == null) { |
| return; |
| } |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| if (encodedMethod.accessFlags.isStatic() || encodedMethod.accessFlags.isConstructor()) { |
| markMethodAsTargeted(clazz, encodedMethod, reason); |
| markDirectStaticOrConstructorMethodAsLive(clazz, encodedMethod, reason); |
| } else { |
| markVirtualMethodAsLive(clazz, encodedMethod, reason); |
| } |
| } |
| } |
| |
| /** Handles reflective uses of {@link Class#newInstance()}. */ |
| private void handleJavaLangClassNewInstance(DexEncodedMethod method, InvokeMethod invoke) { |
| if (!invoke.isInvokeVirtual()) { |
| assert false; |
| return; |
| } |
| |
| DexType instantiatedType = |
| ConstantValueUtils.getDexTypeRepresentedByValue( |
| invoke.asInvokeVirtual().getReceiver(), appView); |
| if (instantiatedType == null || !instantiatedType.isClassType()) { |
| // Give up, we can't tell which class is being instantiated, or the type is not a class type. |
| // The latter should not happen in practice. |
| return; |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(instantiatedType); |
| if (clazz == null) { |
| return; |
| } |
| DexEncodedMethod defaultInitializer = clazz.getDefaultInitializer(); |
| if (defaultInitializer != null) { |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| markClassAsInstantiatedWithReason(clazz, reason); |
| markMethodAsTargeted(clazz, defaultInitializer, reason); |
| markDirectStaticOrConstructorMethodAsLive(clazz, defaultInitializer, reason); |
| } |
| } |
| |
| /** Handles reflective uses of {@link java.lang.reflect.Constructor#newInstance(Object...)}. */ |
| private void handleJavaLangReflectConstructorNewInstance( |
| DexEncodedMethod method, InvokeMethod invoke) { |
| if (!invoke.isInvokeVirtual()) { |
| assert false; |
| return; |
| } |
| |
| Value constructorValue = invoke.asInvokeVirtual().getReceiver().getAliasedValue(); |
| if (constructorValue.isPhi() || !constructorValue.definition.isInvokeVirtual()) { |
| // Give up, we can't tell which class is being instantiated. |
| return; |
| } |
| |
| InvokeVirtual constructorDefinition = constructorValue.definition.asInvokeVirtual(); |
| if (constructorDefinition.getInvokedMethod() |
| != appView.dexItemFactory().classMethods.getDeclaredConstructor) { |
| // Give up, we can't tell which constructor is being invoked. |
| return; |
| } |
| |
| DexType instantiatedType = |
| ConstantValueUtils.getDexTypeRepresentedByValue( |
| constructorDefinition.getReceiver(), appView); |
| if (instantiatedType == null || !instantiatedType.isClassType()) { |
| // Give up, we can't tell which constructor is being invoked, or the type is not a class type. |
| // The latter should not happen in practice. |
| return; |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(instantiatedType); |
| if (clazz == null) { |
| return; |
| } |
| Value parametersValue = constructorDefinition.inValues().get(1); |
| if (parametersValue.isPhi() || !parametersValue.definition.isNewArrayEmpty()) { |
| // Give up, we can't tell which constructor is being invoked. |
| return; |
| } |
| |
| Value parametersSizeValue = parametersValue.definition.asNewArrayEmpty().size(); |
| if (parametersSizeValue.isPhi() || !parametersSizeValue.definition.isConstNumber()) { |
| // Give up, we can't tell which constructor is being invoked. |
| return; |
| } |
| |
| DexEncodedMethod initializer = null; |
| |
| int parametersSize = parametersSizeValue.definition.asConstNumber().getIntValue(); |
| if (parametersSize == 0) { |
| initializer = clazz.getDefaultInitializer(); |
| } else { |
| DexType[] parameterTypes = new DexType[parametersSize]; |
| int missingIndices = parametersSize; |
| for (Instruction user : parametersValue.uniqueUsers()) { |
| if (user.isArrayPut()) { |
| ArrayPut arrayPutInstruction = user.asArrayPut(); |
| if (arrayPutInstruction.array() != parametersValue) { |
| return; |
| } |
| |
| Value indexValue = arrayPutInstruction.index(); |
| if (indexValue.isPhi() || !indexValue.definition.isConstNumber()) { |
| return; |
| } |
| int index = indexValue.definition.asConstNumber().getIntValue(); |
| if (index >= parametersSize) { |
| return; |
| } |
| |
| DexType type = |
| ConstantValueUtils.getDexTypeRepresentedByValue(arrayPutInstruction.value(), appView); |
| if (type == null) { |
| return; |
| } |
| |
| if (parameterTypes[index] == type) { |
| continue; |
| } |
| if (parameterTypes[index] != null) { |
| return; |
| } |
| parameterTypes[index] = type; |
| missingIndices--; |
| } |
| } |
| |
| if (missingIndices == 0) { |
| initializer = clazz.getInitializer(parameterTypes); |
| } |
| } |
| |
| if (initializer != null) { |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| markClassAsInstantiatedWithReason(clazz, reason); |
| markMethodAsTargeted(clazz, initializer, reason); |
| markDirectStaticOrConstructorMethodAsLive(clazz, initializer, reason); |
| } |
| } |
| |
| /** |
| * Handles reflective uses of {@link java.lang.reflect.Proxy#newProxyInstance(ClassLoader, |
| * Class[], InvocationHandler)}. |
| */ |
| private void handleJavaLangReflectProxyNewProxyInstance( |
| DexEncodedMethod method, InvokeMethod invoke) { |
| if (!invoke.isInvokeStatic()) { |
| assert false; |
| return; |
| } |
| |
| Value interfacesValue = invoke.arguments().get(1); |
| if (interfacesValue.isPhi() || !interfacesValue.definition.isNewArrayEmpty()) { |
| // Give up, we can't tell which interfaces the proxy implements. |
| return; |
| } |
| |
| for (Instruction user : interfacesValue.uniqueUsers()) { |
| if (!user.isArrayPut()) { |
| continue; |
| } |
| |
| ArrayPut arrayPut = user.asArrayPut(); |
| DexType type = ConstantValueUtils.getDexTypeRepresentedByValue(arrayPut.value(), appView); |
| if (type == null || !type.isClassType()) { |
| continue; |
| } |
| |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz != null && clazz.isInterface()) { |
| // Add this interface to the set of pinned items to ensure that we do not merge the |
| // interface into its unique subtype, if any. |
| // TODO(b/145344105): This should be superseded by the unknown interface hierarchy. |
| pinnedItems.add(clazz.type); |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| markInterfaceAsInstantiated(clazz, graphReporter.registerClass(clazz, reason)); |
| |
| // Also pin all of its virtual methods to ensure that the devirtualizer does not perform |
| // illegal rewritings of invoke-interface instructions into invoke-virtual instructions. |
| for (DexEncodedMethod virtualMethod : clazz.virtualMethods()) { |
| pinnedItems.add(virtualMethod.method); |
| markVirtualMethodAsReachable(virtualMethod.method, true, null, reason); |
| } |
| } |
| } |
| } |
| |
| private void handleJavaLangEnumValueOf(DexEncodedMethod method, InvokeMethod invoke) { |
| // The use of java.lang.Enum.valueOf(java.lang.Class, java.lang.String) will indirectly |
| // access the values() method of the enum class passed as the first argument. The method |
| // SomeEnumClass.valueOf(java.lang.String) which is generated by javac for all enums will |
| // call this method. |
| if (invoke.inValues().get(0).isConstClass()) { |
| DexType type = invoke.inValues().get(0).definition.asConstClass().getValue(); |
| DexProgramClass clazz = getProgramClassOrNull(type); |
| if (clazz != null && clazz.accessFlags.isEnum()) { |
| DexProgramClass holder = getProgramClassOrNull(method.holder()); |
| markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(holder, method)); |
| } |
| } |
| } |
| |
| private void handleServiceLoaderInvocation(DexEncodedMethod method, InvokeMethod invoke) { |
| if (invoke.inValues().size() == 0) { |
| // Should never happen. |
| return; |
| } |
| |
| Value argument = invoke.inValues().get(0).getAliasedValue(); |
| if (!argument.isPhi() && argument.definition.isConstClass()) { |
| DexType serviceType = argument.definition.asConstClass().getValue(); |
| if (!appView.appServices().allServiceTypes().contains(serviceType)) { |
| // Should never happen. |
| if (Log.ENABLED) { |
| options.reporter.warning( |
| new StringDiagnostic( |
| "The type `" |
| + serviceType.toSourceString() |
| + "` is being passed to the method `" |
| + invoke.getInvokedMethod().toSourceString() |
| + "`, but was not found in `META-INF/services/`.", |
| appInfo.originFor(method.holder()))); |
| } |
| return; |
| } |
| |
| handleServiceInstantiation(serviceType, KeepReason.reflectiveUseIn(method)); |
| } else { |
| KeepReason reason = KeepReason.reflectiveUseIn(method); |
| for (DexType serviceType : appView.appServices().allServiceTypes()) { |
| handleServiceInstantiation(serviceType, reason); |
| } |
| } |
| } |
| |
| private void handleServiceInstantiation(DexType serviceType, KeepReason reason) { |
| instantiatedAppServices.add(serviceType); |
| |
| List<DexType> serviceImplementationTypes = |
| appView.appServices().serviceImplementationsFor(serviceType); |
| for (DexType serviceImplementationType : serviceImplementationTypes) { |
| if (!serviceImplementationType.isClassType()) { |
| // Should never happen. |
| continue; |
| } |
| |
| DexProgramClass serviceImplementationClass = getProgramClassOrNull(serviceImplementationType); |
| if (serviceImplementationClass != null && serviceImplementationClass.isProgramClass()) { |
| markClassAsInstantiatedWithReason(serviceImplementationClass, reason); |
| } |
| } |
| } |
| |
| private static class SetWithReportedReason<T> { |
| |
| private final Set<T> items = Sets.newIdentityHashSet(); |
| private final Map<T, List<Action>> deferredActions = new IdentityHashMap<>(); |
| |
| boolean add(T item, KeepReasonWitness witness) { |
| assert witness != null; |
| if (items.add(item)) { |
| deferredActions.getOrDefault(item, Collections.emptyList()).forEach(Action::execute); |
| return true; |
| } |
| return false; |
| } |
| |
| boolean contains(T item) { |
| return items.contains(item); |
| } |
| |
| boolean registerDeferredAction(T item, Action action) { |
| if (!items.contains(item)) { |
| deferredActions.computeIfAbsent(item, ignore -> new ArrayList<>()).add(action); |
| return true; |
| } |
| return false; |
| } |
| |
| Set<T> getItems() { |
| return Collections.unmodifiableSet(items); |
| } |
| } |
| |
| private class LiveMethodsSet { |
| |
| private final Set<DexEncodedMethod> items = Sets.newIdentityHashSet(); |
| |
| private final BiConsumer<DexEncodedMethod, KeepReason> register; |
| |
| LiveMethodsSet(BiConsumer<DexEncodedMethod, KeepReason> register) { |
| this.register = register; |
| } |
| |
| boolean add(DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) { |
| register.accept(method, reason); |
| transitionUnusedInterfaceToLive(clazz); |
| return items.add(method); |
| } |
| |
| boolean contains(DexEncodedMethod method) { |
| return items.contains(method); |
| } |
| |
| Set<DexEncodedMethod> getItems() { |
| return Collections.unmodifiableSet(items); |
| } |
| } |
| |
| private static class SetWithReason<T> { |
| |
| private final Set<T> items = Sets.newIdentityHashSet(); |
| |
| private final BiConsumer<T, KeepReason> register; |
| |
| public SetWithReason(BiConsumer<T, KeepReason> register) { |
| this.register = register; |
| } |
| |
| boolean add(T item, KeepReason reason) { |
| register.accept(item, reason); |
| return items.add(item); |
| } |
| |
| boolean contains(T item) { |
| return items.contains(item); |
| } |
| |
| Set<T> getItems() { |
| return Collections.unmodifiableSet(items); |
| } |
| } |
| |
| private class AnnotationReferenceMarker implements IndexedItemCollection { |
| |
| private final DexItem annotationHolder; |
| private final DexItemFactory dexItemFactory; |
| private final KeepReason reason; |
| |
| private AnnotationReferenceMarker( |
| DexItem annotationHolder, DexItemFactory dexItemFactory, KeepReason reason) { |
| this.annotationHolder = annotationHolder; |
| this.dexItemFactory = dexItemFactory; |
| this.reason = reason; |
| } |
| |
| @Override |
| public boolean addClass(DexProgramClass dexProgramClass) { |
| return false; |
| } |
| |
| @Override |
| public boolean addField(DexField field) { |
| recordTypeReference(field.holder); |
| recordTypeReference(field.type); |
| DexClass holder = appView.definitionFor(field.holder); |
| 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) { |
| if (!registerFieldRead(field, DexEncodedMethod.ANNOTATION_REFERENCE)) { |
| return false; |
| } |
| markStaticFieldAsLive(target, KeepReason.referencedInAnnotation(annotationHolder)); |
| // When an annotation has a field of an enum type with a default value then Java VM |
| // will use the values() method on that enum class. |
| if (options.isGeneratingClassFiles() |
| && annotationHolder == dexItemFactory.annotationDefault) { |
| DexProgramClass clazz = getProgramClassOrNull(field.type); |
| if (clazz != null && clazz.accessFlags.isEnum()) { |
| markEnumValuesAsReachable(clazz, 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(target, KeepReason.referencedInAnnotation(annotationHolder)); |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean addMethod(DexMethod method) { |
| // Record the references in case they are not program types. |
| recordMethodReference(method); |
| DexProgramClass holder = getProgramClassOrNull(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( |
| holder, 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(holder, 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, reason); |
| } |
| return false; |
| } |
| } |
| |
| } |