/* * Copyright (C) 2007 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 android.view; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Debug; import android.os.Environment; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.DisplayMetrics; import android.util.Log; import android.util.Printer; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Various debugging/tracing tools related to {@link View} and the view hierarchy. */ public class ViewDebug { /** * Log tag used to log errors related to the consistency of the view hierarchy. * * @hide */ public static final String CONSISTENCY_LOG_TAG = "ViewConsistency"; /** * Flag indicating the consistency check should check layout-related properties. * * @hide */ public static final int CONSISTENCY_LAYOUT = 0x1; /** * Flag indicating the consistency check should check drawing-related properties. * * @hide */ public static final int CONSISTENCY_DRAWING = 0x2; /** * Enables or disables view hierarchy tracing. Any invoker of * {@link #trace(View, android.view.ViewDebug.HierarchyTraceType)} should first * check that this value is set to true as not to affect performance. */ public static final boolean TRACE_HIERARCHY = false; /** * Enables or disables view recycler tracing. Any invoker of * {@link #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])} should first * check that this value is set to true as not to affect performance. */ public static final boolean TRACE_RECYCLER = false; /** * Profiles drawing times in the events log. * * @hide */ public static final boolean DEBUG_PROFILE_DRAWING = false; /** * Profiles layout times in the events log. * * @hide */ public static final boolean DEBUG_PROFILE_LAYOUT = false; /** * Enables detailed logging of drag/drop operations. * @hide */ public static final boolean DEBUG_DRAG = false; /** * Enables logging of factors that affect the latency and responsiveness of an application. * * Logs the relative difference between the time an event was created and the time it * was delivered. * * Logs the time spent waiting for Surface.lockCanvas() or eglSwapBuffers(). * This is time that the event loop spends blocked and unresponsive. Ideally, drawing * and animations should be perfectly synchronized with VSYNC so that swap buffers * is instantaneous. * * Logs the time spent in ViewRoot.performTraversals() or ViewRoot.draw(). * @hide */ public static final boolean DEBUG_LATENCY = false; /** *
Enables or disables views consistency check. Even when this property is enabled, * view consistency checks happen only if {@link false} is set * to true. The value of this property can be configured externally in one of the * following files:
** @ViewDebug.ExportedProperty(mapping = { * @ViewDebug.IntToString(from = 0, to = "VISIBLE"), * @ViewDebug.IntToString(from = 4, to = "INVISIBLE"), * @ViewDebug.IntToString(from = 8, to = "GONE") * }) * public int getVisibility() { ... ** * @return An array of int to String mappings * * @see android.view.ViewDebug.IntToString */ IntToString[] mapping() default { }; /** * A mapping can be defined to map array indices to specific strings. * A mapping can be used to see human readable values for the indices * of an array: * ** @ViewDebug.ExportedProperty(indexMapping = { * @ViewDebug.IntToString(from = 0, to = "INVALID"), * @ViewDebug.IntToString(from = 1, to = "FIRST"), * @ViewDebug.IntToString(from = 2, to = "SECOND") * }) * private int[] mElements; ** * @return An array of int to String mappings * * @see android.view.ViewDebug.IntToString * @see #mapping() */ IntToString[] indexMapping() default { }; /** * A flags mapping can be defined to map flags encoded in an integer to * specific strings. A mapping can be used to see human readable values * for the flags of an integer: * ** @ViewDebug.ExportedProperty(flagMapping = { * @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED, name = "ENABLED"), * @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED, name = "DISABLED"), * }) * private int mFlags; ** * A specified String is output when the following is true: * * @return An array of int to String mappings */ FlagToString[] flagMapping() default { }; /** * When deep export is turned on, this property is not dumped. Instead, the * properties contained in this property are dumped. Each child property * is prefixed with the name of this property. * * @return true if the properties of this property should be dumped * * @see #prefix() */ boolean deepExport() default false; /** * The prefix to use on child properties when deep export is enabled * * @return a prefix as a String * * @see #deepExport() */ String prefix() default ""; /** * Specifies the category the property falls into, such as measurement, * layout, drawing, etc. * * @return the category as String */ String category() default ""; } /** * Defines a mapping from an int value to a String. Such a mapping can be used * in a @ExportedProperty to provide more meaningful values to the end user. * * @see android.view.ViewDebug.ExportedProperty */ @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface IntToString { /** * The original int value to map to a String. * * @return An arbitrary int value. */ int from(); /** * The String to use in place of the original int value. * * @return An arbitrary non-null String. */ String to(); } /** * Defines a mapping from an flag to a String. Such a mapping can be used * in a @ExportedProperty to provide more meaningful values to the end user. * * @see android.view.ViewDebug.ExportedProperty */ @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface FlagToString { /** * The mask to apply to the original value. * * @return An arbitrary int value. */ int mask(); /** * The value to compare to the result of: *original value & {@link #mask()}
. * * @return An arbitrary value. */ int equals(); /** * The String to use in place of the original int value. * * @return An arbitrary non-null String. */ String name(); /** * Indicates whether to output the flag when the test is true, * or false. Defaults to true. */ boolean outputIf() default true; } /** * This annotation can be used to mark fields and methods to be dumped when * the view is captured. Methods with this annotation must have no arguments * and must return a valid type of data. */ @Target({ ElementType.FIELD, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface CapturedViewProperty { /** * When retrieveReturn is true, we need to retrieve second level methods * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod() * we will set retrieveReturn = true on the annotation of * myView.getFirstLevelMethod() * @return true if we need the second level methods */ boolean retrieveReturn() default false; } private static HashMap, Method[]> mCapturedViewMethodsForClasses = null; private static HashMap , Field[]> mCapturedViewFieldsForClasses = null; // Maximum delay in ms after which we stop trying to capture a View's drawing private static final int CAPTURE_TIMEOUT = 4000; private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE"; private static final String REMOTE_COMMAND_DUMP = "DUMP"; private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE"; private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT"; private static final String REMOTE_PROFILE = "PROFILE"; private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS"; private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST"; private static HashMap , Field[]> sFieldsForClasses; private static HashMap , Method[]> sMethodsForClasses; private static HashMap sAnnotations; /** * Defines the type of hierarhcy trace to output to the hierarchy traces file. */ public enum HierarchyTraceType { INVALIDATE, INVALIDATE_CHILD, INVALIDATE_CHILD_IN_PARENT, REQUEST_LAYOUT, ON_LAYOUT, ON_MEASURE, DRAW, BUILD_CACHE } private static BufferedWriter sHierarchyTraces; private static ViewRootImpl sHierarhcyRoot; private static String sHierarchyTracePrefix; /** * Defines the type of recycler trace to output to the recycler traces file. */ public enum RecyclerTraceType { NEW_VIEW, BIND_VIEW, RECYCLE_FROM_ACTIVE_HEAP, RECYCLE_FROM_SCRAP_HEAP, MOVE_TO_SCRAP_HEAP, MOVE_FROM_ACTIVE_TO_SCRAP_HEAP } private static class RecyclerTrace { public int view; public RecyclerTraceType type; public int position; public int indexOnScreen; } private static View sRecyclerOwnerView; private static List sRecyclerViews; private static List sRecyclerTraces; private static String sRecyclerTracePrefix; private static final ThreadLocal sLooperProfilerStorage = new ThreadLocal (); /** * Returns the number of instanciated Views. * * @return The number of Views instanciated in the current process. * * @hide */ public static long getViewInstanceCount() { return Debug.countInstancesOfClass(View.class); } /** * Returns the number of instanciated ViewAncestors. * * @return The number of ViewAncestors instanciated in the current process. * * @hide */ public static long getViewAncestorInstanceCount() { return Debug.countInstancesOfClass(ViewRootImpl.class); } /** * Starts profiling the looper associated with the current thread. * You must call {@link #stopLooperProfiling} to end profiling * and obtain the traces. Both methods must be invoked on the * same thread. * * @param traceFile The path where to write the looper traces * * @see #stopLooperProfiling() */ public static void startLooperProfiling(File traceFile) { if (sLooperProfilerStorage.get() == null) { LooperProfiler profiler = new LooperProfiler(traceFile); sLooperProfilerStorage.set(profiler); Looper.myLooper().setMessageLogging(profiler); } } /** * Stops profiling the looper associated with the current thread. * * @see #startLooperProfiling(java.io.File) */ public static void stopLooperProfiling() { LooperProfiler profiler = sLooperProfilerStorage.get(); if (profiler != null) { sLooperProfilerStorage.remove(); Looper.myLooper().setMessageLogging(null); profiler.save(); } } private static class LooperProfiler implements Looper.Profiler, Printer { private static final int LOOPER_PROFILER_VERSION = 1; private static final String LOG_TAG = "LooperProfiler"; private final ArrayList mTraces = new ArrayList (512); private final File mTraceFile; public LooperProfiler(File traceFile) { mTraceFile = traceFile; } @Override public void println(String x) { // Ignore messages } @Override public void profile(Message message, long wallStart, long wallTime, long threadTime) { Entry entry = new Entry(); entry.messageId = message.what; entry.name = message.getTarget().getMessageName(message); entry.wallStart = wallStart; entry.wallTime = wallTime; entry.threadTime = threadTime; mTraces.add(entry); } void save() { // Don't block the UI thread new Thread(new Runnable() { @Override public void run() { saveTraces(); } }, "LooperProfiler[" + mTraceFile + "]").start(); } private void saveTraces() { FileOutputStream fos; try { fos = new FileOutputStream(mTraceFile); } catch (FileNotFoundException e) { Log.e(LOG_TAG, "Could not open trace file: " + mTraceFile); return; } DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fos)); try { out.writeInt(LOOPER_PROFILER_VERSION); out.writeInt(mTraces.size()); for (Entry entry : mTraces) { saveTrace(entry, out); } Log.d(LOG_TAG, "Looper traces ready: " + mTraceFile); } catch (IOException e) { Log.e(LOG_TAG, "Could not write trace file: ", e); } finally { try { out.close(); } catch (IOException e) { // Ignore } } } private void saveTrace(Entry entry, DataOutputStream out) throws IOException { out.writeInt(entry.messageId); out.writeUTF(entry.name); out.writeLong(entry.wallStart); out.writeLong(entry.wallTime); out.writeLong(entry.threadTime); } static class Entry { int messageId; String name; long wallStart; long wallTime; long threadTime; } } /** * Outputs a trace to the currently opened recycler traces. The trace records the type of * recycler action performed on the supplied view as well as a number of parameters. * * @param view the view to trace * @param type the type of the trace * @param parameters parameters depending on the type of the trace */ public static void trace(View view, RecyclerTraceType type, int... parameters) { if (sRecyclerOwnerView == null || sRecyclerViews == null) { return; } if (!sRecyclerViews.contains(view)) { sRecyclerViews.add(view); } final int index = sRecyclerViews.indexOf(view); RecyclerTrace trace = new RecyclerTrace(); trace.view = index; trace.type = type; trace.position = parameters[0]; trace.indexOnScreen = parameters[1]; sRecyclerTraces.add(trace); } /** * Starts tracing the view recycler of the specified view. The trace is identified by a prefix, * used to build the traces files names: /EXTERNAL/view-recycler/PREFIX.traces
and */EXTERNAL/view-recycler/PREFIX.recycler
. * * Only one view recycler can be traced at the same time. After calling this method, any * other invocation will result in aIllegalStateException
unless * {@link #stopRecyclerTracing()} is invoked before. * * Traces files are created only after {@link #stopRecyclerTracing()} is invoked. * * This method will return immediately if TRACE_RECYCLER is false. * * @param prefix the traces files name prefix * @param view the view whose recycler must be traced * * @see #stopRecyclerTracing() * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[]) */ public static void startRecyclerTracing(String prefix, View view) { //noinspection PointlessBooleanExpression,ConstantConditions if (!TRACE_RECYCLER) { return; } if (sRecyclerOwnerView != null) { throw new IllegalStateException("You must call stopRecyclerTracing() before running" + " a new trace!"); } sRecyclerTracePrefix = prefix; sRecyclerOwnerView = view; sRecyclerViews = new ArrayList(); sRecyclerTraces = new LinkedList (); } /** * Stops the current view recycer tracing. * * Calling this method creates the file /EXTERNAL/view-recycler/PREFIX.traces
* containing all the traces (or method calls) relative to the specified view's recycler. * * Calling this method creates the file/EXTERNAL/view-recycler/PREFIX.recycler
* containing all of the views used by the recycler of the view supplied to * {@link #startRecyclerTracing(String, View)}. * * This method will return immediately if TRACE_RECYCLER is false. * * @see #startRecyclerTracing(String, View) * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[]) */ public static void stopRecyclerTracing() { //noinspection PointlessBooleanExpression,ConstantConditions if (!TRACE_RECYCLER) { return; } if (sRecyclerOwnerView == null || sRecyclerViews == null) { throw new IllegalStateException("You must call startRecyclerTracing() before" + " stopRecyclerTracing()!"); } File recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/"); //noinspection ResultOfMethodCallIgnored recyclerDump.mkdirs(); recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".recycler"); try { final BufferedWriter out = new BufferedWriter(new FileWriter(recyclerDump), 8 * 1024); for (View view : sRecyclerViews) { final String name = view.getClass().getName(); out.write(name); out.newLine(); } out.close(); } catch (IOException e) { Log.e("View", "Could not dump recycler content"); return; } recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/"); recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".traces"); try { if (recyclerDump.exists()) { //noinspection ResultOfMethodCallIgnored recyclerDump.delete(); } final FileOutputStream file = new FileOutputStream(recyclerDump); final DataOutputStream out = new DataOutputStream(file); for (RecyclerTrace trace : sRecyclerTraces) { out.writeInt(trace.view); out.writeInt(trace.type.ordinal()); out.writeInt(trace.position); out.writeInt(trace.indexOnScreen); out.flush(); } out.close(); } catch (IOException e) { Log.e("View", "Could not dump recycler traces"); return; } sRecyclerViews.clear(); sRecyclerViews = null; sRecyclerTraces.clear(); sRecyclerTraces = null; sRecyclerOwnerView = null; } /** * Outputs a trace to the currently opened traces file. The trace contains the class name * and instance's hashcode of the specified view as well as the supplied trace type. * * @param view the view to trace * @param type the type of the trace */ public static void trace(View view, HierarchyTraceType type) { if (sHierarchyTraces == null) { return; } try { sHierarchyTraces.write(type.name()); sHierarchyTraces.write(' '); sHierarchyTraces.write(view.getClass().getName()); sHierarchyTraces.write('@'); sHierarchyTraces.write(Integer.toHexString(view.hashCode())); sHierarchyTraces.newLine(); } catch (IOException e) { Log.w("View", "Error while dumping trace of type " + type + " for view " + view); } } /** * Starts tracing the view hierarchy of the specified view. The trace is identified by a prefix, * used to build the traces files names:/EXTERNAL/view-hierarchy/PREFIX.traces
and */EXTERNAL/view-hierarchy/PREFIX.tree
. * * Only one view hierarchy can be traced at the same time. After calling this method, any * other invocation will result in aIllegalStateException
unless * {@link #stopHierarchyTracing()} is invoked before. * * Calling this method creates the file/EXTERNAL/view-hierarchy/PREFIX.traces
* containing all the traces (or method calls) relative to the specified view's hierarchy. * * This method will return immediately if TRACE_HIERARCHY is false. * * @param prefix the traces files name prefix * @param view the view whose hierarchy must be traced * * @see #stopHierarchyTracing() * @see #trace(View, android.view.ViewDebug.HierarchyTraceType) */ public static void startHierarchyTracing(String prefix, View view) { //noinspection PointlessBooleanExpression,ConstantConditions if (!TRACE_HIERARCHY) { return; } if (sHierarhcyRoot != null) { throw new IllegalStateException("You must call stopHierarchyTracing() before running" + " a new trace!"); } File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/"); //noinspection ResultOfMethodCallIgnored hierarchyDump.mkdirs(); hierarchyDump = new File(hierarchyDump, prefix + ".traces"); sHierarchyTracePrefix = prefix; try { sHierarchyTraces = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024); } catch (IOException e) { Log.e("View", "Could not dump view hierarchy"); return; } sHierarhcyRoot = (ViewRootImpl) view.getRootView().getParent(); } /** * Stops the current view hierarchy tracing. This method closes the file */EXTERNAL/view-hierarchy/PREFIX.traces
. * * Calling this method creates the file/EXTERNAL/view-hierarchy/PREFIX.tree
* containing the view hierarchy of the view supplied to * {@link #startHierarchyTracing(String, View)}. * * This method will return immediately if TRACE_HIERARCHY is false. * * @see #startHierarchyTracing(String, View) * @see #trace(View, android.view.ViewDebug.HierarchyTraceType) */ public static void stopHierarchyTracing() { //noinspection PointlessBooleanExpression,ConstantConditions if (!TRACE_HIERARCHY) { return; } if (sHierarhcyRoot == null || sHierarchyTraces == null) { throw new IllegalStateException("You must call startHierarchyTracing() before" + " stopHierarchyTracing()!"); } try { sHierarchyTraces.close(); } catch (IOException e) { Log.e("View", "Could not write view traces"); } sHierarchyTraces = null; File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/"); //noinspection ResultOfMethodCallIgnored hierarchyDump.mkdirs(); hierarchyDump = new File(hierarchyDump, sHierarchyTracePrefix + ".tree"); BufferedWriter out; try { out = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024); } catch (IOException e) { Log.e("View", "Could not dump view hierarchy"); return; } View view = sHierarhcyRoot.getView(); if (view instanceof ViewGroup) { ViewGroup group = (ViewGroup) view; dumpViewHierarchy(group, out, 0); try { out.close(); } catch (IOException e) { Log.e("View", "Could not dump view hierarchy"); } } sHierarhcyRoot = null; } static void dispatchCommand(View view, String command, String parameters, OutputStream clientStream) throws IOException { // Paranoid but safe... view = view.getRootView(); if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) { dump(view, clientStream); } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) { captureLayers(view, new DataOutputStream(clientStream)); } else { final String[] params = parameters.split(" "); if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) { capture(view, clientStream, params[0]); } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) { outputDisplayList(view, params[0]); } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) { invalidate(view, params[0]); } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) { requestLayout(view, params[0]); } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) { profile(view, clientStream, params[0]); } } } private static View findView(View root, String parameter) { // Look by type/hashcode if (parameter.indexOf('@') != -1) { final String[] ids = parameter.split("@"); final String className = ids[0]; final int hashCode = (int) Long.parseLong(ids[1], 16); View view = root.getRootView(); if (view instanceof ViewGroup) { return findView((ViewGroup) view, className, hashCode); } } else { // Look by id final int id = root.getResources().getIdentifier(parameter, null, null); return root.getRootView().findViewById(id); } return null; } private static void invalidate(View root, String parameter) { final View view = findView(root, parameter); if (view != null) { view.postInvalidate(); } } private static void requestLayout(View root, String parameter) { final View view = findView(root, parameter); if (view != null) { root.post(new Runnable() { public void run() { view.requestLayout(); } }); } } private static void profile(View root, OutputStream clientStream, String parameter) throws IOException { final View view = findView(root, parameter); BufferedWriter out = null; try { out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024); if (view != null) { profileViewAndChildren(view, out); } else { out.write("-1 -1 -1"); out.newLine(); } out.write("DONE."); out.newLine(); } catch (Exception e) { android.util.Log.w("View", "Problem profiling the view:", e); } finally { if (out != null) { out.close(); } } } private static void profileViewAndChildren(final View view, BufferedWriter out) throws IOException { profileViewAndChildren(view, out, true); } private static void profileViewAndChildren(final View view, BufferedWriter out, boolean root) throws IOException { long durationMeasure = (root || (view.mPrivateFlags & View.MEASURED_DIMENSION_SET) != 0) ? profileViewOperation( view, new ViewOperation() { public Void[] pre() { forceLayout(view); return null; } private void forceLayout(View view) { view.forceLayout(); if (view instanceof ViewGroup) { ViewGroup group = (ViewGroup) view; final int count = group.getChildCount(); for (int i = 0; i < count; i++) { forceLayout(group.getChildAt(i)); } } } public void run(Void... data) { view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec); } public void post(Void... data) { } }) : 0; long durationLayout = (root || (view.mPrivateFlags & View.LAYOUT_REQUIRED) != 0) ? profileViewOperation( view, new ViewOperation () { public Void[] pre() { return null; } public void run(Void... data) { view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom); } public void post(Void... data) { } }) : 0; long durationDraw = (root || !view.willNotDraw() || (view.mPrivateFlags & View.DRAWN) != 0) ? profileViewOperation( view, new ViewOperation