blob: 6cb5e129db0ad4bdc354180e1fdc8854d3eca3da [file] [log] [blame]
// Copyright (c) 2017, 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.verticalclassmerging;
import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import static com.android.tools.r8.utils.AndroidApiLevelUtils.getApiReferenceLevelForMerging;
import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
import com.android.tools.r8.androidapi.ComputedApiLevel;
import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.CfCode;
import com.android.tools.r8.graph.Code;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
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.ImmediateProgramSubtypingInfo;
import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.PrunedItems;
import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
import com.android.tools.r8.graph.lens.GraphLens;
import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.KeepInfoCollection;
import com.android.tools.r8.shaking.MainDexInfo;
import com.android.tools.r8.utils.Box;
import com.android.tools.r8.utils.FieldSignatureEquivalence;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.MethodSignatureEquivalence;
import com.android.tools.r8.utils.ObjectUtils;
import com.android.tools.r8.utils.Timing;
import com.android.tools.r8.utils.TraversalContinuation;
import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
import com.google.common.base.Equivalence;
import com.google.common.base.Equivalence.Wrapper;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
/**
* Merges Supertypes with a single implementation into their single subtype.
*
* <p>A common use-case for this is to merge an interface into its single implementation.
*
* <p>The class merger only fixes the structure of the graph but leaves the actual instructions
* untouched. Fixup of instructions is deferred via a {@link GraphLens} to the IR building phase.
*/
public class VerticalClassMerger {
private final AppView<AppInfoWithLiveness> appView;
private final DexItemFactory dexItemFactory;
private final InternalOptions options;
private Collection<DexMethod> invokes;
// Set of merge candidates. Note that this must have a deterministic iteration order.
private final Set<DexProgramClass> mergeCandidates = new LinkedHashSet<>();
// Map from source class to target class.
private final MutableBidirectionalManyToOneRepresentativeMap<DexType, DexType> mergedClasses =
BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedInterfaces =
BidirectionalManyToOneHashMap.newIdentityHashMap();
// Set of types that must not be merged into their subtype.
private final Set<DexProgramClass> pinnedClasses = Sets.newIdentityHashSet();
// The resulting graph lens that should be used after class merging.
private final VerticalClassMergerGraphLens.Builder lensBuilder;
// All the bridge methods that have been synthesized during vertical class merging.
private final List<SynthesizedBridgeCode> synthesizedBridges = new ArrayList<>();
private final MainDexInfo mainDexInfo;
public VerticalClassMerger(AppView<AppInfoWithLiveness> appView) {
AppInfoWithLiveness appInfo = appView.appInfo();
this.appView = appView;
this.dexItemFactory = appView.dexItemFactory();
this.options = appView.options();
this.mainDexInfo = appInfo.getMainDexInfo();
this.lensBuilder = new VerticalClassMergerGraphLens.Builder(dexItemFactory);
}
private void initializeMergeCandidates(ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
for (DexProgramClass sourceClass : appView.appInfo().classesWithDeterministicOrder()) {
List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(sourceClass);
if (subclasses.size() != 1) {
continue;
}
DexProgramClass targetClass = ListUtils.first(subclasses);
if (!isMergeCandidate(sourceClass, targetClass)) {
continue;
}
if (!isStillMergeCandidate(sourceClass, targetClass)) {
continue;
}
if (mergeMayLeadToIllegalAccesses(sourceClass, targetClass)) {
continue;
}
mergeCandidates.add(sourceClass);
}
}
// Returns a set of types that must not be merged into other types.
private void initializePinnedTypes() {
// For all pinned fields, also pin the type of the field (because changing the type of the field
// implicitly changes the signature of the field). Similarly, for all pinned methods, also pin
// the return type and the parameter types of the method.
// TODO(b/156715504): Compute referenced-by-pinned in the keep info objects.
List<DexReference> pinnedItems = new ArrayList<>();
KeepInfoCollection keepInfo = appView.getKeepInfo();
keepInfo.forEachPinnedType(pinnedItems::add, options);
keepInfo.forEachPinnedMethod(pinnedItems::add, options);
keepInfo.forEachPinnedField(pinnedItems::add, options);
extractPinnedItems(pinnedItems);
for (DexProgramClass clazz : appView.appInfo().classes()) {
if (Iterables.any(clazz.methods(), method -> method.getAccessFlags().isNative())) {
markClassAsPinned(clazz);
}
}
// It is valid to have an invoke-direct instruction in a default interface method that targets
// another default method in the same interface (see InterfaceMethodDesugaringTests.testInvoke-
// SpecialToDefaultMethod). However, in a class, that would lead to a verification error.
// Therefore, we disallow merging such interfaces into their subtypes.
for (DexMethod signature : appView.appInfo().getVirtualMethodsTargetedByInvokeDirect()) {
markTypeAsPinned(signature.getHolderType());
}
// The set of targets that must remain for proper resolution error cases should not be merged.
// TODO(b/192821424): Can be removed if handled.
extractPinnedItems(appView.appInfo().getFailedMethodResolutionTargets());
}
private <T extends DexReference> void extractPinnedItems(Iterable<T> items) {
for (DexReference item : items) {
if (item.isDexType()) {
markTypeAsPinned(item.asDexType());
} else if (item.isDexField()) {
// Pin the holder and the type of the field.
DexField field = item.asDexField();
markTypeAsPinned(field.getHolderType());
markTypeAsPinned(field.getType());
} else {
assert item.isDexMethod();
// Pin the holder, the return type and the parameter types of the method. If we were to
// merge any of these types into their sub classes, then we would implicitly change the
// signature of this method.
DexMethod method = item.asDexMethod();
markTypeAsPinned(method.getHolderType());
markTypeAsPinned(method.getReturnType());
for (DexType parameterType : method.getParameters()) {
markTypeAsPinned(parameterType);
}
}
}
}
private void markTypeAsPinned(DexType type) {
DexType baseType = type.toBaseType(dexItemFactory);
if (!baseType.isClassType() || appView.appInfo().isPinnedWithDefinitionLookup(baseType)) {
// We check for the case where the type is pinned according to appInfo.isPinned,
// so we only need to add it here if it is not the case.
return;
}
DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(baseType));
if (clazz != null) {
markClassAsPinned(clazz);
}
}
private void markClassAsPinned(DexProgramClass clazz) {
pinnedClasses.add(clazz);
}
// Returns true if [clazz] is a merge candidate. Note that the result of the checks in this
// method do not change in response to any class merges.
private boolean isMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
assert targetClass != null;
ObjectAllocationInfoCollection allocationInfo =
appView.appInfo().getObjectAllocationInfoCollection();
if (allocationInfo.isInstantiatedDirectly(sourceClass)
|| allocationInfo.isInterfaceWithUnknownSubtypeHierarchy(sourceClass)
|| allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass)
|| appView.getKeepInfo(sourceClass).isPinned(options)
|| pinnedClasses.contains(sourceClass)
|| appView.appInfo().isNoVerticalClassMergingOfType(sourceClass)) {
return false;
}
assert sourceClass
.traverseProgramMembers(
member -> {
assert !appView.getKeepInfo(member).isPinned(options);
return TraversalContinuation.doContinue();
})
.shouldContinue();
if (!FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
sourceClass, targetClass, appView)) {
return false;
}
if (appView.appServices().allServiceTypes().contains(sourceClass.getType())
&& appView.getKeepInfo(targetClass).isPinned(options)) {
return false;
}
if (sourceClass.isAnnotation()) {
return false;
}
if (!sourceClass.isInterface()
&& targetClass.isSerializable(appView)
&& !appView.appInfo().isSerializable(sourceClass.getType())) {
// https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html
// 1.10 The Serializable Interface
// ...
// A Serializable class must do the following:
// ...
// * Have access to the no-arg constructor of its first non-serializable superclass
return false;
}
// If there is a constructor in the target, make sure that all source constructors can be
// inlined.
if (!Iterables.isEmpty(targetClass.programInstanceInitializers())) {
TraversalContinuation<?, ?> result =
sourceClass.traverseProgramInstanceInitializers(
method -> TraversalContinuation.breakIf(disallowInlining(method, targetClass)));
if (result.shouldBreak()) {
return false;
}
}
if (sourceClass.getEnclosingMethodAttribute() != null
|| !sourceClass.getInnerClasses().isEmpty()) {
// TODO(b/147504070): Consider merging of enclosing-method and inner-class attributes.
return false;
}
// We abort class merging when merging across nests or from a nest to non-nest.
// Without nest this checks null == null.
if (ObjectUtils.notIdentical(targetClass.getNestHost(), sourceClass.getNestHost())) {
return false;
}
// If there is an invoke-special to a default interface method and we are not merging into an
// interface, then abort, since invoke-special to a virtual class method requires desugaring.
if (sourceClass.isInterface() && !targetClass.isInterface()) {
TraversalContinuation<?, ?> result =
sourceClass.traverseProgramMethods(
method -> {
boolean foundInvokeSpecialToDefaultLibraryMethod =
method.registerCodeReferencesWithResult(
new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method));
return TraversalContinuation.breakIf(foundInvokeSpecialToDefaultLibraryMethod);
});
if (result.shouldBreak()) {
return false;
}
}
return true;
}
// Returns true if [clazz] is a merge candidate. Note that the result of the checks in this
// method may change in response to class merges. Therefore, this method should always be called
// before merging [clazz] into its subtype.
private boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
assert isMergeCandidate(sourceClass, targetClass);
assert !mergedClasses.containsValue(sourceClass.getType());
// For interface types, this is more complicated, see:
// https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5
// We basically can't move the clinit, since it is not called when implementing classes have
// their clinit called - except when the interface has a default method.
if ((sourceClass.hasClassInitializer() && targetClass.hasClassInitializer())
|| targetClass.classInitializationMayHaveSideEffects(
appView, type -> type.isIdenticalTo(sourceClass.getType()))
|| (sourceClass.isInterface()
&& sourceClass.classInitializationMayHaveSideEffects(appView))) {
// TODO(herhut): Handle class initializers.
return false;
}
boolean sourceCanBeSynchronizedOn =
appView.appInfo().isLockCandidate(sourceClass)
|| sourceClass.hasStaticSynchronizedMethods();
boolean targetCanBeSynchronizedOn =
appView.appInfo().isLockCandidate(targetClass)
|| targetClass.hasStaticSynchronizedMethods();
if (sourceCanBeSynchronizedOn && targetCanBeSynchronizedOn) {
return false;
}
if (targetClass.getEnclosingMethodAttribute() != null
|| !targetClass.getInnerClasses().isEmpty()) {
// TODO(b/147504070): Consider merging of enclosing-method and inner-class attributes.
return false;
}
if (methodResolutionMayChange(sourceClass, targetClass)) {
return false;
}
// Field resolution first considers the direct interfaces of [targetClass] before it proceeds
// to the super class.
if (fieldResolutionMayChange(sourceClass, targetClass)) {
return false;
}
// Only merge if api reference level of source class is equal to target class. The check is
// somewhat expensive.
if (appView.options().apiModelingOptions().isApiCallerIdentificationEnabled()) {
AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
ComputedApiLevel sourceApiLevel =
getApiReferenceLevelForMerging(apiLevelCompute, sourceClass);
ComputedApiLevel targetApiLevel =
getApiReferenceLevelForMerging(apiLevelCompute, targetClass);
if (!sourceApiLevel.equals(targetApiLevel)) {
return false;
}
}
return true;
}
private boolean mergeMayLeadToIllegalAccesses(DexProgramClass source, DexProgramClass target) {
if (source.isSamePackage(target)) {
// When merging two classes from the same package, we only need to make sure that [source]
// does not get less visible, since that could make a valid access to [source] from another
// package illegal after [source] has been merged into [target].
assert source.getAccessFlags().isPackagePrivateOrPublic();
assert target.getAccessFlags().isPackagePrivateOrPublic();
// TODO(b/287891322): Allow merging if `source` is only accessed from inside its own package.
return source.getAccessFlags().isPublic() && target.getAccessFlags().isPackagePrivate();
}
// Check that all accesses to [source] and its members from inside the current package of
// [source] will continue to work. This is guaranteed if [target] is public and all members of
// [source] are either private or public.
//
// (Deliberately not checking all accesses to [source] since that would be expensive.)
if (!target.isPublic()) {
return true;
}
for (DexType sourceInterface : source.getInterfaces()) {
DexClass sourceInterfaceClass = appView.definitionFor(sourceInterface);
if (sourceInterfaceClass != null && !sourceInterfaceClass.isPublic()) {
return true;
}
}
for (DexEncodedField field : source.fields()) {
if (!(field.isPublic() || field.isPrivate())) {
return true;
}
}
for (DexEncodedMethod method : source.methods()) {
if (!(method.isPublic() || method.isPrivate())) {
return true;
}
// Check if the target is overriding and narrowing the access.
if (method.isPublic()) {
DexEncodedMethod targetOverride = target.lookupVirtualMethod(method.getReference());
if (targetOverride != null && !targetOverride.isPublic()) {
return true;
}
}
}
// Check that all accesses from [source] to classes or members from the current package of
// [source] will continue to work. This is guaranteed if the methods of [source] do not access
// any private or protected classes or members from the current package of [source].
TraversalContinuation<?, ?> result =
source.traverseProgramMethods(
method -> {
boolean foundIllegalAccess =
method.registerCodeReferencesWithResult(
new IllegalAccessDetector(appView, method));
if (foundIllegalAccess) {
return TraversalContinuation.doBreak();
}
return TraversalContinuation.doContinue();
});
return result.shouldBreak();
}
private Collection<DexMethod> getInvokes(ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
if (invokes == null) {
invokes = new OverloadedMethodSignaturesRetriever(immediateSubtypingInfo).get();
}
return invokes;
}
// Collects all potentially overloaded method signatures that reference at least one type that
// may be the source or target of a merge operation.
private class OverloadedMethodSignaturesRetriever {
private final Reference2BooleanOpenHashMap<DexProto> cache =
new Reference2BooleanOpenHashMap<>();
private final Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
private final Set<DexType> mergeeCandidates = new HashSet<>();
public OverloadedMethodSignaturesRetriever(
ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
for (DexProgramClass mergeCandidate : mergeCandidates) {
List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(mergeCandidate);
if (subclasses.size() == 1) {
mergeeCandidates.add(ListUtils.first(subclasses).getType());
}
}
}
public Collection<DexMethod> get() {
Map<DexString, DexProto> overloadingInfo = new HashMap<>();
// Find all signatures that may reference a type that could be the source or target of a
// merge operation.
Set<Wrapper<DexMethod>> filteredSignatures = new HashSet<>();
for (DexProgramClass clazz : appView.appInfo().classes()) {
for (DexEncodedMethod encodedMethod : clazz.methods()) {
DexMethod method = encodedMethod.getReference();
DexClass definition = appView.definitionFor(method.getHolderType());
if (definition != null
&& definition.isProgramClass()
&& protoMayReferenceMergedSourceOrTarget(method.getProto())) {
filteredSignatures.add(equivalence.wrap(method));
// Record that we have seen a method named [signature.name] with the proto
// [signature.proto]. If at some point, we find a method with the same name, but a
// different proto, it could be the case that a method with the given name is
// overloaded.
DexProto existing =
overloadingInfo.computeIfAbsent(method.getName(), key -> method.getProto());
if (existing.isNotIdenticalTo(DexProto.SENTINEL)
&& !existing.equals(method.getProto())) {
// Mark that this signature is overloaded by mapping it to SENTINEL.
overloadingInfo.put(method.getName(), DexProto.SENTINEL);
}
}
}
}
List<DexMethod> result = new ArrayList<>();
for (Wrapper<DexMethod> wrappedSignature : filteredSignatures) {
DexMethod signature = wrappedSignature.get();
// Ignore those method names that are definitely not overloaded since they cannot lead to
// any collisions.
if (overloadingInfo.get(signature.getName()).isIdenticalTo(DexProto.SENTINEL)) {
result.add(signature);
}
}
return result;
}
private boolean protoMayReferenceMergedSourceOrTarget(DexProto proto) {
boolean result;
if (cache.containsKey(proto)) {
result = cache.getBoolean(proto);
} else {
result = false;
if (typeMayReferenceMergedSourceOrTarget(proto.getReturnType())) {
result = true;
} else {
for (DexType type : proto.getParameters()) {
if (typeMayReferenceMergedSourceOrTarget(type)) {
result = true;
break;
}
}
}
cache.put(proto, result);
}
return result;
}
private boolean typeMayReferenceMergedSourceOrTarget(DexType type) {
type = type.toBaseType(dexItemFactory);
if (type.isClassType()) {
if (mergeeCandidates.contains(type)) {
return true;
}
DexClass clazz = appView.definitionFor(type);
if (clazz != null && clazz.isProgramClass()) {
return mergeCandidates.contains(clazz.asProgramClass());
}
}
return false;
}
}
public static void runIfNecessary(
AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
throws ExecutionException {
timing.begin("VerticalClassMerger");
if (shouldRun(appView)) {
new VerticalClassMerger(appView).run(executorService, timing);
} else {
appView.setVerticallyMergedClasses(VerticallyMergedClasses.empty());
}
assert appView.hasVerticallyMergedClasses();
assert ArtProfileCompletenessChecker.verify(appView);
timing.end();
}
private static boolean shouldRun(AppView<AppInfoWithLiveness> appView) {
return appView.options().getVerticalClassMergerOptions().isEnabled()
&& !appView.hasCfByteCodePassThroughMethods();
}
private void run(ExecutorService executorService, Timing timing) throws ExecutionException {
ImmediateProgramSubtypingInfo immediateSubtypingInfo =
ImmediateProgramSubtypingInfo.create(appView);
initializePinnedTypes(); // Must be initialized prior to mergeCandidates.
initializeMergeCandidates(immediateSubtypingInfo);
timing.begin("merge");
// Visit the program classes in a top-down order according to the class hierarchy.
TopDownClassHierarchyTraversal.forProgramClasses(appView)
.visit(
mergeCandidates, clazz -> mergeClassIfPossible(clazz, immediateSubtypingInfo, timing));
timing.end();
VerticallyMergedClasses verticallyMergedClasses =
new VerticallyMergedClasses(mergedClasses, mergedInterfaces);
appView.setVerticallyMergedClasses(verticallyMergedClasses);
if (verticallyMergedClasses.isEmpty()) {
return;
}
timing.begin("fixup");
VerticalClassMergerGraphLens lens =
new VerticalClassMergerTreeFixer(
appView, lensBuilder, verticallyMergedClasses, synthesizedBridges)
.fixupTypeReferences();
KeepInfoCollection keepInfo = appView.getKeepInfo();
keepInfo.mutate(
mutator ->
mutator.removeKeepInfoForMergedClasses(
PrunedItems.builder().setRemovedClasses(mergedClasses.keySet()).build()));
timing.end();
assert lens != null;
assert verifyGraphLens(lens);
// Include bridges in art profiles.
ProfileCollectionAdditions profileCollectionAdditions =
ProfileCollectionAdditions.create(appView);
if (!profileCollectionAdditions.isNop()) {
for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
profileCollectionAdditions.applyIfContextIsInProfile(
lens.getPreviousMethodSignature(synthesizedBridge.getMethod()),
additionsBuilder -> additionsBuilder.addRule(synthesizedBridge.getMethod()));
}
}
profileCollectionAdditions.commit(appView);
// Rewrite collections using the lens.
appView.rewriteWithLens(lens, executorService, timing);
// Copy keep info to newly synthesized methods.
keepInfo.mutate(
mutator -> {
for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
ProgramMethod bridge =
asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.getMethod()));
ProgramMethod target =
asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.getTarget()));
if (bridge != null && target != null) {
mutator.joinMethod(bridge, info -> info.merge(appView.getKeepInfo(target).joiner()));
continue;
}
assert false;
}
});
appView.notifyOptimizationFinishedForTesting();
}
private boolean verifyGraphLens(VerticalClassMergerGraphLens graphLens) {
// Note that the method assertReferencesNotModified() relies on getRenamedFieldSignature() and
// getRenamedMethodSignature() instead of lookupField() and lookupMethod(). This is important
// for this check to succeed, since it is not guaranteed that calling lookupMethod() with a
// pinned method will return the method itself.
//
// Consider the following example.
//
// class A {
// public void method() {}
// }
// class B extends A {
// @Override
// public void method() {}
// }
// class C extends B {
// @Override
// public void method() {}
// }
//
// If A.method() is pinned, then A cannot be merged into B, but B can still be merged into C.
// Now, if there is an invoke-super instruction in C that hits B.method(), then this needs to
// be rewritten into an invoke-direct instruction. In particular, there could be an instruction
// `invoke-super A.method` in C. This would hit B.method(). Therefore, the graph lens records
// that `invoke-super A.method` instructions, which are in one of the methods from C, needs to
// be rewritten to `invoke-direct C.method$B`. This is valid even though A.method() is actually
// pinned, because this rewriting does not affect A.method() in any way.
assert graphLens.assertPinnedNotModified(appView);
for (DexProgramClass clazz : appView.appInfo().classes()) {
for (DexEncodedMethod encodedMethod : clazz.methods()) {
DexMethod method = encodedMethod.getReference();
DexMethod originalMethod = graphLens.getOriginalMethodSignature(method);
DexMethod renamedMethod = graphLens.getRenamedMethodSignature(originalMethod);
// Must be able to map back and forth.
if (encodedMethod.hasCode() && encodedMethod.getCode() instanceof SynthesizedBridgeCode) {
// For virtual methods, the vertical class merger creates two methods in the sub class
// in order to deal with invoke-super instructions (one that is private and one that is
// virtual). Therefore, it is not possible to go back and forth. Instead, we check that
// the two methods map back to the same original method, and that the original method
// can be mapped to the implementation method.
DexMethod implementationMethod =
((SynthesizedBridgeCode) encodedMethod.getCode()).getTarget();
DexMethod originalImplementationMethod =
graphLens.getOriginalMethodSignature(implementationMethod);
assert originalMethod.isIdenticalTo(originalImplementationMethod);
assert implementationMethod.isIdenticalTo(renamedMethod);
} else {
assert method.isIdenticalTo(renamedMethod);
}
// Verify that all types are up-to-date. After vertical class merging, there should be no
// more references to types that have been merged into another type.
assert !mergedClasses.containsKey(method.getReturnType());
assert Arrays.stream(method.getParameters().getBacking())
.noneMatch(mergedClasses::containsKey);
}
}
return true;
}
private boolean methodResolutionMayChange(DexProgramClass source, DexProgramClass target) {
for (DexEncodedMethod virtualSourceMethod : source.virtualMethods()) {
DexEncodedMethod directTargetMethod =
target.lookupDirectMethod(virtualSourceMethod.getReference());
if (directTargetMethod != null) {
// A private method shadows a virtual method. This situation is rare, since it is not
// allowed by javac. Therefore, we just give up in this case. (In principle, it would be
// possible to rename the private method in the subclass, and then move the virtual method
// to the subclass without changing its name.)
return true;
}
}
// When merging an interface into a class, all instructions on the form "invoke-interface
// [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
// transformation could hide IncompatibleClassChangeErrors.
if (source.isInterface() && !target.isInterface()) {
List<DexEncodedMethod> defaultMethods = new ArrayList<>();
for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
if (!virtualMethod.accessFlags.isAbstract()) {
defaultMethods.add(virtualMethod);
}
}
// For each of the default methods, the subclass [target] could inherit another default method
// with the same signature from another interface (i.e., there is a conflict). In such cases,
// instructions on the form "invoke-interface [source].foo()" will fail with an Incompatible-
// ClassChangeError.
//
// Example:
// interface I1 { default void m() {} }
// interface I2 { default void m() {} }
// class C implements I1, I2 {
// ... invoke-interface I1.m ... <- IncompatibleClassChangeError
// }
for (DexEncodedMethod method : defaultMethods) {
// Conservatively find all possible targets for this method.
LookupResultSuccess lookupResult =
appView
.appInfo()
.resolveMethodOnInterfaceLegacy(method.getHolderType(), method.getReference())
.lookupVirtualDispatchTargets(target, appView)
.asLookupResultSuccess();
assert lookupResult != null;
if (lookupResult == null) {
return true;
}
if (lookupResult.contains(method)) {
Box<Boolean> found = new Box<>(false);
lookupResult.forEach(
interfaceTarget -> {
if (ObjectUtils.identical(interfaceTarget.getDefinition(), method)) {
return;
}
DexClass enclosingClass = interfaceTarget.getHolder();
if (enclosingClass != null && enclosingClass.isInterface()) {
// Found a default method that is different from the one in [source], aborting.
found.set(true);
}
},
lambdaTarget -> {
// The merger should already have excluded lambda implemented interfaces.
assert false;
});
if (found.get()) {
return true;
}
}
}
}
return false;
}
private void mergeClassIfPossible(
DexProgramClass clazz, ImmediateProgramSubtypingInfo immediateSubtypingInfo, Timing timing)
throws ExecutionException {
if (!mergeCandidates.contains(clazz)) {
return;
}
List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(clazz);
if (subclasses.size() != 1) {
return;
}
DexProgramClass targetClass = ListUtils.first(subclasses);
assert !mergedClasses.containsKey(targetClass.getType());
if (mergedClasses.containsValue(clazz.getType())) {
return;
}
assert isMergeCandidate(clazz, targetClass);
if (mergedClasses.containsValue(targetClass.getType())) {
if (!isStillMergeCandidate(clazz, targetClass)) {
return;
}
} else {
assert isStillMergeCandidate(clazz, targetClass);
}
// Guard against the case where we have two methods that may get the same signature
// if we replace types. This is rare, so we approximate and err on the safe side here.
CollisionDetector collisionDetector =
new CollisionDetector(
appView,
getInvokes(immediateSubtypingInfo),
mergedClasses,
clazz.getType(),
targetClass.getType());
if (collisionDetector.mayCollide(timing)) {
return;
}
// Check with main dex classes to see if we are allowed to merge.
if (!mainDexInfo.canMerge(clazz, targetClass, appView.getSyntheticItems())) {
return;
}
ClassMerger merger = new ClassMerger(appView, lensBuilder, mergedClasses, clazz, targetClass);
if (merger.merge()) {
mergedClasses.put(clazz.getType(), targetClass.getType());
if (clazz.isInterface()) {
mergedInterfaces.put(clazz.getType(), targetClass.getType());
}
// Commit the changes to the graph lens.
lensBuilder.merge(merger.getRenamings());
synthesizedBridges.addAll(merger.getSynthesizedBridges());
}
}
private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
if (source.getType().isIdenticalTo(target.getSuperType())) {
// If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
// Target implements an interface that declares a static final field f, this should yield an
// IncompatibleClassChangeError.
// TODO(christofferqa): In the following we only check if a static field from an interface
// shadows an instance field from [source]. We could actually check if there is an iget/iput
// instruction whose resolution would be affected by the merge. The situation where a static
// field shadows an instance field is probably not widespread in practice, though.
FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
for (DexType interfaceType : target.getInterfaces()) {
DexClass clazz = appView.definitionFor(interfaceType);
for (DexEncodedField staticField : clazz.staticFields()) {
staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.getReference()));
}
}
for (DexEncodedField instanceField : source.instanceFields()) {
if (staticFieldsInInterfacesOfTarget.contains(
equivalence.wrap(instanceField.getReference()))) {
// An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
// interface would now hit an instance field from [source], so that an IncompatibleClass-
// ChangeError would no longer be thrown. Abort merge.
return true;
}
}
}
return false;
}
private boolean disallowInlining(ProgramMethod method, DexProgramClass context) {
if (appView.options().inlinerOptions().enableInlining) {
Code code = method.getDefinition().getCode();
if (code.isCfCode()) {
CfCode cfCode = code.asCfCode();
SingleTypeMapperGraphLens lens =
new SingleTypeMapperGraphLens(
appView, lensBuilder, mergedClasses, method.getHolder(), context);
ConstraintWithTarget constraint =
cfCode.computeInliningConstraint(
method, appView, lens, context.programInstanceInitializers().iterator().next());
if (constraint.isNever()) {
return true;
}
// Constructors can have references beyond the root main dex classes. This can increase the
// size of the main dex dependent classes and we should bail out.
if (mainDexInfo.disallowInliningIntoContext(
appView, context, method, appView.getSyntheticItems())) {
return true;
}
return false;
} else if (code.isDefaultInstanceInitializerCode()) {
return false;
}
// For non-jar/cf code we currently cannot guarantee that markForceInline() will succeed.
}
return true;
}
}