blob: bf4f471d7a50b2a74ee78f63256ff3fa756ca288 [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;
import com.android.tools.r8.code.Const;
import com.android.tools.r8.code.Const16;
import com.android.tools.r8.code.Const4;
import com.android.tools.r8.code.ConstHigh16;
import com.android.tools.r8.code.ConstString;
import com.android.tools.r8.code.ConstStringJumbo;
import com.android.tools.r8.code.ConstWide16;
import com.android.tools.r8.code.ConstWide32;
import com.android.tools.r8.code.FillArrayData;
import com.android.tools.r8.code.FillArrayDataPayload;
import com.android.tools.r8.code.Format35c;
import com.android.tools.r8.code.Format3rc;
import com.android.tools.r8.code.Instruction;
import com.android.tools.r8.code.InvokeDirect;
import com.android.tools.r8.code.InvokeDirectRange;
import com.android.tools.r8.code.InvokeInterface;
import com.android.tools.r8.code.InvokeInterfaceRange;
import com.android.tools.r8.code.InvokeStatic;
import com.android.tools.r8.code.InvokeStaticRange;
import com.android.tools.r8.code.InvokeSuper;
import com.android.tools.r8.code.InvokeSuperRange;
import com.android.tools.r8.code.InvokeVirtual;
import com.android.tools.r8.code.InvokeVirtualRange;
import com.android.tools.r8.code.NewArray;
import com.android.tools.r8.code.Sget;
import com.android.tools.r8.code.SgetBoolean;
import com.android.tools.r8.code.SgetByte;
import com.android.tools.r8.code.SgetChar;
import com.android.tools.r8.code.SgetObject;
import com.android.tools.r8.code.SgetShort;
import com.android.tools.r8.code.SgetWide;
import com.android.tools.r8.dex.ApplicationReader;
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.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
@Keep
final public class ResourceShrinker {
@Keep
public final static class Command extends BaseCommand {
Command(AndroidApp app) {
super(app);
}
@Override
InternalOptions getInternalOptions() {
return new InternalOptions();
}
}
@Keep
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.
*/
@KeepForSubclassing
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<FillArrayDataPayload> payloads = Lists.newArrayList();
Instruction[] instructions = implementation.asDexCode().instructions;
int current = 0;
while (current < instructions.length) {
Instruction 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 FillArrayData) {
processFillArray(instructions, current, methodIntArrayPayloadOffsets);
} else if (instruction instanceof FillArrayDataPayload) {
payloads.add((FillArrayDataPayload) instruction);
}
current++;
}
for (FillArrayDataPayload 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::hasAnnotation)
.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(Instruction instruction) {
FillArrayDataPayload payload = (FillArrayDataPayload) 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(
Instruction instruction, Set<Integer> methodIntArrayPayloadOffsets) {
if (!(instruction instanceof FillArrayDataPayload)) {
return false;
}
FillArrayDataPayload payload = (FillArrayDataPayload) instruction;
return methodIntArrayPayloadOffsets.contains(payload.getOffset());
}
private void processFillArray(
Instruction[] instructions, int current, Set<Integer> methodIntArrayPayloadOffsets) {
FillArrayData fillArrayData = (FillArrayData) instructions[current];
if (current > 0 && instructions[current - 1] instanceof NewArray) {
NewArray newArray = (NewArray) 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(Instruction instruction) {
int opcode = instruction.getOpcode();
return opcode == Const4.OPCODE
|| opcode == Const16.OPCODE
|| opcode == Const.OPCODE
|| opcode == ConstWide32.OPCODE
|| opcode == ConstHigh16.OPCODE
|| opcode == ConstWide16.OPCODE;
}
private void processIntConstInstruction(Instruction 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(Instruction instruction) {
int opcode = instruction.getOpcode();
return opcode == ConstString.OPCODE || opcode == ConstStringJumbo.OPCODE;
}
private void processStringConstantInstruction(Instruction instruction) {
assert isStringConstInstruction(instruction);
String constantValue;
if (instruction instanceof ConstString) {
ConstString constString = (ConstString) instruction;
constantValue = constString.getString().toString();
} else if (instruction instanceof ConstStringJumbo) {
ConstStringJumbo constStringJumbo = (ConstStringJumbo) instruction;
constantValue = constStringJumbo.getString().toString();
} else {
throw new AssertionError("Not a string constant instruction.");
}
callback.referencedString(constantValue);
}
private boolean isGetStatic(Instruction instruction) {
int opcode = instruction.getOpcode();
return opcode == Sget.OPCODE
|| opcode == SgetBoolean.OPCODE
|| opcode == SgetByte.OPCODE
|| opcode == SgetChar.OPCODE
|| opcode == SgetObject.OPCODE
|| opcode == SgetShort.OPCODE
|| opcode == SgetWide.OPCODE;
}
private void processGetStatic(Instruction instruction) {
assert isGetStatic(instruction);
DexField field;
if (instruction instanceof Sget) {
Sget sget = (Sget) instruction;
field = sget.getField();
} else if (instruction instanceof SgetBoolean) {
SgetBoolean sgetBoolean = (SgetBoolean) instruction;
field = sgetBoolean.getField();
} else if (instruction instanceof SgetByte) {
SgetByte sgetByte = (SgetByte) instruction;
field = sgetByte.getField();
} else if (instruction instanceof SgetChar) {
SgetChar sgetChar = (SgetChar) instruction;
field = sgetChar.getField();
} else if (instruction instanceof SgetObject) {
SgetObject sgetObject = (SgetObject) instruction;
field = sgetObject.getField();
} else if (instruction instanceof SgetShort) {
SgetShort sgetShort = (SgetShort) instruction;
field = sgetShort.getField();
} else if (instruction instanceof SgetWide) {
SgetWide sgetWide = (SgetWide) instruction;
field = sgetWide.getField();
} else {
throw new AssertionError("Not a get static instruction");
}
callback.referencedStaticField(field.holder.getInternalName(), field.name.toString());
}
private boolean isInvokeInstruction(Instruction instruction) {
int opcode = instruction.getOpcode();
return opcode == InvokeVirtual.OPCODE
|| opcode == InvokeSuper.OPCODE
|| opcode == InvokeDirect.OPCODE
|| opcode == InvokeStatic.OPCODE
|| opcode == InvokeInterface.OPCODE;
}
private void processInvokeInstruction(Instruction instruction) {
assert isInvokeInstruction(instruction);
Format35c ins35c = (Format35c) instruction;
DexMethod method = (DexMethod) ins35c.BBBB;
callback.referencedMethod(
method.holder.getInternalName(),
method.name.toString(),
method.proto.toDescriptorString());
}
private boolean isInvokeRangeInstruction(Instruction instruction) {
int opcode = instruction.getOpcode();
return opcode == InvokeVirtualRange.OPCODE
|| opcode == InvokeSuperRange.OPCODE
|| opcode == InvokeDirectRange.OPCODE
|| opcode == InvokeStaticRange.OPCODE
|| opcode == InvokeInterfaceRange.OPCODE;
}
private void processInvokeRangeInstruction(Instruction instruction) {
assert isInvokeRangeInstruction(instruction);
Format3rc ins3rc = (Format3rc) instruction;
DexMethod method = (DexMethod) ins3rc.BBBB;
callback.referencedMethod(
method.holder.getInternalName(),
method.name.toString(),
method.proto.toDescriptorString());
}
}
public static void run(Command command, ReferenceChecker callback)
throws IOException, ExecutionException {
AndroidApp inputApp = command.getInputApp();
Timing timing = new Timing("resource shrinker analyzer");
DexApplication dexApplication =
new ApplicationReader(inputApp, command.getInternalOptions(), timing).read();
for (DexProgramClass programClass : dexApplication.classes()) {
new DexClassUsageVisitor(programClass, callback).visit();
}
}
}