| // 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; | |
| import com.android.tools.r8.dex.ApplicationReader; | |
| import com.android.tools.r8.dex.code.DexConst; | |
| import com.android.tools.r8.dex.code.DexConst16; | |
| import com.android.tools.r8.dex.code.DexConst4; | |
| import com.android.tools.r8.dex.code.DexConstHigh16; | |
| import com.android.tools.r8.dex.code.DexConstString; | |
| import com.android.tools.r8.dex.code.DexConstStringJumbo; | |
| import com.android.tools.r8.dex.code.DexConstWide16; | |
| import com.android.tools.r8.dex.code.DexConstWide32; | |
| import com.android.tools.r8.dex.code.DexFillArrayData; | |
| import com.android.tools.r8.dex.code.DexFillArrayDataPayload; | |
| import com.android.tools.r8.dex.code.DexFormat35c; | |
| import com.android.tools.r8.dex.code.DexFormat3rc; | |
| import com.android.tools.r8.dex.code.DexInstruction; | |
| import com.android.tools.r8.dex.code.DexInvokeDirect; | |
| import com.android.tools.r8.dex.code.DexInvokeDirectRange; | |
| import com.android.tools.r8.dex.code.DexInvokeInterface; | |
| import com.android.tools.r8.dex.code.DexInvokeInterfaceRange; | |
| import com.android.tools.r8.dex.code.DexInvokeStatic; | |
| import com.android.tools.r8.dex.code.DexInvokeStaticRange; | |
| import com.android.tools.r8.dex.code.DexInvokeSuper; | |
| import com.android.tools.r8.dex.code.DexInvokeSuperRange; | |
| import com.android.tools.r8.dex.code.DexInvokeVirtual; | |
| import com.android.tools.r8.dex.code.DexInvokeVirtualRange; | |
| import com.android.tools.r8.dex.code.DexNewArray; | |
| import com.android.tools.r8.dex.code.DexSget; | |
| import com.android.tools.r8.dex.code.DexSgetBoolean; | |
| import com.android.tools.r8.dex.code.DexSgetByte; | |
| import com.android.tools.r8.dex.code.DexSgetChar; | |
| import com.android.tools.r8.dex.code.DexSgetObject; | |
| import com.android.tools.r8.dex.code.DexSgetShort; | |
| import com.android.tools.r8.dex.code.DexSgetWide; | |
| import com.android.tools.r8.graph.Code; | |
| import com.android.tools.r8.graph.DexAnnotation; | |
| import com.android.tools.r8.graph.DexAnnotationElement; | |
| import com.android.tools.r8.graph.DexApplication; | |
| 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.DexMethod; | |
| import com.android.tools.r8.graph.DexProgramClass; | |
| import com.android.tools.r8.graph.DexValue; | |
| import com.android.tools.r8.ir.code.SingleConstant; | |
| import com.android.tools.r8.ir.code.WideConstant; | |
| import com.android.tools.r8.keepanno.annotations.KeepForApi; | |
| import com.android.tools.r8.references.ClassReference; | |
| import com.android.tools.r8.references.MethodReference; | |
| import com.android.tools.r8.utils.AndroidApp; | |
| import com.android.tools.r8.utils.InternalOptions; | |
| import com.android.tools.r8.utils.Timing; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.collect.Sets; | |
| import com.google.common.collect.Streams; | |
| import java.io.IOException; | |
| import java.util.List; | |
| import java.util.Objects; | |
| import java.util.Set; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.stream.Stream; | |
| /** | |
| * This class is deprecated and should not be used. It is a temporary solution to use R8 to analyze | |
| * dex files and compute resource shrinker related bits. | |
| * | |
| * <p>Users or this API should implement {@link ReferenceChecker} interface, and through callbacks | |
| * in that interface, they will be notified when element relevant for resource shrinking is found. | |
| * | |
| * <p>This class extracts all integer constants and string constants, which might refer to resource. | |
| * More specifically, we look for the following while analyzing dex: | |
| * | |
| * <ul> | |
| * <li>const instructions that might load integers or strings | |
| * <li>static fields that have an initial value. This initial value might be integer, string, or | |
| * array of integers. | |
| * <li>integer array payloads. Only payloads referenced in fill-array-data instructions will be | |
| * processed. More specifically, if a payload is referenced in fill-array-data, and we are | |
| * able to determine that array is not array of integers, payload will be ignored. Otherwise, | |
| * it will be processed once fill-array-data-payload instruction is encountered. | |
| * <li>all annotations (class, field, method) that contain annotation element whose value is | |
| * integer, string or array of integers are processed. | |
| * </ul> | |
| * | |
| * <p>Please note that switch payloads are not analyzed. Although they might contain integer | |
| * constants, ones referring to resource ids would have to be loaded in the code analyzed in list | |
| * above. | |
| * | |
| * <p>Usage of this feature is intentionally not supported from the command line. | |
| */ | |
| // TODO(b/121121779) Remove keep if possible. | |
| @Deprecated | |
| @KeepForApi | |
| final public class ResourceShrinker { | |
| @KeepForApi | |
| public final static class Command extends BaseCommand { | |
| Command(AndroidApp app) { | |
| super(app); | |
| } | |
| @Override | |
| InternalOptions getInternalOptions() { | |
| return new InternalOptions(); | |
| } | |
| } | |
| @KeepForApi | |
| public final static class Builder extends BaseCommand.Builder<Command, Builder> { | |
| @Override | |
| Builder self() { | |
| return this; | |
| } | |
| @Override | |
| Command makeCommand() { | |
| return new Command(getAppBuilder().build()); | |
| } | |
| } | |
| /** | |
| * Classes that would like to process data relevant to resource shrinking should implement this | |
| * interface. | |
| */ | |
| @KeepForApi | |
| public interface ReferenceChecker { | |
| /** | |
| * Returns if the class with specified internal name should be processed. Typically, | |
| * resource type classes like R$drawable, R$styleable etc. should be skipped. | |
| */ | |
| boolean shouldProcess(String internalName); | |
| void referencedInt(int value); | |
| void referencedString(String value); | |
| void referencedStaticField(String internalName, String fieldName); | |
| void referencedMethod(String internalName, String methodName, String methodDescriptor); | |
| default void startMethodVisit(MethodReference methodReference) {} | |
| default void endMethodVisit(MethodReference methodReference) {} | |
| default void startClassVisit(ClassReference classReference) {} | |
| default void endClassVisit(ClassReference classReference) {} | |
| } | |
| private static final class DexClassUsageVisitor { | |
| private final DexProgramClass classDef; | |
| private final ReferenceChecker callback; | |
| DexClassUsageVisitor(DexProgramClass classDef, ReferenceChecker callback) { | |
| this.classDef = classDef; | |
| this.callback = callback; | |
| } | |
| public void visit() { | |
| callback.startClassVisit(classDef.getClassReference()); | |
| if (!callback.shouldProcess(classDef.type.getInternalName())) { | |
| return; | |
| } | |
| for (DexEncodedField field : classDef.staticFields()) { | |
| DexValue staticValue = field.getStaticValue(); | |
| if (staticValue != null) { | |
| processFieldValue(staticValue); | |
| } | |
| } | |
| for (DexEncodedMethod method : classDef.allMethodsSorted()) { | |
| callback.startMethodVisit(method.getReference().asMethodReference()); | |
| processMethod(method); | |
| callback.endMethodVisit(method.getReference().asMethodReference()); | |
| } | |
| if (classDef.hasClassOrMemberAnnotations()) { | |
| processAnnotations(classDef); | |
| } | |
| callback.endClassVisit(classDef.getClassReference()); | |
| } | |
| private void processFieldValue(DexValue value) { | |
| switch (value.getValueKind()) { | |
| case ARRAY: | |
| for (DexValue elementValue : value.asDexValueArray().getValues()) { | |
| if (elementValue.isDexValueInt()) { | |
| callback.referencedInt(elementValue.asDexValueInt().getValue()); | |
| } | |
| } | |
| break; | |
| case INT: | |
| callback.referencedInt(value.asDexValueInt().getValue()); | |
| break; | |
| case STRING: | |
| callback.referencedString(value.asDexValueString().value.toString()); | |
| break; | |
| default: | |
| // Intentionally empty. | |
| } | |
| } | |
| private void processMethod(DexEncodedMethod method) { | |
| Code implementation = method.getCode(); | |
| if (implementation != null) { | |
| // Tracks the offsets of integer array payloads. | |
| final Set<Integer> methodIntArrayPayloadOffsets = Sets.newHashSet(); | |
| // First we collect payloads, and then we process them because payload can be before the | |
| // fill-array-data instruction referencing it. | |
| final List<DexFillArrayDataPayload> payloads = Lists.newArrayList(); | |
| DexInstruction[] instructions = implementation.asDexCode().instructions; | |
| int current = 0; | |
| while (current < instructions.length) { | |
| DexInstruction instruction = instructions[current]; | |
| if (isIntConstInstruction(instruction)) { | |
| processIntConstInstruction(instruction); | |
| } else if (isStringConstInstruction(instruction)) { | |
| processStringConstantInstruction(instruction); | |
| } else if (isGetStatic(instruction)) { | |
| processGetStatic(instruction); | |
| } else if (isInvokeInstruction(instruction)) { | |
| processInvokeInstruction(instruction); | |
| } else if (isInvokeRangeInstruction(instruction)) { | |
| processInvokeRangeInstruction(instruction); | |
| } else if (instruction instanceof DexFillArrayData) { | |
| processFillArray(instructions, current, methodIntArrayPayloadOffsets); | |
| } else if (instruction instanceof DexFillArrayDataPayload) { | |
| payloads.add((DexFillArrayDataPayload) instruction); | |
| } | |
| current++; | |
| } | |
| for (DexFillArrayDataPayload payload : payloads) { | |
| if (isIntArrayPayload(payload, methodIntArrayPayloadOffsets)) { | |
| processIntArrayPayload(payload); | |
| } | |
| } | |
| } | |
| } | |
| private void processAnnotations(DexProgramClass classDef) { | |
| Stream<DexAnnotation> classAnnotations = classDef.annotations().stream(); | |
| Stream<DexAnnotation> fieldAnnotations = | |
| Streams.stream(classDef.fields()) | |
| .filter(DexEncodedField::hasAnnotations) | |
| .flatMap(f -> f.annotations().stream()); | |
| Stream<DexAnnotation> methodAnnotations = | |
| Streams.stream(classDef.methods()) | |
| .filter(DexEncodedMethod::hasAnyAnnotations) | |
| .flatMap(m -> m.annotations().stream()); | |
| Streams.concat(classAnnotations, fieldAnnotations, methodAnnotations) | |
| .forEach( | |
| annotation -> { | |
| for (DexAnnotationElement element : annotation.annotation.elements) { | |
| DexValue value = element.value; | |
| processAnnotationValue(value); | |
| } | |
| }); | |
| } | |
| private void processIntArrayPayload(DexInstruction instruction) { | |
| DexFillArrayDataPayload payload = (DexFillArrayDataPayload) instruction; | |
| for (int i = 0; i < payload.data.length / 2; i++) { | |
| int intValue = payload.data[2 * i + 1] << 16 | payload.data[2 * i]; | |
| callback.referencedInt(intValue); | |
| } | |
| } | |
| private boolean isIntArrayPayload( | |
| DexInstruction instruction, Set<Integer> methodIntArrayPayloadOffsets) { | |
| if (!(instruction instanceof DexFillArrayDataPayload)) { | |
| return false; | |
| } | |
| DexFillArrayDataPayload payload = (DexFillArrayDataPayload) instruction; | |
| return methodIntArrayPayloadOffsets.contains(payload.getOffset()); | |
| } | |
| private void processFillArray( | |
| DexInstruction[] instructions, int current, Set<Integer> methodIntArrayPayloadOffsets) { | |
| DexFillArrayData fillArrayData = (DexFillArrayData) instructions[current]; | |
| if (current > 0 && instructions[current - 1] instanceof DexNewArray) { | |
| DexNewArray newArray = (DexNewArray) instructions[current - 1]; | |
| if (!Objects.equals(newArray.getType().descriptor.toString(), "[I")) { | |
| return; | |
| } | |
| // Typically, new-array is right before fill-array-data. If not, assume referenced array is | |
| // of integers. This can be improved later, but for now we make sure no ints are missed. | |
| } | |
| methodIntArrayPayloadOffsets.add( | |
| fillArrayData.getPayloadOffset() + fillArrayData.getOffset()); | |
| } | |
| private void processAnnotationValue(DexValue value) { | |
| switch (value.getValueKind()) { | |
| case ANNOTATION: | |
| for (DexAnnotationElement element : value.asDexValueAnnotation().value.elements) { | |
| processAnnotationValue(element.value); | |
| } | |
| break; | |
| case ARRAY: | |
| for (DexValue elementValue : value.asDexValueArray().getValues()) { | |
| processAnnotationValue(elementValue); | |
| } | |
| break; | |
| case INT: | |
| callback.referencedInt(value.asDexValueInt().value); | |
| break; | |
| case STRING: | |
| callback.referencedString(value.asDexValueString().value.toString()); | |
| break; | |
| default: | |
| // Intentionally empty. | |
| } | |
| } | |
| private boolean isIntConstInstruction(DexInstruction instruction) { | |
| int opcode = instruction.getOpcode(); | |
| return opcode == DexConst4.OPCODE | |
| || opcode == DexConst16.OPCODE | |
| || opcode == DexConst.OPCODE | |
| || opcode == DexConstWide32.OPCODE | |
| || opcode == DexConstHigh16.OPCODE | |
| || opcode == DexConstWide16.OPCODE; | |
| } | |
| private void processIntConstInstruction(DexInstruction instruction) { | |
| assert isIntConstInstruction(instruction); | |
| int constantValue; | |
| if (instruction instanceof SingleConstant) { | |
| SingleConstant singleConstant = (SingleConstant) instruction; | |
| constantValue = singleConstant.decodedValue(); | |
| } else if (instruction instanceof WideConstant) { | |
| WideConstant wideConstant = (WideConstant) instruction; | |
| if (((int) wideConstant.decodedValue()) != wideConstant.decodedValue()) { | |
| // We care only about values that fit in int range. | |
| return; | |
| } | |
| constantValue = (int) wideConstant.decodedValue(); | |
| } else { | |
| throw new AssertionError("Not an int const instruction."); | |
| } | |
| callback.referencedInt(constantValue); | |
| } | |
| private boolean isStringConstInstruction(DexInstruction instruction) { | |
| int opcode = instruction.getOpcode(); | |
| return opcode == DexConstString.OPCODE || opcode == DexConstStringJumbo.OPCODE; | |
| } | |
| private void processStringConstantInstruction(DexInstruction instruction) { | |
| assert isStringConstInstruction(instruction); | |
| String constantValue; | |
| if (instruction instanceof DexConstString) { | |
| DexConstString constString = (DexConstString) instruction; | |
| constantValue = constString.getString().toString(); | |
| } else if (instruction instanceof DexConstStringJumbo) { | |
| DexConstStringJumbo constStringJumbo = (DexConstStringJumbo) instruction; | |
| constantValue = constStringJumbo.getString().toString(); | |
| } else { | |
| throw new AssertionError("Not a string constant instruction."); | |
| } | |
| callback.referencedString(constantValue); | |
| } | |
| private boolean isGetStatic(DexInstruction instruction) { | |
| int opcode = instruction.getOpcode(); | |
| return opcode == DexSget.OPCODE | |
| || opcode == DexSgetBoolean.OPCODE | |
| || opcode == DexSgetByte.OPCODE | |
| || opcode == DexSgetChar.OPCODE | |
| || opcode == DexSgetObject.OPCODE | |
| || opcode == DexSgetShort.OPCODE | |
| || opcode == DexSgetWide.OPCODE; | |
| } | |
| private void processGetStatic(DexInstruction instruction) { | |
| assert isGetStatic(instruction); | |
| DexField field; | |
| if (instruction instanceof DexSget) { | |
| DexSget sget = (DexSget) instruction; | |
| field = sget.getField(); | |
| } else if (instruction instanceof DexSgetBoolean) { | |
| DexSgetBoolean sgetBoolean = (DexSgetBoolean) instruction; | |
| field = sgetBoolean.getField(); | |
| } else if (instruction instanceof DexSgetByte) { | |
| DexSgetByte sgetByte = (DexSgetByte) instruction; | |
| field = sgetByte.getField(); | |
| } else if (instruction instanceof DexSgetChar) { | |
| DexSgetChar sgetChar = (DexSgetChar) instruction; | |
| field = sgetChar.getField(); | |
| } else if (instruction instanceof DexSgetObject) { | |
| DexSgetObject sgetObject = (DexSgetObject) instruction; | |
| field = sgetObject.getField(); | |
| } else if (instruction instanceof DexSgetShort) { | |
| DexSgetShort sgetShort = (DexSgetShort) instruction; | |
| field = sgetShort.getField(); | |
| } else if (instruction instanceof DexSgetWide) { | |
| DexSgetWide sgetWide = (DexSgetWide) instruction; | |
| field = sgetWide.getField(); | |
| } else { | |
| throw new AssertionError("Not a get static instruction"); | |
| } | |
| callback.referencedStaticField(field.holder.getInternalName(), field.name.toString()); | |
| } | |
| private boolean isInvokeInstruction(DexInstruction instruction) { | |
| int opcode = instruction.getOpcode(); | |
| return opcode == DexInvokeVirtual.OPCODE | |
| || opcode == DexInvokeSuper.OPCODE | |
| || opcode == DexInvokeDirect.OPCODE | |
| || opcode == DexInvokeStatic.OPCODE | |
| || opcode == DexInvokeInterface.OPCODE; | |
| } | |
| private void processInvokeInstruction(DexInstruction instruction) { | |
| assert isInvokeInstruction(instruction); | |
| DexFormat35c ins35c = (DexFormat35c) instruction; | |
| DexMethod method = (DexMethod) ins35c.BBBB; | |
| callback.referencedMethod( | |
| method.holder.getInternalName(), | |
| method.name.toString(), | |
| method.proto.toDescriptorString()); | |
| } | |
| private boolean isInvokeRangeInstruction(DexInstruction instruction) { | |
| int opcode = instruction.getOpcode(); | |
| return opcode == DexInvokeVirtualRange.OPCODE | |
| || opcode == DexInvokeSuperRange.OPCODE | |
| || opcode == DexInvokeDirectRange.OPCODE | |
| || opcode == DexInvokeStaticRange.OPCODE | |
| || opcode == DexInvokeInterfaceRange.OPCODE; | |
| } | |
| private void processInvokeRangeInstruction(DexInstruction instruction) { | |
| assert isInvokeRangeInstruction(instruction); | |
| DexFormat3rc ins3rc = (DexFormat3rc) instruction; | |
| DexMethod method = (DexMethod) ins3rc.BBBB; | |
| callback.referencedMethod( | |
| method.holder.getInternalName(), | |
| method.name.toString(), | |
| method.proto.toDescriptorString()); | |
| } | |
| } | |
| @SuppressWarnings("RedundantThrows") | |
| public static void run(Command command, ReferenceChecker callback) | |
| throws IOException, ExecutionException { | |
| runForTesting(command.getInputApp(), command.getInternalOptions(), callback); | |
| } | |
| public static void runForTesting( | |
| AndroidApp inputApp, InternalOptions options, ReferenceChecker callback) | |
| throws IOException, ExecutionException { | |
| Timing timing = new Timing("resource shrinker analyzer"); | |
| DexApplication dexApplication = new ApplicationReader(inputApp, options, timing).read(); | |
| for (DexProgramClass programClass : dexApplication.classes()) { | |
| new DexClassUsageVisitor(programClass, callback).visit(); | |
| } | |
| } | |
| } |