/* * 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 com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.lint.client.api.LintDriver; import com.android.tools.lint.client.api.SdkInfo; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.ClassContext; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Detector.ClassScanner; import com.android.tools.lint.detector.api.Implementation; import com.android.tools.lint.detector.api.Issue; 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 org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.analysis.Analyzer; import org.objectweb.asm.tree.analysis.AnalyzerException; import org.objectweb.asm.tree.analysis.BasicInterpreter; import org.objectweb.asm.tree.analysis.BasicValue; import org.objectweb.asm.tree.analysis.Frame; import java.util.Collections; import java.util.List; /** * Checks for missing view tag detectors */ public class ViewTagDetector extends Detector implements ClassScanner { /** Using setTag and leaking memory */ public static final Issue ISSUE = Issue.create( "ViewTag", //$NON-NLS-1$ "Tagged object leaks", "Prior to Android 4.0, the implementation of `View.setTag(int, Object)` would " + "store the objects in a static map, where the values were strongly referenced. " + "This means that if the object contains any references pointing back to the " + "context, the context (which points to pretty much everything else) will leak. " + "If you pass a view, the view provides a reference to the context " + "that created it. Similarly, view holders typically contain a view, and cursors " + "are sometimes also associated with views.", Category.PERFORMANCE, 6, Severity.WARNING, new Implementation( ViewTagDetector.class, Scope.CLASS_FILE_SCOPE)); /** Constructs a new {@link ViewTagDetector} */ public ViewTagDetector() { } @NonNull @Override public Speed getSpeed() { return Speed.FAST; } // ---- Implements ClassScanner ---- @Override @Nullable public List<String> getApplicableCallNames() { return Collections.singletonList("setTag"); //$NON-NLS-1$ } @Override public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, @NonNull MethodNode method, @NonNull MethodInsnNode call) { // The leak behavior is fixed in ICS: // http://code.google.com/p/android/issues/detail?id=18273 if (context.getMainProject().getMinSdk() >= 14) { return; } String owner = call.owner; String desc = call.desc; if (owner.equals("android/view/View") //$NON-NLS-1$ && desc.equals("(ILjava/lang/Object;)V")) { //$NON-NLS-1$ Analyzer analyzer = new Analyzer(new BasicInterpreter() { @Override public BasicValue newValue(Type type) { if (type == null) { return BasicValue.UNINITIALIZED_VALUE; } else if (type.getSort() == Type.VOID) { return null; } else { return new BasicValue(type); } } }); try { Frame[] frames = analyzer.analyze(classNode.name, method); InsnList instructions = method.instructions; Frame frame = frames[instructions.indexOf(call)]; if (frame.getStackSize() < 3) { return; } BasicValue stackValue = (BasicValue) frame.getStack(2); Type type = stackValue.getType(); if (type == null) { return; } String internalName = type.getInternalName(); String className = type.getClassName(); LintDriver driver = context.getDriver(); SdkInfo sdkInfo = context.getClient().getSdkInfo(context.getMainProject()); String objectType = null; while (className != null) { if (className.equals("android.view.View")) { //$NON-NLS-1$ objectType = "views"; break; } else if (className.endsWith("ViewHolder")) { //$NON-NLS-1$ objectType = "view holders"; break; } else if (className.endsWith("Cursor") //$NON-NLS-1$ && className.startsWith("android.")) { //$NON-NLS-1$ objectType = "cursors"; break; } // TBD: Bitmaps, drawables? That's tricky, because as explained in // http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html // apparently these are used along with nulling out the callbacks, // and that's harder to detect here String parent = sdkInfo.getParentViewClass(className); if (parent == null) { if (internalName == null) { internalName = className.replace('.', '/'); } assert internalName != null; parent = driver.getSuperClass(internalName); } className = parent; internalName = null; } if (objectType != null) { Location location = context.getLocation(call); String message = String.format("Avoid setting %1$s as values for `setTag`: " + "Can lead to memory leaks in versions older than Android 4.0", objectType); context.report(ISSUE, method, call, location, message); } } catch (AnalyzerException e) { context.log(e, null); } } } }