/* * 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.ParcelFileDescriptor; import android.os.RemoteException; import android.os.SystemClock; 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.FileDescriptor; 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.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; 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 getViewRootImplCount() {
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.
*
* @hide
*/
public static void startLooperProfiling(String path, FileDescriptor fileDescriptor) {
if (sLooperProfilerStorage.get() == null) {
LooperProfiler profiler = new LooperProfiler(path, fileDescriptor);
sLooperProfilerStorage.set(profiler);
Looper.myLooper().setMessageLogging(profiler);
}
}
/**
* Stops profiling the looper associated with the current thread.
*
* @see #startLooperProfiling(String, java.io.FileDescriptor)
*
* @hide
*/
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 String LOG_TAG = "LooperProfiler";
private static final int TRACE_VERSION_NUMBER = 3;
private static final int ACTION_EXIT_METHOD = 0x1;
private static final int HEADER_SIZE = 32;
private static final String HEADER_MAGIC = "SLOW";
private static final short HEADER_RECORD_SIZE = (short) 14;
private final long mTraceWallStart;
private final long mTraceThreadStart;
private final ArrayList mTraces = new ArrayList(512);
private final HashMap mTraceNames = new HashMap(32);
private int mTraceId = 0;
private final String mPath;
private ParcelFileDescriptor mFileDescriptor;
LooperProfiler(String path, FileDescriptor fileDescriptor) {
mPath = path;
try {
mFileDescriptor = ParcelFileDescriptor.dup(fileDescriptor);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not write trace file " + mPath, e);
throw new RuntimeException(e);
}
mTraceWallStart = SystemClock.currentTimeMicro();
mTraceThreadStart = SystemClock.currentThreadTimeMicro();
}
@Override
public void println(String x) {
// Ignore messages
}
@Override
public void profile(Message message, long wallStart, long wallTime,
long threadStart, long threadTime) {
Entry entry = new Entry();
entry.traceId = getTraceId(message);
entry.wallStart = wallStart;
entry.wallTime = wallTime;
entry.threadStart = threadStart;
entry.threadTime = threadTime;
mTraces.add(entry);
}
private int getTraceId(Message message) {
String name = message.getTarget().getMessageName(message);
Integer traceId = mTraceNames.get(name);
if (traceId == null) {
traceId = mTraceId++ << 4;
mTraceNames.put(name, traceId);
}
return traceId;
}
void save() {
// Don't block the UI thread
new Thread(new Runnable() {
@Override
public void run() {
saveTraces();
}
}, "LooperProfiler[" + mPath + "]").start();
}
private void saveTraces() {
FileOutputStream fos = new FileOutputStream(mFileDescriptor.getFileDescriptor());
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fos));
try {
writeHeader(out, mTraceWallStart, mTraceNames, mTraces);
out.flush();
writeTraces(fos, out.size(), mTraceWallStart, mTraceThreadStart, mTraces);
Log.d(LOG_TAG, "Looper traces ready: " + mPath);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not write trace file " + mPath, e);
} finally {
try {
out.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Could not write trace file " + mPath, e);
}
try {
mFileDescriptor.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Could not write trace file " + mPath, e);
}
}
}
private static void writeTraces(FileOutputStream out, long offset, long wallStart,
long threadStart, ArrayList entries) throws IOException {
FileChannel channel = out.getChannel();
// Header
ByteBuffer buffer = ByteBuffer.allocateDirect(HEADER_SIZE);
buffer.put(HEADER_MAGIC.getBytes());
buffer = buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort((short) TRACE_VERSION_NUMBER); // version
buffer.putShort((short) HEADER_SIZE); // offset to data
buffer.putLong(wallStart); // start time in usec
buffer.putShort(HEADER_RECORD_SIZE); // size of a record in bytes
// padding to 32 bytes
for (int i = 0; i < HEADER_SIZE - 18; i++) {
buffer.put((byte) 0);
}
buffer.flip();
channel.position(offset).write(buffer);
buffer = ByteBuffer.allocateDirect(14).order(ByteOrder.LITTLE_ENDIAN);
for (Entry entry : entries) {
buffer.putShort((short) 1); // we simulate only one thread
buffer.putInt(entry.traceId); // entering method
buffer.putInt((int) (entry.threadStart - threadStart));
buffer.putInt((int) (entry.wallStart - wallStart));
buffer.flip();
channel.write(buffer);
buffer.clear();
buffer.putShort((short) 1);
buffer.putInt(entry.traceId | ACTION_EXIT_METHOD); // exiting method
buffer.putInt((int) (entry.threadStart + entry.threadTime - threadStart));
buffer.putInt((int) (entry.wallStart + entry.wallTime - wallStart));
buffer.flip();
channel.write(buffer);
buffer.clear();
}
channel.close();
}
private static void writeHeader(DataOutputStream out, long start,
HashMap names, ArrayList entries) throws IOException {
Entry last = entries.get(entries.size() - 1);
long wallTotal = (last.wallStart + last.wallTime) - start;
startSection("version", out);
addValue(null, Integer.toString(TRACE_VERSION_NUMBER), out);
addValue("data-file-overflow", "false", out);
addValue("clock", "dual", out);
addValue("elapsed-time-usec", Long.toString(wallTotal), out);
addValue("num-method-calls", Integer.toString(entries.size()), out);
addValue("clock-call-overhead-nsec", "1", out);
addValue("vm", "dalvik", out);
startSection("threads", out);
addThreadId(1, "main", out);
startSection("methods", out);
addMethods(names, out);
startSection("end", out);
}
private static void addMethods(HashMap names, DataOutputStream out)
throws IOException {
for (Map.Entry name : names.entrySet()) {
out.writeBytes(String.format("0x%08x\tEventQueue\t%s\t()V\tLooper\t-2\n",
name.getValue(), name.getKey()));
}
}
private static void addThreadId(int id, String name, DataOutputStream out)
throws IOException {
out.writeBytes(Integer.toString(id) + '\t' + name + '\n');
}
private static void addValue(String name, String value, DataOutputStream out)
throws IOException {
if (name != null) {
out.writeBytes(name + "=");
}
out.writeBytes(value + '\n');
}
private static void startSection(String name, DataOutputStream out) throws IOException {
out.writeBytes("*" + name + '\n');
}
static class Entry {
int traceId;
long wallStart;
long wallTime;
long threadStart;
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 a IllegalStateException 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 a IllegalStateException 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