blob: 667b29fea4d28409d2777f53e341434e44f19323 [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.JavaEvaluatorKt.TYPE_BOOLEAN;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_BOOLEAN_WRAPPER;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_BYTE_WRAPPER;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_CHARACTER_WRAPPER;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_DOUBLE_WRAPPER;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_FLOAT_WRAPPER;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_INT;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_INTEGER_WRAPPER;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_LONG_WRAPPER;
import static com.android.tools.lint.detector.api.Lint.getMethodName;
import static com.android.tools.lint.detector.api.Lint.skipParentheses;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.client.api.UElementHandler;
import com.android.tools.lint.detector.api.Category;
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.Lint;
import com.android.tools.lint.detector.api.LintFix;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.SourceCodeScanner;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import org.jetbrains.uast.UBinaryExpression;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UCallableReferenceExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UIfExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UParenthesizedExpression;
import org.jetbrains.uast.UPolyadicExpression;
import org.jetbrains.uast.UPrefixExpression;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.UReferenceExpression;
import org.jetbrains.uast.USimpleNameReferenceExpression;
import org.jetbrains.uast.USuperExpression;
import org.jetbrains.uast.UThisExpression;
import org.jetbrains.uast.UThrowExpression;
import org.jetbrains.uast.UUnaryExpression;
import org.jetbrains.uast.UastCallKind;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
/**
* 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 SourceCodeScanner {
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",
"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)
.setAndroidSpecific(true);
/** Using HashMaps where SparseArray would be better */
public static final Issue USE_SPARSE_ARRAY =
Issue.create(
"UseSparseArrays",
"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)
.setAndroidSpecific(true);
/** Using {@code new Integer()} instead of the more efficient {@code Integer.valueOf} */
public static final Issue USE_VALUE_OF =
Issue.create(
"UseValueOf",
"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";
static final String ON_DRAW = "onDraw";
static final String ON_LAYOUT = "onLayout";
private static final String LAYOUT = "layout";
private static final String HASH_MAP = "java.util.HashMap";
private static final String SPARSE_ARRAY = "android.util.SparseArray";
public static final String CLASS_CANVAS = "android.graphics.Canvas";
/** Constructs a new {@link JavaPerformanceDetector} check */
public JavaPerformanceDetector() {}
// ---- implements SourceCodeScanner ----
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
List<Class<? extends UElement>> types = new ArrayList<>(3);
types.add(UCallExpression.class);
types.add(UMethod.class);
return types;
}
@Override
public UElementHandler createUastHandler(@NonNull JavaContext context) {
return new PerformanceVisitor(context);
}
private static class PerformanceVisitor extends UElementHandler {
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 void visitMethod(@NonNull UMethod node) {
mFlagAllocations = isBlockedAllocationMethod(node);
}
@Override
public void visitCallExpression(@NonNull UCallExpression node) {
UastCallKind kind = node.getKind();
if (kind == UastCallKind.CONSTRUCTOR_CALL
|| kind == UastCallKind.NEW_ARRAY_WITH_DIMENSIONS
|| kind == UastCallKind.NEW_ARRAY_WITH_INITIALIZER) {
visitConstructorCallExpression(node, kind != UastCallKind.CONSTRUCTOR_CALL);
} else if (UastExpressionUtils.isMethodCall(node)) {
visitMethodCallExpression(node);
}
}
private void visitConstructorCallExpression(
@NonNull UCallExpression node, boolean isArray) {
String typeName = null;
UReferenceExpression classReference = node.getClassReference();
if (mCheckMaps || mCheckValueOf) {
if (classReference != null) {
typeName = UastUtils.getQualifiedName(classReference);
}
}
if (!isArray && mCheckMaps) {
if (SPARSE_ARRAY.equals(typeName)) {
checkSparseArray(node);
}
}
if (!isArray && mCheckValueOf) {
if (typeName != null
&& (typeName.equals(TYPE_INTEGER_WRAPPER)
|| typeName.equals(TYPE_BOOLEAN_WRAPPER)
|| typeName.equals(TYPE_FLOAT_WRAPPER)
|| typeName.equals(TYPE_CHARACTER_WRAPPER)
|| typeName.equals(TYPE_LONG_WRAPPER)
|| typeName.equals(TYPE_DOUBLE_WRAPPER)
|| typeName.equals(TYPE_BYTE_WRAPPER))
//&& node.astTypeReference().astParts().size() == 1
&& node.getValueArgumentCount() == 1) {
String argument = node.getValueArguments().get(0).asSourceString();
String replacedType = typeName.substring(typeName.lastIndexOf('.') + 1);
LintFix fix = null;
if (!Lint.isKotlin(node.getSourcePsi())) {
fix =
LintFix.create()
.name("Replace with valueOf()", true)
.replace()
.pattern("(new\\s+" + replacedType + ")")
.with(replacedType + ".valueOf")
.autoFix()
.build();
}
mContext.report(
USE_VALUE_OF,
node,
mContext.getLocation(node),
getUseValueOfErrorMessage(typeName, argument),
fix);
}
}
if (mFlagAllocations
&& !(skipParentheses(node.getUastParent()) instanceof UThrowExpression)
&& 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:
UMethod method = UastUtils.getParentOfType(node, UMethod.class);
if (method != null
&& isBlockedAllocationMethod(method)
&& !isLazilyInitialized(node)) {
reportAllocation(node);
}
}
}
private void reportAllocation(UCallExpression node) {
Location location = mContext.getLocation(node);
mContext.report(
PAINT_ALLOC,
node,
location,
"Avoid object allocations during draw/layout operations (preallocate and "
+ "reuse instead)");
}
private void visitMethodCallExpression(UCallExpression node) {
if (!mFlagAllocations) {
return;
}
UExpression receiver = node.getReceiver();
if (receiver == null) {
return;
}
String functionName = getMethodName(node);
if (functionName == null) {
return;
}
// Look for forbidden methods
if (functionName.equals("createBitmap") || functionName.equals("createScaledBitmap")) {
PsiMethod method = node.resolve();
if (method != null
&& mContext.getEvaluator()
.isMemberInClass(method, "android.graphics.Bitmap")
&& !isLazilyInitialized(node)) {
reportAllocation(node);
}
} else if (functionName.startsWith("decode")) {
// decodeFile, decodeByteArray, ...
PsiMethod method = node.resolve();
if (method != null
&& mContext.getEvaluator()
.isMemberInClass(method, "android.graphics.BitmapFactory")
&& !isLazilyInitialized(node)) {
reportAllocation(node);
}
} else if (functionName.equals("getClipBounds")
|| functionName.equals("clipBounds")) { // Kotlin
if (node.getValueArgumentCount() == 0) {
Location callLocation = mContext.getLocation(node);
mContext.report(
PAINT_ALLOC,
node,
callLocation,
"Avoid object allocations during draw operations: Use "
+ "`Canvas.getClipBounds(Rect)` instead of `Canvas.getClipBounds()` "
+ "which allocates a temporary `Rect`");
}
}
}
/**
* 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(UElement node) {
UElement curr = node.getUastParent();
while (curr != null) {
if (curr instanceof UMethod) {
return false;
} else if (curr instanceof UIfExpression) {
UIfExpression ifNode = (UIfExpression) 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<>();
AssignmentTracker visitor = new AssignmentTracker(assignments);
if (ifNode.getThenExpression() != null) {
ifNode.getThenExpression().accept(visitor);
}
if (!assignments.isEmpty()) {
List<String> references = new ArrayList<>();
addReferencedVariables(references, ifNode.getCondition());
if (!references.isEmpty()) {
SetView<String> intersection =
Sets.intersection(
new HashSet<>(assignments), new HashSet<>(references));
return !intersection.isEmpty();
}
}
return false;
}
curr = curr.getUastParent();
}
return false;
}
/** Adds any variables referenced in the given expression into the given list */
// TODO: This is old, bogus code. We should switch to a simple PsiVariable visitor!
private static void addReferencedVariables(
@NonNull Collection<String> variables, @Nullable UExpression expression) {
if (expression instanceof UPolyadicExpression) {
UPolyadicExpression polyadicExpression = (UPolyadicExpression) expression;
for (UExpression operand : polyadicExpression.getOperands()) {
addReferencedVariables(variables, operand);
}
} else if (expression instanceof UPrefixExpression) {
UPrefixExpression unary = (UPrefixExpression) expression;
addReferencedVariables(variables, unary.getOperand());
} else if (expression instanceof UUnaryExpression) {
UUnaryExpression unary = (UUnaryExpression) expression;
addReferencedVariables(variables, unary.getOperand());
} else if (expression instanceof UParenthesizedExpression) {
UParenthesizedExpression exp = (UParenthesizedExpression) expression;
addReferencedVariables(variables, exp.getExpression());
} else if (expression instanceof USimpleNameReferenceExpression) {
USimpleNameReferenceExpression reference =
(USimpleNameReferenceExpression) expression;
variables.add(reference.getIdentifier());
} else if (expression instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression ref = (UQualifiedReferenceExpression) expression;
UExpression receiver = ref.getReceiver();
UExpression selector = ref.getSelector();
if (receiver instanceof UThisExpression || receiver instanceof USuperExpression) {
String identifier =
(selector instanceof USimpleNameReferenceExpression)
? ((USimpleNameReferenceExpression) selector).getIdentifier()
: null;
if (identifier != null) {
variables.add(identifier);
}
} else if (receiver instanceof UCallableReferenceExpression
&& selector instanceof USimpleNameReferenceExpression
&& "isInitialized"
.equals(((USimpleNameReferenceExpression) selector).getIdentifier())
&& (((UCallableReferenceExpression) receiver).getQualifierExpression()
== null)) {
// If's of the form "if (!::field.isInitialized)"
String name = ((UCallableReferenceExpression) receiver).getCallableName();
variables.add(name);
}
}
}
/**
* Returns whether the given method declaration represents a method where allocating objects
* is not allowed for performance reasons
*/
private boolean isBlockedAllocationMethod(@NonNull PsiMethod node) {
JavaEvaluator evaluator = mContext.getEvaluator();
return isOnDrawMethod(evaluator, node)
|| isOnMeasureMethod(evaluator, node)
|| isOnLayoutMethod(evaluator, node)
|| isLayoutMethod(evaluator, 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(
@NonNull JavaEvaluator evaluator, @NonNull PsiMethod node) {
return ON_DRAW.equals(node.getName()) && evaluator.parametersMatch(node, CLASS_CANVAS);
}
/**
* 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(
@NonNull JavaEvaluator evaluator, @NonNull PsiMethod node) {
return ON_LAYOUT.equals(node.getName())
&& evaluator.parametersMatch(
node, TYPE_BOOLEAN, TYPE_INT, TYPE_INT, TYPE_INT, TYPE_INT);
}
/**
* 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(
@NonNull JavaEvaluator evaluator, @NonNull PsiMethod node) {
return ON_MEASURE.equals(node.getName())
&& evaluator.parametersMatch(node, TYPE_INT, TYPE_INT);
}
/**
* 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(
@NonNull JavaEvaluator evaluator, @NonNull PsiMethod node) {
return LAYOUT.equals(node.getName())
&& evaluator.parametersMatch(node, TYPE_INT, TYPE_INT, TYPE_INT, TYPE_INT);
}
/**
* 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
*
* <p>Deprecated because, while SparseArray is more memory efficient and cache friendly than
* HashMap, it is also generally slower than a HashMap because lookups require a binary
* search. It is also not intended to be appropriate for maps containing a large number of
* items. Thus it does not make sense to unconditionally recommend SparseArray.
*/
@Deprecated
private void checkHashMap(@NonNull UCallExpression node) {
if (!mContext.getProject().isAndroidProject()) {
return;
}
List<PsiType> types = node.getTypeArguments();
if (types.size() == 2) {
PsiType first = types.get(0);
String typeName = first.getCanonicalText();
int minSdk = mContext.getMainProject().getMinSdk();
if (TYPE_INTEGER_WRAPPER.equals(typeName) || TYPE_BYTE_WRAPPER.equals(typeName)) {
String valueType = types.get(1).getCanonicalText();
if (valueType.equals(TYPE_INTEGER_WRAPPER)) {
mContext.report(
USE_SPARSE_ARRAY,
node,
mContext.getLocation(node),
"Use new `SparseIntArray(...)` instead for better performance");
} else if (valueType.equals(TYPE_LONG_WRAPPER) && minSdk >= 18) {
mContext.report(
USE_SPARSE_ARRAY,
node,
mContext.getLocation(node),
"Use `new SparseLongArray(...)` instead for better performance");
} else if (valueType.equals(TYPE_BOOLEAN_WRAPPER)) {
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.substring(valueType.lastIndexOf('.') + 1)));
}
} else if (TYPE_LONG_WRAPPER.equals(typeName)
&& (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(@NonNull UCallExpression node) {
List<PsiType> types = node.getTypeArguments();
if (types.size() == 1) {
String valueType = types.get(0).getCanonicalText();
if (valueType.equals(TYPE_INTEGER_WRAPPER)) {
mContext.report(
USE_SPARSE_ARRAY,
node,
mContext.getLocation(node),
"Use `new SparseIntArray(...)` instead for better performance");
} else if (valueType.equals(TYPE_BOOLEAN_WRAPPER)) {
mContext.report(
USE_SPARSE_ARRAY,
node,
mContext.getLocation(node),
"Use `new SparseBooleanArray(...)` instead for better performance");
}
}
}
}
private static String getUseValueOfErrorMessage(String typeName, String argument) {
return String.format(
"Use `%1$s.valueOf(%2$s)` instead",
typeName.substring(typeName.lastIndexOf('.') + 1), argument);
}
/** Visitor which records variable names assigned into */
private static class AssignmentTracker extends AbstractUastVisitor {
private final Collection<String> mVariables;
public AssignmentTracker(Collection<String> variables) {
mVariables = variables;
}
@Override
public boolean visitBinaryExpression(UBinaryExpression node) {
if (UastExpressionUtils.isAssignment(node)) {
UExpression left = node.getLeftOperand();
if (left instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression ref = (UQualifiedReferenceExpression) left;
if (ref.getReceiver() instanceof UThisExpression
|| ref.getReceiver() instanceof USuperExpression) {
PsiElement resolved = ref.resolve();
if (resolved instanceof PsiField) {
mVariables.add(((PsiField) resolved).getName());
}
} else {
PsiElement resolved = ref.resolve();
if (resolved instanceof PsiField) {
mVariables.add(((PsiField) resolved).getName());
}
}
} else if (left instanceof USimpleNameReferenceExpression) {
mVariables.add(((USimpleNameReferenceExpression) left).getIdentifier());
}
}
return super.visitBinaryExpression(node);
}
}
}