blob: 52c6b6a091e7af56a485078525f8b2af1358d7ce [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.SUPPORT_LIB_ARTIFACT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_BOOLEAN;
import static com.android.tools.lint.client.api.JavaParser.TYPE_INT;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.TextFormat;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.ConstructorInvocation;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.If;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
import lombok.ast.This;
import lombok.ast.Throw;
import lombok.ast.TypeReference;
import lombok.ast.TypeReferencePart;
import lombok.ast.UnaryExpression;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableReference;
/**
* Looks for performance issues in Java files, such as memory allocations during
* drawing operations and using HashMap instead of SparseArray.
*/
public class JavaPerformanceDetector extends Detector implements Detector.JavaScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
JavaPerformanceDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Allocating objects during a paint method */
public static final Issue PAINT_ALLOC = Issue.create(
"DrawAllocation", //$NON-NLS-1$
"Memory allocations within drawing code",
"You should avoid allocating objects during a drawing or layout operation. These " +
"are called frequently, so a smooth UI can be interrupted by garbage collection " +
"pauses caused by the object allocations.\n" +
"\n" +
"The way this is generally handled is to allocate the needed objects up front " +
"and to reuse them for each drawing operation.\n" +
"\n" +
"Some methods allocate memory on your behalf (such as `Bitmap.create`), and these " +
"should be handled in the same way.",
Category.PERFORMANCE,
9,
Severity.WARNING,
IMPLEMENTATION);
/** Using HashMaps where SparseArray would be better */
public static final Issue USE_SPARSE_ARRAY = Issue.create(
"UseSparseArrays", //$NON-NLS-1$
"HashMap can be replaced with SparseArray",
"For maps where the keys are of type integer, it's typically more efficient to " +
"use the Android `SparseArray` API. This check identifies scenarios where you might " +
"want to consider using `SparseArray` instead of `HashMap` for better performance.\n" +
"\n" +
"This is *particularly* useful when the value types are primitives like ints, " +
"where you can use `SparseIntArray` and avoid auto-boxing the values from `int` to " +
"`Integer`.\n" +
"\n" +
"If you need to construct a `HashMap` because you need to call an API outside of " +
"your control which requires a `Map`, you can suppress this warning using for " +
"example the `@SuppressLint` annotation.",
Category.PERFORMANCE,
4,
Severity.WARNING,
IMPLEMENTATION);
/** Using {@code new Integer()} instead of the more efficient {@code Integer.valueOf} */
public static final Issue USE_VALUE_OF = Issue.create(
"UseValueOf", //$NON-NLS-1$
"Should use `valueOf` instead of `new`",
"You should not call the constructor for wrapper classes directly, such as" +
"`new Integer(42)`. Instead, call the `valueOf` factory method, such as " +
"`Integer.valueOf(42)`. This will typically use less memory because common integers " +
"such as 0 and 1 will share a single instance.",
Category.PERFORMANCE,
4,
Severity.WARNING,
IMPLEMENTATION);
static final String ON_MEASURE = "onMeasure"; //$NON-NLS-1$
static final String ON_DRAW = "onDraw"; //$NON-NLS-1$
static final String ON_LAYOUT = "onLayout"; //$NON-NLS-1$
private static final String INTEGER = "Integer"; //$NON-NLS-1$
private static final String BOOLEAN = "Boolean"; //$NON-NLS-1$
private static final String BYTE = "Byte"; //$NON-NLS-1$
private static final String LONG = "Long"; //$NON-NLS-1$
private static final String CHARACTER = "Character"; //$NON-NLS-1$
private static final String DOUBLE = "Double"; //$NON-NLS-1$
private static final String FLOAT = "Float"; //$NON-NLS-1$
private static final String HASH_MAP = "HashMap"; //$NON-NLS-1$
private static final String SPARSE_ARRAY = "SparseArray"; //$NON-NLS-1$
private static final String CANVAS = "Canvas"; //$NON-NLS-1$
private static final String LAYOUT = "layout"; //$NON-NLS-1$
/** Constructs a new {@link JavaPerformanceDetector} check */
public JavaPerformanceDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
List<Class<? extends Node>> types = new ArrayList<Class<? extends Node>>(3);
types.add(ConstructorInvocation.class);
types.add(MethodDeclaration.class);
types.add(MethodInvocation.class);
return types;
}
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
return new PerformanceVisitor(context);
}
private static class PerformanceVisitor extends ForwardingAstVisitor {
private final JavaContext mContext;
private final boolean mCheckMaps;
private final boolean mCheckAllocations;
private final boolean mCheckValueOf;
/** Whether allocations should be "flagged" in the current method */
private boolean mFlagAllocations;
public PerformanceVisitor(JavaContext context) {
mContext = context;
mCheckAllocations = context.isEnabled(PAINT_ALLOC);
mCheckMaps = context.isEnabled(USE_SPARSE_ARRAY);
mCheckValueOf = context.isEnabled(USE_VALUE_OF);
}
@Override
public boolean visitMethodDeclaration(MethodDeclaration node) {
mFlagAllocations = isBlockedAllocationMethod(node);
return super.visitMethodDeclaration(node);
}
@Override
public boolean visitConstructorInvocation(ConstructorInvocation node) {
String typeName = null;
if (mCheckMaps) {
TypeReference reference = node.astTypeReference();
typeName = reference.astParts().last().astIdentifier().astValue();
// TODO: Should we handle factory method constructions of HashMaps as well,
// e.g. via Guava? This is a bit trickier since we need to infer the type
// arguments from the calling context.
if (typeName.equals(HASH_MAP)) {
checkHashMap(node, reference);
} else if (typeName.equals(SPARSE_ARRAY)) {
checkSparseArray(node, reference);
}
}
if (mCheckValueOf) {
if (typeName == null) {
TypeReference reference = node.astTypeReference();
typeName = reference.astParts().last().astIdentifier().astValue();
}
if ((typeName.equals(INTEGER)
|| typeName.equals(BOOLEAN)
|| typeName.equals(FLOAT)
|| typeName.equals(CHARACTER)
|| typeName.equals(LONG)
|| typeName.equals(DOUBLE)
|| typeName.equals(BYTE))
&& node.astTypeReference().astParts().size() == 1
&& node.astArguments().size() == 1) {
String argument = node.astArguments().first().toString();
mContext.report(USE_VALUE_OF, node, mContext.getLocation(node), getUseValueOfErrorMessage(
typeName, argument));
}
}
if (mFlagAllocations && !(node.getParent() instanceof Throw) && mCheckAllocations) {
// Make sure we're still inside the method declaration that marked
// mInDraw as true, in case we've left it and we're in a static
// block or something:
Node method = node;
while (method != null) {
if (method instanceof MethodDeclaration) {
break;
}
method = method.getParent();
}
if (method != null && isBlockedAllocationMethod(((MethodDeclaration) method))
&& !isLazilyInitialized(node)) {
reportAllocation(node);
}
}
return super.visitConstructorInvocation(node);
}
private void reportAllocation(Node node) {
mContext.report(PAINT_ALLOC, node, mContext.getLocation(node),
"Avoid object allocations during draw/layout operations (preallocate and " +
"reuse instead)");
}
@Override
public boolean visitMethodInvocation(MethodInvocation node) {
if (mFlagAllocations && node.astOperand() != null) {
// Look for forbidden methods
String methodName = node.astName().astValue();
if (methodName.equals("createBitmap") //$NON-NLS-1$
|| methodName.equals("createScaledBitmap")) { //$NON-NLS-1$
String operand = node.astOperand().toString();
if (operand.equals("Bitmap") //$NON-NLS-1$
|| operand.equals("android.graphics.Bitmap")) { //$NON-NLS-1$
if (!isLazilyInitialized(node)) {
reportAllocation(node);
}
}
} else if (methodName.startsWith("decode")) { //$NON-NLS-1$
// decodeFile, decodeByteArray, ...
String operand = node.astOperand().toString();
if (operand.equals("BitmapFactory") //$NON-NLS-1$
|| operand.equals("android.graphics.BitmapFactory")) { //$NON-NLS-1$
if (!isLazilyInitialized(node)) {
reportAllocation(node);
}
}
} else if (methodName.equals("getClipBounds")) { //$NON-NLS-1$
if (node.astArguments().isEmpty()) {
mContext.report(PAINT_ALLOC, node, mContext.getLocation(node),
"Avoid object allocations during draw operations: Use " +
"`Canvas.getClipBounds(Rect)` instead of `Canvas.getClipBounds()` " +
"which allocates a temporary `Rect`");
}
}
}
return super.visitMethodInvocation(node);
}
/**
* Check whether the given invocation is done as a lazy initialization,
* e.g. {@code if (foo == null) foo = new Foo();}.
* <p>
* This tries to also handle the scenario where the check is on some
* <b>other</b> variable - e.g.
* <pre>
* if (foo == null) {
* foo == init1();
* bar = new Bar();
* }
* </pre>
* or
* <pre>
* if (!initialized) {
* initialized = true;
* bar = new Bar();
* }
* </pre>
*/
private static boolean isLazilyInitialized(Node node) {
Node curr = node.getParent();
while (curr != null) {
if (curr instanceof MethodDeclaration) {
return false;
} else if (curr instanceof If) {
If ifNode = (If) curr;
// See if the if block represents a lazy initialization:
// compute all variable names seen in the condition
// (e.g. for "if (foo == null || bar != foo)" the result is "foo,bar"),
// and then compute all variables assigned to in the if body,
// and if there is an overlap, we'll consider the whole if block
// guarded (so lazily initialized and an allocation we won't complain
// about.)
List<String> assignments = new ArrayList<String>();
AssignmentTracker visitor = new AssignmentTracker(assignments);
ifNode.astStatement().accept(visitor);
if (!assignments.isEmpty()) {
List<String> references = new ArrayList<String>();
addReferencedVariables(references, ifNode.astCondition());
if (!references.isEmpty()) {
SetView<String> intersection = Sets.intersection(
new HashSet<String>(assignments),
new HashSet<String>(references));
return !intersection.isEmpty();
}
}
return false;
}
curr = curr.getParent();
}
return false;
}
/** Adds any variables referenced in the given expression into the given list */
private static void addReferencedVariables(Collection<String> variables,
Expression expression) {
if (expression instanceof BinaryExpression) {
BinaryExpression binary = (BinaryExpression) expression;
addReferencedVariables(variables, binary.astLeft());
addReferencedVariables(variables, binary.astRight());
} else if (expression instanceof UnaryExpression) {
UnaryExpression unary = (UnaryExpression) expression;
addReferencedVariables(variables, unary.astOperand());
} else if (expression instanceof VariableReference) {
VariableReference reference = (VariableReference) expression;
variables.add(reference.astIdentifier().astValue());
} else if (expression instanceof Select) {
Select select = (Select) expression;
if (select.astOperand() instanceof This) {
variables.add(select.astIdentifier().astValue());
}
}
}
/**
* Returns whether the given method declaration represents a method
* where allocating objects is not allowed for performance reasons
*/
private static boolean isBlockedAllocationMethod(MethodDeclaration node) {
return isOnDrawMethod(node) || isOnMeasureMethod(node) || isOnLayoutMethod(node)
|| isLayoutMethod(node);
}
/**
* Returns true if this method looks like it's overriding android.view.View's
* {@code protected void onDraw(Canvas canvas)}
*/
private static boolean isOnDrawMethod(MethodDeclaration node) {
if (ON_DRAW.equals(node.astMethodName().astValue())) {
StrictListAccessor<VariableDefinition, MethodDeclaration> parameters =
node.astParameters();
if (parameters != null && parameters.size() == 1) {
VariableDefinition arg0 = parameters.first();
TypeReferencePart type = arg0.astTypeReference().astParts().last();
String typeName = type.getTypeName();
if (typeName.equals(CANVAS)) {
return true;
}
}
}
return false;
}
/**
* Returns true if this method looks like it's overriding
* android.view.View's
* {@code protected void onLayout(boolean changed, int left, int top,
* int right, int bottom)}
*/
private static boolean isOnLayoutMethod(MethodDeclaration node) {
if (ON_LAYOUT.equals(node.astMethodName().astValue())) {
StrictListAccessor<VariableDefinition, MethodDeclaration> parameters =
node.astParameters();
if (parameters != null && parameters.size() == 5) {
Iterator<VariableDefinition> iterator = parameters.iterator();
if (!iterator.hasNext()) {
return false;
}
// Ensure that the argument list matches boolean, int, int, int, int
TypeReferencePart type = iterator.next().astTypeReference().astParts().last();
if (!type.getTypeName().equals(TYPE_BOOLEAN) || !iterator.hasNext()) {
return false;
}
for (int i = 0; i < 4; i++) {
type = iterator.next().astTypeReference().astParts().last();
if (!type.getTypeName().equals(TYPE_INT)) {
return false;
}
if (!iterator.hasNext()) {
return i == 3;
}
}
}
}
return false;
}
/**
* Returns true if this method looks like it's overriding android.view.View's
* {@code protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)}
*/
private static boolean isOnMeasureMethod(MethodDeclaration node) {
if (ON_MEASURE.equals(node.astMethodName().astValue())) {
StrictListAccessor<VariableDefinition, MethodDeclaration> parameters =
node.astParameters();
if (parameters != null && parameters.size() == 2) {
VariableDefinition arg0 = parameters.first();
VariableDefinition arg1 = parameters.last();
TypeReferencePart type1 = arg0.astTypeReference().astParts().last();
TypeReferencePart type2 = arg1.astTypeReference().astParts().last();
return TYPE_INT.equals(type1.getTypeName())
&& TYPE_INT.equals(type2.getTypeName());
}
}
return false;
}
/**
* Returns true if this method looks like it's overriding android.view.View's
* {@code public void layout(int l, int t, int r, int b)}
*/
private static boolean isLayoutMethod(MethodDeclaration node) {
if (LAYOUT.equals(node.astMethodName().astValue())) {
StrictListAccessor<VariableDefinition, MethodDeclaration> parameters =
node.astParameters();
if (parameters != null && parameters.size() == 4) {
Iterator<VariableDefinition> iterator = parameters.iterator();
for (int i = 0; i < 4; i++) {
if (!iterator.hasNext()) {
return false;
}
VariableDefinition next = iterator.next();
TypeReferencePart type = next.astTypeReference().astParts().last();
if (!TYPE_INT.equals(type.getTypeName())) {
return false;
}
}
return true;
}
}
return false;
}
/**
* Checks whether the given constructor call and type reference refers
* to a HashMap constructor call that is eligible for replacement by a
* SparseArray call instead
*/
private void checkHashMap(ConstructorInvocation node, TypeReference reference) {
// reference.hasTypeArguments returns false where it should not
StrictListAccessor<TypeReference, TypeReference> types = reference.getTypeArguments();
if (types != null && types.size() == 2) {
TypeReference first = types.first();
String typeName = first.getTypeName();
int minSdk = mContext.getMainProject().getMinSdk();
if (typeName.equals(INTEGER) || typeName.equals(BYTE)) {
String valueType = types.last().getTypeName();
if (valueType.equals(INTEGER)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use new `SparseIntArray(...)` instead for better performance");
} else if (valueType.equals(LONG) && minSdk >= 18) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseLongArray(...)` instead for better performance");
} else if (valueType.equals(BOOLEAN)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseBooleanArray(...)` instead for better performance");
} else {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
String.format(
"Use `new SparseArray<%1$s>(...)` instead for better performance",
valueType));
}
} else if (typeName.equals(LONG) && (minSdk >= 16 ||
Boolean.TRUE == mContext.getMainProject().dependsOn(
SUPPORT_LIB_ARTIFACT))) {
boolean useBuiltin = minSdk >= 16;
String message = useBuiltin ?
"Use `new LongSparseArray(...)` instead for better performance" :
"Use `new android.support.v4.util.LongSparseArray(...)` instead for better performance";
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
message);
}
}
}
private void checkSparseArray(ConstructorInvocation node, TypeReference reference) {
// reference.hasTypeArguments returns false where it should not
StrictListAccessor<TypeReference, TypeReference> types = reference.getTypeArguments();
if (types != null && types.size() == 1) {
TypeReference first = types.first();
String valueType = first.getTypeName();
if (valueType.equals(INTEGER)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseIntArray(...)` instead for better performance");
} else if (valueType.equals(BOOLEAN)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseBooleanArray(...)` instead for better performance");
}
}
}
}
private static String getUseValueOfErrorMessage(String typeName, String argument) {
// Keep in sync with {@link #getReplacedType} below
return String.format("Use `%1$s.valueOf(%2$s)` instead", typeName, argument);
}
/**
* For an error message for an {@link #USE_VALUE_OF} issue reported by this detector,
* returns the type being replaced. Intended to use for IDE quickfix implementations.
*/
@Nullable
public static String getReplacedType(@NonNull String message, @NonNull TextFormat format) {
message = format.toText(message);
int index = message.indexOf('.');
if (index != -1 && message.startsWith("Use ")) {
return message.substring(4, index);
}
return null;
}
/** Visitor which records variable names assigned into */
private static class AssignmentTracker extends ForwardingAstVisitor {
private final Collection<String> mVariables;
public AssignmentTracker(Collection<String> variables) {
mVariables = variables;
}
@Override
public boolean visitBinaryExpression(BinaryExpression node) {
BinaryOperator operator = node.astOperator();
if (operator == BinaryOperator.ASSIGN || operator == BinaryOperator.OR_ASSIGN) {
Expression left = node.astLeft();
String variable;
if (left instanceof Select && ((Select) left).astOperand() instanceof This) {
variable = ((Select) left).astIdentifier().astValue();
} else {
variable = left.toString();
}
mVariables.add(variable);
}
return super.visitBinaryExpression(node);
}
}
}