| /* |
| * 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); |
| } |
| } |
| } |