/* * Copyright (C) 2011 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 com.android.annotations.NonNull; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.ClassContext; 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.LintUtils; 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.Speed; import com.google.common.collect.Maps; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.VarInsnNode; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Looks for getter calls within the same class that could be replaced by * direct field references instead. */ public class FieldGetterDetector extends Detector implements Detector.ClassScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "FieldGetter", //$NON-NLS-1$ "Using getter instead of field", "Accessing a field within the class that defines a getter for that field is " + "at least 3 times faster than calling the getter. For simple getters that do " + "nothing other than return the field, you might want to just reference the " + "local field directly instead.\n" + "\n" + "*NOTE*: As of Android 2.3 (Gingerbread), this optimization is performed " + "automatically by Dalvik, so there is no need to change your code; this is " + "only relevant if you are targeting older versions of Android.", Category.PERFORMANCE, 4, Severity.WARNING, new Implementation( FieldGetterDetector.class, Scope.CLASS_FILE_SCOPE)). // This is a micro-optimization: not enabled by default setEnabledByDefault(false). addMoreInfo( "http://developer.android.com/guide/practices/design/performance.html#internal_get_set"); //$NON-NLS-1$ private ArrayList<Entry> mPendingCalls; /** Constructs a new {@link FieldGetterDetector} check */ public FieldGetterDetector() { } @NonNull @Override public Speed getSpeed() { return Speed.FAST; } // ---- Implements ClassScanner ---- @Override public int[] getApplicableAsmNodeTypes() { return new int[] { AbstractInsnNode.METHOD_INSN }; } @Override public void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode, @NonNull MethodNode method, @NonNull AbstractInsnNode instruction) { // As of Gingerbread/API 9, Dalvik performs this optimization automatically if (context.getProject().getMinSdk() >= 9) { return; } if ((method.access & Opcodes.ACC_STATIC) != 0) { // Not an instance method return; } if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) { return; } MethodInsnNode node = (MethodInsnNode) instruction; String name = node.name; String owner = node.owner; AbstractInsnNode prev = LintUtils.getPrevInstruction(instruction); if (prev == null || prev.getOpcode() != Opcodes.ALOAD) { return; } VarInsnNode prevVar = (VarInsnNode) prev; if (prevVar.var != 0) { // Not on "this", variable 0 in instance methods? return; } if (((name.startsWith("get") && name.length() > 3 //$NON-NLS-1$ && Character.isUpperCase(name.charAt(3))) || (name.startsWith("is") && name.length() > 2 //$NON-NLS-1$ && Character.isUpperCase(name.charAt(2)))) && owner.equals(classNode.name)) { // Calling a potential getter method on self. We now need to // investigate the method body of the getter call and make sure // it's really a plain getter, not just a method which happens // to have a method name like a getter, or a method which not // only returns a field but possibly computes it or performs // other initialization or side effects. This is done in a // second pass over the bytecode, initiated by the finish() // method. if (mPendingCalls == null) { mPendingCalls = new ArrayList<Entry>(); } mPendingCalls.add(new Entry(name, node, method)); } super.checkInstruction(context, classNode, method, instruction); } @Override public void afterCheckFile(@NonNull Context c) { ClassContext context = (ClassContext) c; if (mPendingCalls != null) { Set<String> names = new HashSet<String>(mPendingCalls.size()); for (Entry entry : mPendingCalls) { names.add(entry.name); } Map<String, String> getters = checkMethods(context.getClassNode(), names); if (!getters.isEmpty()) { for (String getter : getters.keySet()) { for (Entry entry : mPendingCalls) { String name = entry.name; // There can be more than one reference to the same name: // one for each call site if (name.equals(getter)) { Location location = context.getLocation(entry.call); String fieldName = getters.get(getter); if (fieldName == null) { fieldName = ""; } context.report(ISSUE, entry.method, entry.call, location, String.format( "Calling getter method `%1$s()` on self is " + "slower than field access (`%2$s`)", getter, fieldName)); } } } } } mPendingCalls = null; } // Holder class for getters to be checked private static class Entry { public final String name; public final MethodNode method; public final MethodInsnNode call; public Entry(String name, MethodInsnNode call, MethodNode method) { super(); this.name = name; this.call = call; this.method = method; } } // Validate that these getter methods are really just simple field getters // like these int and String getters: // public int getFoo(); // Code: // 0: aload_0 // 1: getfield #21; //Field mFoo:I // 4: ireturn // // public java.lang.String getBar(); // Code: // 0: aload_0 // 1: getfield #25; //Field mBar:Ljava/lang/String; // 4: areturn // // Returns a map of valid getters as keys, and if the field name is found, the field name // for each getter as its value. private static Map<String, String> checkMethods(ClassNode classNode, Set<String> names) { Map<String, String> validGetters = Maps.newHashMap(); @SuppressWarnings("rawtypes") List methods = classNode.methods; String fieldName = null; checkMethod: for (Object methodObject : methods) { MethodNode method = (MethodNode) methodObject; if (names.contains(method.name) && method.desc.startsWith("()")) { //$NON-NLS-1$ // (): No arguments InsnList instructions = method.instructions; int mState = 1; for (AbstractInsnNode curr = instructions.getFirst(); curr != null; curr = curr.getNext()) { switch (curr.getOpcode()) { case -1: // Skip label and line number nodes continue; case Opcodes.ALOAD: if (mState == 1) { fieldName = null; mState = 2; } else { continue checkMethod; } break; case Opcodes.GETFIELD: if (mState == 2) { FieldInsnNode field = (FieldInsnNode) curr; fieldName = field.name; mState = 3; } else { continue checkMethod; } break; case Opcodes.ARETURN: case Opcodes.FRETURN: case Opcodes.IRETURN: case Opcodes.DRETURN: case Opcodes.LRETURN: case Opcodes.RETURN: if (mState == 3) { validGetters.put(method.name, fieldName); } continue checkMethod; default: continue checkMethod; } } } } return validGetters; } }