// 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.naming; import com.android.tools.r8.graph.AppInfoWithSubtyping; import com.android.tools.r8.graph.DexClass; import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexMethod; import com.android.tools.r8.graph.DexProto; import com.android.tools.r8.graph.DexString; import com.android.tools.r8.graph.DexType; import com.android.tools.r8.shaking.RootSetBuilder.RootSet; import com.android.tools.r8.utils.MethodSignatureEquivalence; import com.android.tools.r8.utils.Timing; import com.google.common.base.Equivalence.Wrapper; import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * A pass to rename methods using common, short names. *

* To assign names, we model the scopes of methods names and overloading/shadowing based on the * subtyping tree of classes. Such a naming scope is encoded by {@link NamingState}. It keeps * track of its parent node, names that have been reserved (due to keep annotations or otherwise) * and what names have been used for renaming so far. *

* As in the Dalvik VM method dispatch takes argument and return types of methods into account, we * can further reuse names if the prototypes of two methods differ. For this, we store the above * state separately for each proto using a map from protos to {@link NamingState.InternalState} * objects. These internal state objects are also linked. *

* Name assignment happens in 4 stages. In the first stage, we record all names that are used by * library classes or are flagged using a keep rule as reserved. This step also allocates the * {@link NamingState} objects for library classes. We can fully allocate these objects as we * never perform naming for library classes. For non-library classes, we only allocate a state * for the highest non-library class, i.e., we allocate states for every direct subtype of a library * class. The states at the boundary between library and program classes are referred to as the * frontier states in the code. *

* When reserving names in program classes, we reserve them in the state of the corresponding * frontier class. This is to ensure that the names are not used for renaming in any supertype. * Thus, they will still be available in the subtype where they are reserved. Note that name * reservation only blocks names from being used for minification. We assume that the input program * is correctly named. *

* In stage 2, we reserve names that stem from interfaces. These are not propagated to * subinterfaces or implementing classes. Instead, stage 3 makes sure to query related states when * making naming decisions. *

* In stage 3, we compute minified names for all interface methods. We do this first to reduce * assignment conflicts. Interfaces do not build a tree-like inheritance structure we can exploit. * Thus, we have to infer the structure on the fly. For this, we compute a sets of reachable * interfaces. i.e., interfaces that are related via subtyping. Based on these sets, we then * find, for each method signature, the classes and interfaces this method signature is defined in. * For classes, as we still use frontier states at this point, we do not have to consider subtype * relations. For interfaces, we reserve the name in all reachable interfaces and thus ensure * availability. *

* Name assignment in this phase is a search over all impacted naming states. Using the naming state * of the interface this method first originated from, we propose names until we find a matching * one. We use the naming state of the interface to not impact name availability in naming states of * classes. Hence, skipping over names during interface naming does not impact their availability in * the next phase. *

* In the final stage, we assign names to methods by traversing the subtype tree, now allocating * separate naming states for each class starting from the frontier. In the first swoop, we allocate * all non-private methods, updating naming states accordingly. In a second swoop, we then allocate * private methods, as those may safely use names that are used by a public method further down in * the subtyping tree. *

* Finally, the computed renamings are returned as a map from {@link DexMethod} to * {@link DexString}. The MethodNameMinifier object should not be retained to ensure all * intermediate state is freed. *

* TODO(herhut): Currently, we do not minify members of annotation interfaces, as this would require * parsing and minification of the string arguments to annotations. */ public class MethodNameMinifier { private final AppInfoWithSubtyping appInfo; private final RootSet rootSet; private final Map> states = new IdentityHashMap<>(); private final NamingState globalState; private MethodSignatureEquivalence equivalence = MethodSignatureEquivalence.get(); private final List dictionary; public MethodNameMinifier(AppInfoWithSubtyping appInfo, RootSet rootSet, List dictionary) { this.appInfo = appInfo; this.rootSet = rootSet; this.dictionary = dictionary; this.globalState = NamingState.createRoot(appInfo.dexItemFactory, dictionary); } public Map computeRenaming(Timing timing) { // Phase 1: Reserve all the names that need to be kept and allocate linked state in the // library part. timing.begin("Phase 1"); Map frontierMap = new IdentityHashMap<>(); reserveNamesInClasses(appInfo.dexItemFactory.objectType, appInfo.dexItemFactory.objectType, null, frontierMap); timing.end(); // Phase 2: Reserve all the names that are required for interfaces. timing.begin("Phase 2"); DexType.forAllInterfaces(appInfo.dexItemFactory, iface -> { reserveNamesInInterfaces(iface, frontierMap); }); timing.end(); // Phase 3: Assign names to interface methods. These are assigned by finding a name that is // free in all naming states that may hold an implementation. timing.begin("Phase 3"); Map renaming = new IdentityHashMap<>(); assignNamesToInterfaceMethods(frontierMap, renaming, timing); timing.end(); // Phase 4: Assign names top-down by traversing the subtype hierarchy. timing.begin("Phase 4"); assignNamesToClassesMethods(appInfo.dexItemFactory.objectType, false, renaming); timing.end(); // Phase 4: Do the same for private methods. timing.begin("Phase 5"); assignNamesToClassesMethods(appInfo.dexItemFactory.objectType, true, renaming); timing.end(); return renaming; } private void assignNamesToClassesMethods(DexType type, boolean doPrivates, Map renaming) { DexClass holder = appInfo.definitionFor(type); if (holder != null && !holder.isLibraryClass()) { NamingState state = states .computeIfAbsent(type, k -> states.get(holder.superType).createChild()); assignNamesToMethods(holder.directMethods(), state, doPrivates, renaming); assignNamesToMethods(holder.virtualMethods(), state, doPrivates, renaming); } type.forAllExtendsSubtypes( subtype -> assignNamesToClassesMethods(subtype, doPrivates, renaming)); } private void assignNamesToMethods(DexEncodedMethod[] methods, NamingState state, boolean doPrivates, Map renaming) { for (DexEncodedMethod encodedMethod : methods) { if (encodedMethod.accessFlags.isPrivate() != doPrivates) { continue; } DexMethod method = encodedMethod.method; if (!state.isReserved(method.name, method.proto) && !encodedMethod.accessFlags.isConstructor()) { renaming.put(method, state.assignNewNameFor(method.name, method.proto, !doPrivates)); } } } private Set> getReachableStates(DexType type, Map frontierMap) { Set interfaces = Sets.newIdentityHashSet(); interfaces.add(type); collectSuperInterfaces(type, interfaces); collectSubInterfaces(type, interfaces); Set> reachableStates = new HashSet<>(); for (DexType iface : interfaces) { // Add the interface itself reachableStates.add(states.get(iface)); // And the frontiers that correspond to the classes that implement the interface. iface.forAllImplementsSubtypes(t -> { NamingState state = states.get(frontierMap.get(t)); assert state != null; reachableStates.add(state); }); } return reachableStates; } private void assignNamesToInterfaceMethods(Map frontierMap, Map renaming, Timing timing) { // First compute a map from method signatures to a set of naming states for interfaces and // frontier states of classes that implement them. We add the frontier states so that we can // reserve the names for later method naming. timing.begin("Compute map"); // A map from DexMethods to all the states linked to interfaces they appear in. Map, Set>> globalStateMap = new HashMap<>(); // A map from DexMethods to all the definitions seen. Needed as the Wrapper equalizes them all. Map, Set> sourceMethodsMap = new HashMap<>(); // A map from DexMethods to the first interface state it was seen in. Used to pick good names. Map, NamingState> originStates = new HashMap<>(); DexType.forAllInterfaces(appInfo.dexItemFactory, iface -> { assert iface.isInterface(); DexClass clazz = appInfo.definitionFor(iface); if (clazz != null) { Set> collectedStates = getReachableStates(iface, frontierMap); addStatesToGlobalMapForMethods(clazz.directMethods(), collectedStates, globalStateMap, sourceMethodsMap, originStates, iface); addStatesToGlobalMapForMethods(clazz.virtualMethods(), collectedStates, globalStateMap, sourceMethodsMap, originStates, iface); } }); timing.end(); // Go over every method and assign a name. timing.begin("Allocate names"); // Sort the methods by the number of dependent states, so that we use short names for methods // references in many places. List> methods = new ArrayList<>(globalStateMap.keySet()); methods.sort((a, b) -> globalStateMap.get(b).size() - globalStateMap.get(a).size()); for (Wrapper key : methods) { DexMethod method = key.get(); assignNameForInterfaceMethodInAllStates(method, globalStateMap.get(key), sourceMethodsMap.get(key), renaming, originStates.get(key)); } timing.end(); } private void collectSuperInterfaces(DexType iface, Set interfaces) { DexClass clazz = appInfo.definitionFor(iface); // In cases where we lack the interface's definition, we can at least look at subtypes and // tie those up to get proper naming. if (clazz != null) { for (DexType type : clazz.interfaces.values) { if (interfaces.add(type)) { collectSuperInterfaces(type, interfaces); } } } } private void collectSubInterfaces(DexType iface, Set interfaces) { DexClass clazz = appInfo.definitionFor(iface); iface.forAllExtendsSubtypes(subtype -> { assert subtype.isInterface(); if (interfaces.add(subtype)) { collectSubInterfaces(subtype, interfaces); } }); } private void addStatesToGlobalMapForMethods( DexEncodedMethod[] methods, Set> collectedStates, Map, Set>> globalStateMap, Map, Set> sourceMethodsMap, Map, NamingState> originStates, DexType originInterface) { for (DexEncodedMethod method : methods) { Wrapper key = equivalence.wrap(method.method); Set> stateSet = globalStateMap .computeIfAbsent(key, k -> new HashSet<>()); stateSet.addAll(collectedStates); sourceMethodsMap.computeIfAbsent(key, k -> new HashSet<>()).add(method.method); originStates.putIfAbsent(key, states.get(originInterface)); } } private void assignNameForInterfaceMethodInAllStates(DexMethod method, Set> collectedStates, Set sourceMethods, Map renaming, NamingState originState) { boolean isReserved = false; if (globalState.isReserved(method.name, method.proto)) { for (NamingState state : collectedStates) { if (state.isReserved(method.name, method.proto)) { isReserved = true; break; } } if (isReserved) { // This method's name is reserved in at least on naming state, so reserve it everywhere. for (NamingState state : collectedStates) { state.reserveName(method.name, method.proto); } return; } } // We use the origin state to allocate a name here so that we can reuse names between different // unrelated interfaces. This saves some space. The alternative would be to use a global state // for allocating names, which would save the work to search here. DexString candidate = null; do { candidate = originState.assignNewNameFor(method.name, method.proto, false); for (NamingState state : collectedStates) { if (!state.isAvailable(method.name, method.proto, candidate)) { candidate = null; break; } } } while (candidate == null); for (NamingState state : collectedStates) { state.addRenaming(method.name, method.proto, candidate); } // Rename all methods in interfaces that gave rise to this renaming. for (DexMethod sourceMethod : sourceMethods) { renaming.put(sourceMethod, candidate); } } private void reserveNamesInClasses(DexType type, DexType libraryFrontier, NamingState parent, Map frontierMap) { assert !type.isInterface(); DexClass holder = appInfo.definitionFor(type); NamingState state = allocateNamingStateAndReserve(holder, type, libraryFrontier, parent, frontierMap); // If this is a library class (or effectively a library class as it is missing) move the // frontier forward. type.forAllExtendsSubtypes(subtype -> { assert !subtype.isInterface(); reserveNamesInClasses(subtype, holder == null || holder.isLibraryClass() ? subtype : libraryFrontier, state, frontierMap); }); } private void reserveNamesInInterfaces(DexType type, Map frontierMap) { assert type.isInterface(); frontierMap.put(type, type); DexClass holder = appInfo.definitionFor(type); allocateNamingStateAndReserve(holder, type, type, null, frontierMap); } private NamingState allocateNamingStateAndReserve(DexClass holder, DexType type, DexType libraryFrontier, NamingState parent, Map frontierMap) { frontierMap.put(type, libraryFrontier); NamingState state = states.computeIfAbsent(libraryFrontier, ignore -> parent == null ? NamingState.createRoot(appInfo.dexItemFactory, dictionary) : parent.createChild()); if (holder != null) { boolean keepAll = holder.isLibraryClass() || holder.accessFlags.isAnnotation(); reserveNamesForMethods(holder.virtualMethods(), keepAll, state); reserveNamesForMethods(holder.directMethods(), keepAll, state); } return state; } private void reserveNamesForMethods(DexEncodedMethod[] methods, boolean keepAll, NamingState state) { for (DexEncodedMethod method : methods) { if (keepAll || rootSet.noObfuscation.contains(method)) { state.reserveName(method.method.name, method.method.proto); globalState.reserveName(method.method.name, method.method.proto); } } } }