AnimatorTracer:Android 原生动画 Perfetto Trace + 详细 Log 集成文档
日期: 2026-04-29(更新: 2026-05-13)
作者: Claude Code
状态: 已实现
关联: FolmeTracer(Folme 动画追踪,已在 folme-4.0.0-alpha09.aar 中集成)
一、背景与动机
1.1 问题
MiuiSystemUI 中同时使用多套动画体系:
| 动画体系 | 使用规模 | 追踪能力 |
|---|---|---|
| Folme (miuix.animation) | 149 个文件、776 条 import | FolmeTracer 已覆盖(alpha09 新增) |
| Android 原生 (ObjectAnimator/ValueAnimator) | 165 个文件、417 处调用 | AnimatorTracer 已覆盖 |
| DynamicAnimation (SpringAnimation/FlingAnimation) | 43+ 文件 | DynamicAnimationTracer 已覆盖 |
框架层 ValueAnimator 原有 Trace.asyncTraceBegin/End,但 section 名称极其简陋:
ValueAnimator → "animator"
ObjectAnimator → "animator:alpha"
无法提供:调用堆栈、属性起止值、interpolator 详情、目标对象信息。在 Perfetto 中看到一堆 "animator" slice 时完全无法区分是哪个动画。
1.2 目标
在 AOSP 框架层插桩,添加与 FolmeTracer 风格统一的追踪能力:
- Perfetto Trace: 丰富的异步 section 名称
"Anim|42|Obj|Button@3f2c|alpha:1.0->0.0|AccelDecel|300ms"(包含 ID,方便与 Logcat 对应) - Logcat Log: 结构化框线格式,包含完整调用堆栈,一步定位触发动画的 SystemUI 代码行
- 零侵入: 通过 system property 控制,关闭时接近零开销
- 全覆盖: 自动追踪 ValueAnimator、ObjectAnimator、AnimatorSet 子动画、ViewPropertyAnimator、SpringAnimation、FlingAnimation
- 全链路 ID: start → init → end 通过同一个
mTraceId串联 - 结束原因追踪: 区分
cancel/force_end/complete+ 结束堆栈
二、架构总览
2.1 四个 Tracer 的分工
┌─────────────────────────────────────────────────────────────────────┐
│ System Property: debug.animator.trace │
│ (控制 AnimatorTracer + DynamicAnimationTracer) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────────────────────────┐ │
│ │ AnimatorTracer │ │ DynamicAnimationTracer │ │
│ │ (android.animation)│ │ (c.a.i.dynamicanimation.animation) │ │
│ ├──────────────────┤ ├──────────────────────────────────────┤ │
│ │ ValueAnimator │ │ SpringAnimation │ │
│ │ ObjectAnimator │ │ FlingAnimation │ │
│ │ AnimatorSet (子) │ │ DynamicAnimation │ │
│ │ ViewPropertyAnim │ │ │ │
│ └──────────────────┘ └──────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ System Property: debug.view.prop.trace │
│ (独立控制 ViewPropertyTracer) │
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ViewPropertyTracer (android.view) │ │
│ ├──────────────────────────────────────────────────────────────┤ │
│ │ View.setAlpha / setTranslation / setScale / setRotation ... │ │
│ │ → Perfetto Counter Track (折线图) + Logcat 逐帧属性值 │ │
│ │ → 仅在 AnimationHandler 动画帧期间生效 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ System Property: debug.folme.trace │
│ (独立控制 FolmeTracer) │
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────────────┐ │
│ │ FolmeTracer │ │
│ │ (miuix.animation)│ │
│ ├──────────────────┤ │
│ │ Folme 状态动画 │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
2.2 共享设计
-
AnimatorTracer 作为核心模块,提供公共方法供 DynamicAnimationTracer 使用:
AnimatorTracer.init()— 幂等初始化,读取 system propertyAnimatorTracer.isEnabled()— 检查是否启用AnimatorTracer.nextTraceId()— 全局递增 ID(跨两种动画共享 ID 空间)AnimatorTracer.captureCallStack(String... extraFilterPrefixes)— 堆栈捕获,支持自定义过滤前缀AnimatorTracer.formatValue(Object)— 值格式化(整数颜色 → hex)AnimatorTracer.formatFloat(float)— 浮点格式化(整数值省小数点)
2.3 进程过滤
框架层修改影响所有 app。通过 system property 的值实现精确过滤:
"0" / "" / "false" → 关闭(默认)
"1" / "true" → 所有进程
"com.android.systemui" → 仅 SystemUI
"com.android.systemui,com.miui.home" → 多个进程
三个 property 使用相同的格式和过滤逻辑:
debug.animator.trace— AnimatorTracer + DynamicAnimationTracerdebug.view.prop.trace— ViewPropertyTracerdebug.folme.trace— FolmeTracer
使用 ActivityThread.currentProcessName() 获取当前进程名,在各自 init() 中一次性比对。
三、AnimatorTracer 设计
3.1 Hook 点选择
Android 原生动画的核心生命周期:
用户调用:
ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).start()
│
▼
ValueAnimator.start() ← 公开方法,在调用方线程
│ 【Hook A: init + 捕获调用堆栈】
├── start(boolean playBackwards) ← 私有方法
│ ├── addAnimationCallback() ← 注册到 Choreographer
│ └── startAnimation() ← 无 startDelay 时直接调用
│
▼ (Choreographer 帧回调)
ValueAnimator.startAnimation() ← 动画实际开始
├── initAnimation() ← PropertyValuesHolder 初始化
│ └── AnimatorTracer.onAnimationInit() 【Hook B: Init 标记】
│
├── AnimatorTracer.onAnimationStart() 【Hook C: Trace begin + 详细 Log】
│ (此时 mValues[] 已填充,startValue/endValue 可用)
├── mRunning = true
└── notifyStartListeners()
│
▼ (每帧执行)
doAnimationFrame() → animateValue()
│
▼
ValueAnimator.endAnimation() ← 动画结束
├── notifyEndListeners()
├── AnimatorTracer.onAnimationEnd() 【Hook D: Trace end + 结构化 Log】
└── Trace.asyncTraceEnd() (原有)
3.2 关键设计决策
| 设计决策 | 选择 | 原因 |
|---|---|---|
堆栈在 start() 捕获 | 此时仍运行在调用方线程 | 到 startAnimation() 时堆栈已被 Choreographer 帧调度替换 |
Trace + Log 在 initAnimation() 之后输出 | 确保 mValues 已初始化 | 否则 mKeyframes.getValue() 返回 null |
| 堆栈通过字段桥接 | mTraceCallStack 字段 | start() 中写入,startAnimation() 中读取后置 null |
endAsyncSection 使用缓存的 sectionName | Perfetto 要求 begin/end name 完全相同 | 动画运行期间 target 状态可能变化 |
| AtomicInteger 生成 ID | 即使多线程也保证唯一 | 与 DynamicAnimationTracer 共享 ID 空间 |
| ID 放入 section name | "Anim|42|Obj|..." | Perfetto UI 直接可见 ID,无需对照时间戳去 logcat 找日志 |
3.3 endAnimation 的 3 种调用来源
// 来源 1: cancel() — 外部主动取消
public void cancel() { // L1175
...
mTraceEndReason = "cancel"; // ← 设置原因
mTraceCallStack = captureCallStack(); // ← 捕获结束堆栈
endAnimation(); // → reason = "cancel"
}
// 来源 2: end() — 外部强制结束(跳到最终值)
public void end() { // L1204
...
mTraceEndReason = "force_end"; // ← 设置原因
mTraceCallStack = captureCallStack(); // ← 捕获结束堆栈
endAnimation(); // → reason = "force_end"
}
// 来源 3: doAnimationFrame() — 自然播放完毕
boolean doAnimationFrame(long frameTime) { // L1508
...
if (finished) {
endAnimation(true /* fromLastFrame */); // → reason = "complete" (mTraceEndReason 为 null)
}
}| 来源 | mTraceEndReason | 需要结束堆栈? | 原因 |
|---|---|---|---|
cancel() | "cancel" | 是 | 需要知道谁 cancel 了它 |
end() | "force_end" | 是 | 需要知道谁 end 了它 |
doAnimationFrame() | null (→ “complete”) | 否 | 堆栈是 Choreographer 帧调度,无意义 |
3.4 数据流总览
┌─────────────────────────────────────────────────────────────────────┐
│ 完整生命周期数据流 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ start(playBackwards) │
│ ├── AnimatorTracer.init() [幂等初始化] │
│ ├── mTraceCallStack = captureCallStack() [捕获 start 堆栈] │
│ ├── addAnimationCallback() │
│ └── startAnimation() [无 delay 时直接调用] │
│ ├── initAnimation() │
│ │ └── AnimatorTracer.onAnimationInit(this) │
│ │ 输出: "Anim Init | ID: 42 | alpha" │
│ ├── AnimatorTracer.onAnimationStart(this, mTraceCallStack) │
│ │ 输出: ┌── Begin 完整日志 (ID + 属性 + start堆栈) │
│ └── mTraceCallStack = null [释放引用] │
│ │
│ ─── 动画执行中 (Choreographer 帧驱动) ─── │
│ │
│ 结束路径 A: cancel() │
│ ├── mTraceEndReason = "cancel" │
│ ├── mTraceCallStack = captureCallStack() [捕获 cancel 堆栈] │
│ └── endAnimation(false) │
│ ├── AnimatorTracer.onAnimationEnd(this) │
│ │ 输出: ┌── End 完整日志 (ID + reason=cancel + 堆栈) │
│ ├── mTraceCallStack = null │
│ └── mTraceEndReason = null │
│ │
│ 结束路径 B: end() │
│ ├── mTraceEndReason = "force_end" │
│ ├── mTraceCallStack = captureCallStack() [捕获 end 堆栈] │
│ └── endAnimation(false) │
│ ├── AnimatorTracer.onAnimationEnd(this) │
│ │ 输出: ┌── End 完整日志 (ID + reason=force_end + 堆栈) │
│ ├── mTraceCallStack = null │
│ └── mTraceEndReason = null │
│ │
│ 结束路径 C: doAnimationFrame() → finished │
│ └── endAnimation(true) │
│ ├── AnimatorTracer.onAnimationEnd(this) │
│ │ 输出: ┌── End 日志 (ID + reason=complete, 无堆栈) │
│ └── mTraceEndReason 已为 null │
│ │
└─────────────────────────────────────────────────────────────────────┘
四、DynamicAnimationTracer 设计
4.1 背景
DynamicAnimation(SpringAnimation / FlingAnimation)不继承 ValueAnimator,有独立的帧驱动管线(直接注册 AnimationHandler.AnimationFrameCallback),需要独立的 Tracer。
4.2 Hook 点
用户调用:
SpringAnimation(view, TRANSLATION_X).setSpring(spring).start()
│
▼
DynamicAnimation.start() ← 公开方法
├── AnimatorTracer.init() [共享初始化]
├── mTraceCallStack = AnimatorTracer.captureCallStack(
│ "com.android.internal.dynamicanimation.") [捕获堆栈,过滤内部帧]
└── startAnimationInternal()
├── mRunning = true
├── mValue = getPropertyValue() [获取当前属性值]
├── DynamicAnimationTracer.onAnimationStart(this, mTraceCallStack)
│ 输出: Trace begin + 详细 Log
├── mTraceCallStack = null
└── addAnimationFrameCallback()
│
▼ (每帧执行)
doAnimationFrame() → updateValueAndVelocity()
│
▼
endAnimationInternal(canceled) ← 动画结束
├── mRunning = false
├── DynamicAnimationTracer.onAnimationEnd(this, canceled)
│ 输出: Trace end + 结构化 End Log
└── notifyEndListeners()
4.3 Section Name 格式
| 动画类型 | 格式 | 示例 |
|---|---|---|
| SpringAnimation | Spring|{id}|{target}|{prop}|s={stiffness},d={damping}|{start}->{end} | Spring|105|NotifRow@3f2c|translationY|s=200,d=0.8|0->-100 |
| FlingAnimation | Fling|{id}|{target}|{prop}|f={friction}|v0={velocity} | Fling|106|ListView@ab12|translationX|f=1.5|v0=2500 |
4.4 与 AnimatorTracer 的共享
| 功能 | 来源 |
|---|---|
init() / isEnabled() | AnimatorTracer(共享 system property) |
nextTraceId() | AnimatorTracer(共享 ID 空间,避免冲突) |
captureCallStack(extraFilter) | AnimatorTracer(额外过滤 com.android.internal.dynamicanimation. 前缀) |
formatFloat() / formatValue() | AnimatorTracer(公共工具方法) |
五、源码实现
5.1 文件总览
| 文件 | 路径 | 操作 | 行数 |
|---|---|---|---|
AnimatorTracer.java | core/java/android/animation/ | 新增 | 327 行 |
ValueAnimator.java | core/java/android/animation/ | 修改 | +4 字段 +14 行插桩 |
DynamicAnimationTracer.java | core/java/com/android/internal/dynamicanimation/animation/ | 新增 | 162 行 |
DynamicAnimation.java | core/java/com/android/internal/dynamicanimation/animation/ | 修改 | +3 字段 +8 行插桩 |
5.2 AnimatorTracer.java 完整源码
package android.animation;
import android.app.ActivityThread;
import android.os.SystemProperties;
import android.os.Trace;
import android.util.Log;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Traces Android native animator (ValueAnimator/ObjectAnimator) lifecycle
* for Perfetto async sections and Logcat diagnostics.
*
* Controlled by system property {@code debug.animator.trace}:
* <ul>
* <li>{@code "0"} or unset: disabled (default)</li>
* <li>{@code "1"} or {@code "true"}: trace all processes</li>
* <li>{@code "com.android.systemui"}: trace only the named process</li>
* <li>{@code "com.android.systemui,com.miui.home"}: comma-separated process list</li>
* </ul>
*
* @hide
*/
public final class AnimatorTracer {
private static final String TAG = "AnimatorTracer";
private static final String PROP_NAME = "debug.animator.trace";
private static final int MAX_STACK_DEPTH = 15;
private static final int MAX_SECTION_LEN = 200;
private static volatile boolean sInited;
private static volatile boolean sEnabled;
private static final AtomicInteger sIdGenerator = new AtomicInteger();
private AnimatorTracer() {}
public static void init() {
if (sInited) return;
synchronized (AnimatorTracer.class) {
if (sInited) return;
sInited = true;
try {
String val = SystemProperties.get(PROP_NAME, "");
if (val.isEmpty() || "0".equals(val) || "false".equalsIgnoreCase(val)) {
sEnabled = false;
return;
}
if ("1".equals(val) || "true".equalsIgnoreCase(val)) {
sEnabled = true;
} else {
String proc = currentProcessName();
sEnabled = proc != null && val.contains(proc);
}
} catch (Exception e) {
sEnabled = false;
}
if (sEnabled) {
Log.i(TAG, "AnimatorTracer enabled for process: " + currentProcessName());
}
}
}
public static boolean isEnabled() {
return sEnabled;
}
/** @hide */
public static int nextTraceId() {
return sIdGenerator.incrementAndGet();
}
public static String captureCallStack() {
return captureCallStack(new String[0]);
}
/** @hide */
public static String captureCallStack(String... extraFilterPrefixes) {
if (!sEnabled) return null;
StackTraceElement[] traces = Thread.currentThread().getStackTrace();
StringBuilder sb = new StringBuilder(512);
outer:
for (int i = 3; i < Math.min(traces.length, 3 + MAX_STACK_DEPTH); i++) {
String cls = traces[i].getClassName();
if (cls.startsWith("android.animation.") || cls.startsWith("java.lang.reflect.")) {
continue;
}
for (String prefix : extraFilterPrefixes) {
if (cls.startsWith(prefix)) continue outer;
}
sb.append("│ at ").append(traces[i]).append('\n');
}
return sb.toString();
}
// ─── 生命周期: Init ───────────────────────────────────────────────
public static void onAnimationInit(ValueAnimator animator) {
if (!sEnabled || animator.mTraceId == 0) return;
StringBuilder sb = new StringBuilder(64);
sb.append("Anim Init | ID: ").append(animator.mTraceId);
if (animator instanceof ObjectAnimator) {
String propName = ((ObjectAnimator) animator).getPropertyName();
if (propName != null) {
sb.append(" | ").append(propName);
}
}
Log.i(TAG, sb.toString());
}
// ─── 生命周期: Start ──────────────────────────────────────────────
public static void onAnimationStart(ValueAnimator animator, String callStack) {
if (!sEnabled) return;
int traceId = sIdGenerator.incrementAndGet();
animator.mTraceId = traceId;
String sectionName = buildSectionName(animator, traceId);
animator.mTraceSectionName = sectionName;
Trace.beginAsyncSection(sectionName, traceId);
logBegin(animator, traceId, callStack);
}
// ─── 生命周期: End ────────────────────────────────────────────────
public static void onAnimationEnd(ValueAnimator animator) {
if (animator.mTraceSectionName == null) return;
Trace.endAsyncSection(animator.mTraceSectionName, animator.mTraceId);
String reason = animator.mTraceEndReason != null ? animator.mTraceEndReason : "complete";
String callStack = animator.mTraceCallStack;
logEnd(animator, reason, callStack);
animator.mTraceSectionName = null;
animator.mTraceEndReason = null;
animator.mTraceCallStack = null;
}
// ─── Section Name 构建 ────────────────────────────────────────────
private static String buildSectionName(ValueAnimator animator, int traceId) {
StringBuilder sb = new StringBuilder(MAX_SECTION_LEN);
sb.append("Anim|").append(traceId).append('|');
if (animator instanceof ObjectAnimator) {
sb.append("Obj|");
Object target = ((ObjectAnimator) animator).getTarget();
if (target != null) {
sb.append(target.getClass().getSimpleName());
sb.append('@');
sb.append(Integer.toHexString(System.identityHashCode(target)));
} else {
sb.append("null");
}
} else {
sb.append("Val");
}
sb.append('|');
PropertyValuesHolder[] values = animator.mValues;
if (values != null) {
int limit = Math.min(values.length, 3);
for (int i = 0; i < limit; i++) {
if (i > 0) sb.append(',');
String name = values[i].mPropertyName;
sb.append(name != null ? name : "?");
try {
Object startVal = values[i].mKeyframes.getValue(0f);
Object endVal = values[i].mKeyframes.getValue(1f);
if (startVal != null && endVal != null) {
sb.append(':');
sb.append(formatValue(startVal));
sb.append("->");
sb.append(formatValue(endVal));
}
} catch (Exception ignored) {
}
}
if (values.length > 3) {
sb.append(",+").append(values.length - 3);
}
}
sb.append('|');
sb.append(describeInterpolator(animator));
sb.append('|');
sb.append(animator.getDuration()).append("ms");
if (sb.length() > MAX_SECTION_LEN) {
sb.setLength(MAX_SECTION_LEN - 3);
sb.append("...");
}
return sb.toString();
}
// ─── Log 输出 ─────────────────────────────────────────────────────
private static void logBegin(ValueAnimator animator, int traceId, String callStack) {
StringBuilder sb = new StringBuilder(512);
sb.append("┌── Native Animator Begin ─────────────────────────────\n");
sb.append("│ ID: ").append(traceId).append('\n');
if (animator instanceof ObjectAnimator) {
ObjectAnimator oa = (ObjectAnimator) animator;
sb.append("│ Type: ObjectAnimator\n");
Object target = oa.getTarget();
if (target != null) {
sb.append("│ Target: ").append(target.getClass().getName());
sb.append('@').append(Integer.toHexString(System.identityHashCode(target)));
sb.append('\n');
}
} else {
sb.append("│ Type: ValueAnimator\n");
}
sb.append("│ Duration: ").append(animator.getDuration()).append("ms");
int repeatCount = animator.getRepeatCount();
if (repeatCount != 0) {
sb.append(" (repeat: ");
sb.append(repeatCount == ValueAnimator.INFINITE ? "INFINITE" : repeatCount);
sb.append(')');
}
sb.append('\n');
long startDelay = animator.getStartDelay();
if (startDelay > 0) {
sb.append("│ StartDelay: ").append(startDelay).append("ms\n");
}
PropertyValuesHolder[] values = animator.mValues;
if (values != null && values.length > 0) {
sb.append("│ Properties:\n");
for (PropertyValuesHolder pvh : values) {
String name = pvh.mPropertyName;
sb.append("│ ").append(name != null ? name : "?");
try {
Object startVal = pvh.mKeyframes.getValue(0f);
Object endVal = pvh.mKeyframes.getValue(1f);
if (startVal != null && endVal != null) {
sb.append(": ").append(formatValue(startVal));
sb.append(" → ").append(formatValue(endVal));
}
} catch (Exception ignored) {
}
sb.append('\n');
}
}
sb.append("│ Interpolator: ").append(describeInterpolator(animator)).append('\n');
if (callStack != null && !callStack.isEmpty()) {
sb.append("│ CallStack:\n");
sb.append(callStack);
}
sb.append("└───────────────────────────────────────────────────────");
Log.i(TAG, sb.toString());
}
private static void logEnd(ValueAnimator animator, String reason, String callStack) {
StringBuilder sb = new StringBuilder(512);
sb.append("┌── Native Animator End ───────────────────────────────\n");
sb.append("│ ID: ").append(animator.mTraceId).append('\n');
sb.append("│ Reason: ").append(reason).append('\n');
if (animator instanceof ObjectAnimator) {
ObjectAnimator oa = (ObjectAnimator) animator;
Object target = oa.getTarget();
if (target != null) {
sb.append("│ Target: ").append(target.getClass().getName());
sb.append('@').append(Integer.toHexString(System.identityHashCode(target)));
sb.append('\n');
}
String propName = oa.getPropertyName();
if (propName != null) {
sb.append("│ Property: ").append(propName).append('\n');
}
}
sb.append("│ Duration: ").append(animator.getDuration()).append("ms\n");
if (callStack != null && !callStack.isEmpty()) {
sb.append("│ EndCallStack:\n");
sb.append(callStack);
}
sb.append("└───────────────────────────────────────────────────────");
Log.i(TAG, sb.toString());
}
// ─── 工具方法 ─────────────────────────────────────────────────────
private static String describeInterpolator(ValueAnimator animator) {
Object interp = animator.getInterpolator();
if (interp == null) return "null";
String name = interp.getClass().getSimpleName();
if (name.endsWith("Interpolator")) {
name = name.substring(0, name.length() - "Interpolator".length());
}
if (name.isEmpty()) {
name = interp.getClass().getName();
}
return name;
}
/** @hide */
public static String formatValue(Object value) {
if (value instanceof Float) {
float f = (Float) value;
return f == (long) f ? String.valueOf((long) f) : String.valueOf(f);
}
if (value instanceof Integer) {
int i = (Integer) value;
if ((i & 0xFF000000) != 0) {
return "0x" + Integer.toHexString(i);
}
return String.valueOf(i);
}
return String.valueOf(value);
}
/** @hide */
public static String formatFloat(float f) {
return f == (long) f ? String.valueOf((long) f) : String.valueOf(f);
}
private static String currentProcessName() {
try {
return ActivityThread.currentProcessName();
} catch (Exception e) {
return null;
}
}
}5.3 DynamicAnimationTracer.java 完整源码
package com.android.internal.dynamicanimation.animation;
import android.animation.AnimatorTracer;
import android.os.Trace;
import android.util.Log;
/**
* Traces DynamicAnimation (SpringAnimation/FlingAnimation) lifecycle
* for Perfetto async sections and Logcat diagnostics.
*
* Shares the same system property control ({@code debug.animator.trace})
* and ID space with {@link AnimatorTracer}.
*
* @hide
*/
public final class DynamicAnimationTracer {
private static final String TAG = "DynAnimTracer";
private static final int MAX_SECTION_LEN = 200;
private static final String FILTER_PREFIX = "com.android.internal.dynamicanimation.";
private DynamicAnimationTracer() {}
public static void onAnimationStart(DynamicAnimation<?> anim, String callStack) {
if (!AnimatorTracer.isEnabled()) return;
int traceId = AnimatorTracer.nextTraceId();
anim.mTraceId = traceId;
String sectionName = buildSectionName(anim, traceId);
anim.mTraceSectionName = sectionName;
Trace.beginAsyncSection(sectionName, traceId);
logBegin(anim, traceId, callStack);
}
public static void onAnimationEnd(DynamicAnimation<?> anim, boolean canceled) {
if (anim.mTraceSectionName == null) return;
Trace.endAsyncSection(anim.mTraceSectionName, anim.mTraceId);
logEnd(anim, canceled);
anim.mTraceSectionName = null;
}
private static String buildSectionName(DynamicAnimation<?> anim, int traceId) {
StringBuilder sb = new StringBuilder(MAX_SECTION_LEN);
if (anim instanceof SpringAnimation) {
sb.append("Spring|");
} else if (anim instanceof FlingAnimation) {
sb.append("Fling|");
} else {
sb.append("Dyn|");
}
sb.append(traceId).append('|');
sb.append(describeTarget(anim)).append('|');
sb.append(getPropertyName(anim)).append('|');
if (anim instanceof SpringAnimation) {
SpringForce spring = ((SpringAnimation) anim).getSpring();
if (spring != null) {
sb.append("s=").append(AnimatorTracer.formatFloat(spring.getStiffness()));
sb.append(",d=").append(AnimatorTracer.formatFloat(spring.getDampingRatio()));
sb.append('|');
sb.append(AnimatorTracer.formatFloat(anim.mValue));
sb.append("->");
sb.append(AnimatorTracer.formatFloat(spring.getFinalPosition()));
}
} else if (anim instanceof FlingAnimation) {
sb.append("f=").append(AnimatorTracer.formatFloat(((FlingAnimation) anim).getFriction()));
sb.append('|');
sb.append("v0=").append(AnimatorTracer.formatFloat(anim.mVelocity));
}
if (sb.length() > MAX_SECTION_LEN) {
sb.setLength(MAX_SECTION_LEN - 3);
sb.append("...");
}
return sb.toString();
}
private static void logBegin(DynamicAnimation<?> anim, int traceId, String callStack) {
StringBuilder sb = new StringBuilder(512);
if (anim instanceof SpringAnimation) {
sb.append("┌── Spring Animation Begin ────────────────────────────\n");
} else if (anim instanceof FlingAnimation) {
sb.append("┌── Fling Animation Begin ─────────────────────────────\n");
} else {
sb.append("┌── Dynamic Animation Begin ───────────────────────────\n");
}
sb.append("│ ID: ").append(traceId).append('\n');
if (anim.mTarget != null) {
sb.append("│ Target: ").append(anim.mTarget.getClass().getName());
sb.append('@').append(Integer.toHexString(System.identityHashCode(anim.mTarget)));
sb.append('\n');
}
sb.append("│ Property: ").append(getPropertyName(anim)).append('\n');
sb.append("│ StartValue: ").append(AnimatorTracer.formatFloat(anim.mValue)).append('\n');
if (anim instanceof SpringAnimation) {
SpringForce spring = ((SpringAnimation) anim).getSpring();
if (spring != null) {
sb.append("│ FinalPosition: ").append(AnimatorTracer.formatFloat(spring.getFinalPosition())).append('\n');
sb.append("│ Stiffness: ").append(AnimatorTracer.formatFloat(spring.getStiffness())).append('\n');
sb.append("│ DampingRatio: ").append(AnimatorTracer.formatFloat(spring.getDampingRatio())).append('\n');
}
} else if (anim instanceof FlingAnimation) {
sb.append("│ Friction: ").append(AnimatorTracer.formatFloat(((FlingAnimation) anim).getFriction())).append('\n');
}
if (anim.mVelocity != 0) {
sb.append("│ StartVelocity: ").append(AnimatorTracer.formatFloat(anim.mVelocity)).append('\n');
}
if (callStack != null && !callStack.isEmpty()) {
sb.append("│ CallStack:\n");
sb.append(callStack);
}
sb.append("└───────────────────────────────────────────────────────");
Log.i(TAG, sb.toString());
}
private static void logEnd(DynamicAnimation<?> anim, boolean canceled) {
StringBuilder sb = new StringBuilder(256);
if (anim instanceof SpringAnimation) {
sb.append("┌── Spring Animation End ──────────────────────────────\n");
} else if (anim instanceof FlingAnimation) {
sb.append("┌── Fling Animation End ───────────────────────────────\n");
} else {
sb.append("┌── Dynamic Animation End ─────────────────────────────\n");
}
sb.append("│ ID: ").append(anim.mTraceId).append('\n');
sb.append("│ Reason: ").append(canceled ? "cancel" : "complete").append('\n');
sb.append("│ Property: ").append(getPropertyName(anim)).append('\n');
sb.append("│ FinalValue: ").append(AnimatorTracer.formatFloat(anim.mValue)).append('\n');
sb.append("│ FinalVelocity: ").append(AnimatorTracer.formatFloat(anim.mVelocity)).append('\n');
sb.append("└───────────────────────────────────────────────────────");
Log.i(TAG, sb.toString());
}
private static String describeTarget(DynamicAnimation<?> anim) {
if (anim.mTarget == null) {
return "FloatHolder";
}
return anim.mTarget.getClass().getSimpleName()
+ '@' + Integer.toHexString(System.identityHashCode(anim.mTarget));
}
private static String getPropertyName(DynamicAnimation<?> anim) {
if (anim.mProperty != null) {
String name = anim.mProperty.getName();
if (name != null && !name.isEmpty()) {
return name;
}
}
return "?";
}
}5.4 ValueAnimator.java 修改详情
Change A: 新增 4 个字段(Line 255-258)
定位: 搜索 HashMap<String, PropertyValuesHolder> mValuesMap,在其之后插入。
HashMap<String, PropertyValuesHolder> mValuesMap;
/** @hide */ int mTraceId;
/** @hide */ String mTraceSectionName;
/** @hide */ String mTraceCallStack;
/** @hide */ String mTraceEndReason;
/**
* If set to non-negative value, this will override {@link #sDurationScale}.
*/
private float mDurationScale = -1f;Change B: start(boolean) 中插桩(Line 1110-1112)
定位: if (Looper.myLooper() == null) throw 之后,mReversing = playBackwards 之前。
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
AnimatorTracer.init();
if (AnimatorTracer.isEnabled()) {
mTraceCallStack = AnimatorTracer.captureCallStack();
}
mReversing = playBackwards;
...Change C: initAnimation() 末尾插桩(Line 651)
定位: mInitialized = true; 之后。
@CallSuper
void initAnimation() {
if (!mInitialized) {
if (mValues != null) {
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].init();
}
}
mInitialized = true;
AnimatorTracer.onAnimationInit(this);
}
}Change D: cancel() 中插桩(Line 1197-1199)
定位: notifyListeners(AnimatorCaller.ON_CANCEL, false); 之后,endAnimation(); 之前。
notifyListeners(AnimatorCaller.ON_CANCEL, false);
}
mTraceEndReason = "cancel";
if (AnimatorTracer.isEnabled()) {
mTraceCallStack = AnimatorTracer.captureCallStack();
}
endAnimation();
}Change E: end() 中插桩(Line 1221-1223)
定位: consumePendingEndListeners return 之后,endAnimation(); 之前。
if (mAnimationEndRequested) {
consumePendingEndListeners(true /* notifyListeners */);
return;
}
mTraceEndReason = "force_end";
if (AnimatorTracer.isEnabled()) {
mTraceCallStack = AnimatorTracer.captureCallStack();
}
endAnimation();
}Change F: startAnimation() 中插桩(Line 1354-1356)
定位: initAnimation(); 之后,mRunning = true 之前。
private void startAnimation() {
...
mAnimationEndRequested = false;
initAnimation();
if (AnimatorTracer.isEnabled()) {
AnimatorTracer.onAnimationStart(this, mTraceCallStack);
mTraceCallStack = null;
}
mRunning = true;
...Change G: endAnimation(boolean) 中插桩(Line 1323)
定位: notifyEndListenersFromEndAnimation(...) 之后,原有 Trace.asyncTraceEnd 之前。
notifyEndListenersFromEndAnimation(mReversing, postNotifyEndListener);
AnimatorTracer.onAnimationEnd(this);
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
}5.5 DynamicAnimation.java 修改详情
Change A: 新增 3 个字段(Line 315-317)
定位: boolean mRunning = false; 之后。
boolean mRunning = false;
/** @hide */ int mTraceId;
/** @hide */ String mTraceSectionName;
/** @hide */ String mTraceCallStack;
float mMaxValue = Float.MAX_VALUE;Change B: start() 中插桩(Line 609-612)
定位: Looper 线程检查之后,if (!mRunning) 之前。
public void start() {
if (!isCurrentThread()) {
throw new AndroidRuntimeException(...);
}
AnimatorTracer.init();
if (AnimatorTracer.isEnabled()) {
mTraceCallStack = AnimatorTracer.captureCallStack(
"com.android.internal.dynamicanimation.");
}
if (!mRunning) {
startAnimationInternal();
}
}注意 captureCallStack 传入额外过滤前缀 "com.android.internal.dynamicanimation.",确保堆栈中不包含 DynamicAnimation 内部帧。
Change C: startAnimationInternal() 中插桩(Line 669-671)
定位: 获取 mValue 和越界检查之后,addAnimationFrameCallback 之前。
private void startAnimationInternal() {
if (!mRunning) {
mRunning = true;
if (!mStartValueIsSet) {
mValue = getPropertyValue();
}
// Sanity check...
if (AnimatorTracer.isEnabled()) {
DynamicAnimationTracer.onAnimationStart(this, mTraceCallStack);
mTraceCallStack = null;
}
getAnimationHandler().addAnimationFrameCallback(this, 0);
}
}Change D: endAnimationInternal() 中插桩(Line 727)
定位: removeCallback 之后,通知 listeners 之前。
private void endAnimationInternal(boolean canceled) {
mRunning = false;
getAnimationHandler().removeCallback(this);
mLastFrameTime = 0;
mStartValueIsSet = false;
DynamicAnimationTracer.onAnimationEnd(this, canceled);
for (int i = 0; i < mEndListeners.size(); i++) {
...
}
}六、输出格式详解
6.1 ValueAnimator/ObjectAnimator — 自然完成
AnimatorTracer: Anim Init | ID: 42 | alpha
AnimatorTracer: ┌── Native Animator Begin ─────────────────────────────
AnimatorTracer: │ ID: 42
AnimatorTracer: │ Type: ObjectAnimator
AnimatorTracer: │ Target: com.android.systemui.statusbar.NotificationRow@3f2c1a8
AnimatorTracer: │ Duration: 300ms
AnimatorTracer: │ Properties:
AnimatorTracer: │ alpha: 1 → 0
AnimatorTracer: │ Interpolator: AccelerateDecelerate
AnimatorTracer: │ CallStack:
AnimatorTracer: │ at com.android.systemui.statusbar.NotificationRow.animateRemoval(NotificationRow.java:432)
AnimatorTracer: │ at com.android.systemui.statusbar.NotificationStackScrollLayout.onChildRemoved(...)
AnimatorTracer: └───────────────────────────────────────────────────────
... (300ms 后) ...
AnimatorTracer: ┌── Native Animator End ───────────────────────────────
AnimatorTracer: │ ID: 42
AnimatorTracer: │ Reason: complete
AnimatorTracer: │ Target: com.android.systemui.statusbar.NotificationRow@3f2c1a8
AnimatorTracer: │ Property: alpha
AnimatorTracer: │ Duration: 300ms
AnimatorTracer: └───────────────────────────────────────────────────────
6.2 ValueAnimator/ObjectAnimator — 被 cancel(带结束堆栈)
AnimatorTracer: ┌── Native Animator End ───────────────────────────────
AnimatorTracer: │ ID: 58
AnimatorTracer: │ Reason: cancel
AnimatorTracer: │ Target: com.android.systemui.qs.QSTileView@ab120f3
AnimatorTracer: │ Property: scaleX
AnimatorTracer: │ Duration: 200ms
AnimatorTracer: │ EndCallStack:
AnimatorTracer: │ at com.android.systemui.qs.QSTileView.cancelAnimations(QSTileView.java:203)
AnimatorTracer: │ at com.android.systemui.qs.QSTileView.onStateChanged(QSTileView.java:89)
AnimatorTracer: └───────────────────────────────────────────────────────
6.3 SpringAnimation
DynAnimTracer: ┌── Spring Animation Begin ────────────────────────────
DynAnimTracer: │ ID: 105
DynAnimTracer: │ Target: com.android.systemui.statusbar.NotificationRow@3f2c1a8
DynAnimTracer: │ Property: translationY
DynAnimTracer: │ StartValue: 0
DynAnimTracer: │ FinalPosition: -100
DynAnimTracer: │ Stiffness: 200
DynAnimTracer: │ DampingRatio: 0.8
DynAnimTracer: │ StartVelocity: 500
DynAnimTracer: │ CallStack:
DynAnimTracer: │ at com.android.systemui.statusbar.NotificationRow.startSpring(...)
DynAnimTracer: └───────────────────────────────────────────────────────
... (弹簧收敛后) ...
DynAnimTracer: ┌── Spring Animation End ──────────────────────────────
DynAnimTracer: │ ID: 105
DynAnimTracer: │ Reason: complete
DynAnimTracer: │ Property: translationY
DynAnimTracer: │ FinalValue: -100
DynAnimTracer: │ FinalVelocity: 0
DynAnimTracer: └───────────────────────────────────────────────────────
6.4 FlingAnimation
DynAnimTracer: ┌── Fling Animation Begin ─────────────────────────────
DynAnimTracer: │ ID: 106
DynAnimTracer: │ Target: com.android.systemui.media.MediaScrollView@ab12f3
DynAnimTracer: │ Property: translationX
DynAnimTracer: │ StartValue: 0
DynAnimTracer: │ Friction: 1.5
DynAnimTracer: │ StartVelocity: 2500
DynAnimTracer: │ CallStack:
DynAnimTracer: │ at com.android.systemui.media.MediaScrollView.fling(...)
DynAnimTracer: └───────────────────────────────────────────────────────
DynAnimTracer: ┌── Fling Animation End ───────────────────────────────
DynAnimTracer: │ ID: 106
DynAnimTracer: │ Reason: complete
DynAnimTracer: │ Property: translationX
DynAnimTracer: │ FinalValue: 450.3
DynAnimTracer: │ FinalVelocity: 0
DynAnimTracer: └───────────────────────────────────────────────────────
6.5 Perfetto Section Name 格式汇总
| 动画类型 | Section 前缀 | 格式 | 示例 |
|---|---|---|---|
| ObjectAnimator | Anim| | Anim|{id}|Obj|{class}@{hash}|{props}|{interp}|{dur}ms | Anim|42|Obj|NotifRow@3f2c|alpha:1->0|AccelDecel|300ms |
| ValueAnimator | Anim| | Anim|{id}|Val|{props}|{interp}|{dur}ms | Anim|43|Val|?:0->1|Linear|200ms |
| SpringAnimation | Spring| | Spring|{id}|{target}|{prop}|s={s},d={d}|{from}->{to} | Spring|105|NotifRow@3f2c|translationY|s=200,d=0.8|0->-100 |
| FlingAnimation | Fling| | Fling|{id}|{target}|{prop}|f={f}|v0={v} | Fling|106|ScrollView@ab12|translationX|f=1.5|v0=2500 |
七、覆盖范围
| 动画类型 | 是否覆盖 | Tracer | 原理 |
|---|---|---|---|
ObjectAnimator.ofFloat(view, "alpha", 0, 1).start() | 是 | AnimatorTracer | ObjectAnimator 继承 ValueAnimator,start() 走父类 |
ValueAnimator.ofFloat(0, 1).start() | 是 | AnimatorTracer | 直接命中 hook 点 |
AnimatorSet 中的子动画 | 是 | AnimatorTracer | 子动画通过 node.mAnimation.start() 启动 |
view.animate().alpha(0).start() (ViewPropertyAnimator) | 是 | AnimatorTracer | 内部创建 ValueAnimator |
XML 动画 (AnimatorInflater.loadAnimator()) | 是 | AnimatorTracer | 加载后调用 start() 时命中 |
SpringAnimation | 是 | DynamicAnimationTracer | 独立 hook start() / endAnimationInternal() |
FlingAnimation | 是 | DynamicAnimationTracer | 同上 |
| Folme 动画 | 否(由 FolmeTracer 覆盖) | FolmeTracer | 走 miuix.animation 独立管线 |
| View 属性逐帧值 | 是 | ViewPropertyTracer | View setter 层面捕获,含自定义 ValueAnimator+Listener 模式 |
八、五个 Tracer 对比
| 维度 | AnimatorTracer | DynamicAnimationTracer | ViewPropertyTracer | FolmeTracer | ViewRefreshTracer |
|---|---|---|---|---|---|
| 追踪目标 | ValueAnimator/ObjectAnimator | SpringAnimation/FlingAnimation | View 属性逐帧值 | Folme 状态动画 | invalidate/requestLayout/scheduleVsync |
| Property 控制 | debug.animator.trace | 同左(共享) | debug.view.prop.trace | debug.folme.trace | debug.view.refresh.trace |
| Perfetto 输出 | Async Section | Async Section | Counter Track(折线图) | Async Section | 无(纯 Logcat) |
| Log Tag | AnimatorTracer | DynAnimTracer | ViewPropTrace | FolmeTracer | ViewRefresh |
| Log Level | Log.i | Log.i | Log.i | Log.i | Log.d |
| 输出频率 | start/end 时 | start/end 时 | 每帧 | start/end 时 | 每次 invalidate/requestLayout/scheduleVsync |
| End 原因 | cancel / force_end / complete | cancel / complete | N/A | cancel / complete / replaced | N/A |
| 堆栈 | start 时 + cancel/end 时 | start 时 | 无 | start 时 | 每次调用都有(new Exception()) |
| 实现位置 | AOSP framework | AOSP framework (internal) | AOSP framework (view) | folme AAR (miuix) | AOSP framework (view) |
| 性能影响 | 低 | 低 | 极低 | 低 | 中等(仅限调试) |
| ID 空间 | 全局 AtomicInteger | 同左(共享) | 无(按 View 实例区分) | 独立 TransitionInfo.id | 无 |
九、线程安全
ValueAnimator的start()/startAnimation()/endAnimation()均在 UI 线程执行DynamicAnimation的start()/cancel()/endAnimationInternal()同样限制在创建线程- 所有 trace 字段(
mTraceId、mTraceSectionName、mTraceCallStack、mTraceEndReason)仅单线程读写,无需同步 AnimatorTracer.init()使用 volatile + synchronized 双重检查(跨进程安全)sIdGenerator使用 AtomicInteger(虽然当前单线程,但跨模块共享更安全)
十、性能影响
| 场景 | 开销 |
|---|---|
| 关闭时(默认) | init(): 一次 volatile 读立即返回;isEnabled(): 一次 volatile 读为 false 跳过。JIT 内联后接近零 |
| 开启时 — 堆栈捕获 | Thread.getStackTrace() 约 0.1ms/次,仅在 start() / cancel() / end() 触发,非每帧 |
| 开启时 — initAnimation 标记 | 一次 StringBuilder + Log.i,微秒级 |
| 开启时 — logEnd 结构化格式 | ~500 字节输出,走 Logcat 异步 I/O |
| 开启时 — Trace | beginAsyncSection / endAsyncSection 微秒级 |
自然完成(complete)路径不捕获堆栈,高频动画完成不受影响。
十一、验证方法
11.1 开启追踪
# 仅追踪 SystemUI 进程
adb shell setprop debug.animator.trace com.android.systemui
# 重启 SystemUI 使配置生效(property 在进程启动时读取一次)
adb shell kill $(adb shell pidof com.android.systemui)11.2 验证 Logcat 输出
# 同时查看所有 Tracer 日志(含 ViewPropertyTracer)
adb shell setprop debug.folme.trace 1
adb shell setprop debug.view.prop.trace com.android.systemui
adb shell kill $(adb shell pidof com.android.systemui)
adb logcat -s AnimatorTracer:I DynAnimTracer:I FolmeTracer:I ViewPropTrace:I操作手机触发动画:
- 下拉通知栏 → 通知展开/收起动画(Animator + Spring)
- 切换快捷开关 → QS tile 动画
- 滑动通知 → FlingAnimation
- 锁屏输入密码 → 数字键盘动画
11.3 验证 Perfetto Trace
adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/trace.perfetto-trace \
<<EOF
buffers: { size_kb: 65536 }
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
atrace_categories: "view"
atrace_categories: "app"
atrace_apps: "com.android.systemui"
}
}
}
duration_ms: 10000
EOF在 Perfetto UI 中:
- 找到
com.android.systemui进程的异步 track - 搜索
"Anim|"→ 过滤原生 Animator slice - 搜索
"Spring|"→ 过滤 SpringAnimation slice - 搜索
"Fling|"→ 过滤 FlingAnimation slice - 搜索
"Folme|"→ 过滤 Folme 动画 slice - 关联 Log: 点击某个 slice 看到 ID → logcat 中
grep "ID: <N>"
11.4 验证 ViewPropertyTracer
# 单独开启 ViewPropertyTracer
adb shell setprop debug.view.prop.trace com.android.systemui
adb shell kill $(adb shell pidof com.android.systemui)
adb logcat -s ViewPropTrace:I
# 操作手机触发动画,预期看到:
# ViewPropTrace: ── F:1234567 ──
# ViewPropTrace: │ NotifRow@3f2c alpha=0.750 transY=-4511.5 验证关闭时零开销
adb shell setprop debug.animator.trace 0
adb shell setprop debug.view.prop.trace 0
adb shell kill $(adb shell pidof com.android.systemui)
adb logcat -s AnimatorTracer DynAnimTracer ViewPropTrace
# 预期:无任何输出十二、排查场景示例
场景 1: 动画闪烁 — 刚开始就结束
- Perfetto 中看到 ID = 58,slice 仅 16ms
- Logcat 搜索
"ID: 58" - 找到 Begin 日志 → CallStack 知道谁创建了动画
- 找到 End 日志 →
Reason: cancel+ EndCallStack 知道谁取消了它 - 结论:两段代码在同一帧内先后调用了 start 和 cancel
场景 2: 动画卡在某处不结束
- Perfetto 中看到
Anim|42|...slice 一直延伸到 trace 结束 - 有 Begin 日志,无对应 End 日志
- 检查 Begin 日志的
repeat: INFINITE— 是否设置了无限循环? - 如果不是无限循环,说明动画被泄漏
场景 3: Spring 动画不收敛
- Perfetto 中看到
Spring|105|...slice 异常长(>2s) - Logcat 搜索
"ID: 105"→ Begin 日志显示 DampingRatio 和 Stiffness - 如果 DampingRatio < 0.5 且 Stiffness < 100 → 参数配置不当导致振荡过久
场景 4: 动画被意外替换
- 搜索目标对象 hash(如
@3f2c1a8) - 找到同一对象同一属性的两个 Begin 日志
- 第一个的 End 显示
Reason: cancel— 被新动画覆盖 - EndCallStack 指向第二个动画的 start 位置
十三、FolmeTracer 参考
详细设计方案见 miuix 仓库:
/home/zbc/micode/miuix/library/folme/FOLME_TRACER_GUIDE.md
FolmeTracer 的数据流:
Folme.use(view).state().to(pressed, config)
└── FolmeEngine.fromTo()
├── info.traceCallStack = FolmeTracer.captureCallStack()
└── toAnim(info)
└── AnimManager.onStart()
└── FolmeTracer.beginTransition(info)
输出: Trace begin + 详细 Log
... (动画执行) ...
└── AnimManager.onEnd() / onReplaced()
└── FolmeTracer.endTransition(info, reason)
输出: Trace end + 简短 Log
联合使用:
adb shell setprop debug.folme.trace 1
adb shell setprop debug.animator.trace com.android.systemui
adb shell kill $(adb shell pidof com.android.systemui)
adb logcat -s FolmeTracer:I AnimatorTracer:I DynAnimTracer:I十四、Git Diff 汇总
新增: core/java/android/animation/AnimatorTracer.java (327 行)
新增: core/java/com/android/internal/dynamicanimation/animation/DynamicAnimationTracer.java (162 行)
修改: core/java/android/animation/ValueAnimator.java
- Line 255-258: +4 行 (mTraceId, mTraceSectionName, mTraceCallStack, mTraceEndReason)
- Line 651: +1 行 (initAnimation 中 onAnimationInit)
- Line 1110-1112: +3 行 (start 中 init + 堆栈捕获)
- Line 1197-1199: +3 行 (cancel 中 reason + 堆栈捕获)
- Line 1221-1223: +3 行 (end 中 reason + 堆栈捕获)
- Line 1323: +1 行 (endAnimation 中 onAnimationEnd)
- Line 1354-1356: +3 行 (startAnimation 中 onAnimationStart)
修改: core/java/com/android/internal/dynamicanimation/animation/DynamicAnimation.java
- Line 315-317: +3 行 (mTraceId, mTraceSectionName, mTraceCallStack)
- Line 609-612: +4 行 (start 中 init + 堆栈捕获)
- Line 669-671: +3 行 (startAnimationInternal 中 onAnimationStart)
- Line 727: +1 行 (endAnimationInternal 中 onAnimationEnd)
总计:2 个新文件 + 2 个文件修改。
ViewPropertyTracer 部分
新增: core/java/android/view/ViewPropertyTracer.java (189 行)
修改: core/java/android/view/View.java
- Line 5677: +1 行 (mTraceCounterPrefix 缓存字段)
- Line 20021: +1 行 (setRotation 中 traceFloat)
- Line 20069: +1 行 (setRotationY 中 traceFloat)
- Line 20116: +1 行 (setRotationX 中 traceFloat)
- Line 20155: +1 行 (setScaleX 中 traceFloat)
- Line 20195: +1 行 (setScaleY 中 traceFloat)
- Line 20454: +1 行 (setAlpha 中 traceFloat)
- Line 20985: +1 行 (setElevation 中 tracePosition)
- Line 21021: +1 行 (setTranslationX 中 tracePosition)
- Line 21059: +1 行 (setTranslationY 中 tracePosition)
- Line 21111: +1 行 (setTranslationZ 中 tracePosition)
修改: core/java/android/animation/AnimationHandler.java
- Line 25: +1 行 (import ViewPropertyTracer)
- Line 312: +1 行 (addAnimationFrameCallback 中 init)
- Line 379: +1 行 (doAnimationFrame 中 beginFrame)
- Line 390: +1 行 (doAnimationFrame 中 endFrame)
总计(含 ViewPropertyTracer):3 个新文件 + 4 个文件修改。
十五、ViewPropertyTracer:View 属性逐帧追踪
15.1 背景
AnimatorTracer 追踪动画生命周期(start/end/interpolator/callstack),但无法追踪:
ValueAnimator+UpdateListener模式下实际修改了哪些 View 属性- 每一帧的实际属性值(alpha=0.75、translationY=-45 等)
- 动画曲线的逐帧形状
ViewPropertyTracer 通过在 View.java 的属性 setter 中插桩,解决上述盲区。
15.2 System Property
| Property | 用途 | 默认值 |
|---|---|---|
debug.view.prop.trace | ViewPropertyTracer 独立开关 | "" (关闭) |
值格式(与 debug.animator.trace 一致):
"0"/""/"false"→ 关闭"1"/"true"→ 所有进程"com.android.systemui"→ 仅 SystemUI"com.android.systemui,com.miui.home"→ 多进程
15.3 追踪原理
AnimationHandler.doAnimationFrame()
├── ViewPropertyTracer.beginFrame() ← 开启帧收集
├── callbacks[i].doAnimationFrame()
│ → ValueAnimator.animateValue()
│ → UpdateListener.onAnimationUpdate()
│ → view.setAlpha(0.75)
│ → ViewPropertyTracer.traceFloat(view, "alpha", 0.75)
│ ├── Trace.setCounter("V|Row@3f2c|alpha", 750) [Perfetto 折线图]
│ └── 记录到帧缓冲 [Logcat 批量输出]
└── ViewPropertyTracer.endFrame() ← 输出本帧所有变化
关键设计:仅在 AnimationHandler 动画帧期间(sInAnimationFrame = true)输出。布局、手动设置属性等非动画操作不会被追踪。
15.4 覆盖的 View 属性
| 属性 | setter | Trace 方法 | Counter 值 |
|---|---|---|---|
| alpha | setAlpha(float) | traceFloat | value * 1000 |
| scaleX | setScaleX(float) | traceFloat | value * 1000 |
| scaleY | setScaleY(float) | traceFloat | value * 1000 |
| rotation | setRotation(float) | traceFloat | value * 1000 |
| rotationX | setRotationX(float) | traceFloat | value * 1000 |
| rotationY | setRotationY(float) | traceFloat | value * 1000 |
| translationX | setTranslationX(float) | tracePosition | Math.round(value) |
| translationY | setTranslationY(float) | tracePosition | Math.round(value) |
| translationZ | setTranslationZ(float) | tracePosition | Math.round(value) |
| elevation | setElevation(float) | tracePosition | Math.round(value) |
setX/setY/setZ 不插桩(内部调用 setTranslationX/Y/Z,避免重复)。
15.5 输出格式
Logcat 输出(Tag: ViewPropTrace)
ViewPropTrace: ── F:1234567 ──
ViewPropTrace: │ NotificationRow@3f2c alpha=0.750 transY=-45
ViewPropTrace: │ QSTileView@ab12 scaleX=0.920 scaleY=0.920
ViewPropTrace: ── F:1234583 ──
ViewPropTrace: │ NotificationRow@3f2c alpha=0.680 transY=-52
ViewPropTrace: │ QSTileView@ab12 scaleX=0.880 scaleY=0.880
格式说明:
F:后为帧时间戳(uptimeMillis)- 同一 View 的多个属性合并在一行
- 每帧一次
Log.i调用(批量输出,非逐属性输出) - float 值显示 3 位小数,整数值省略小数点
Perfetto Counter Track
Counter 名称格式: V|{SimpleClassName}@{hash低16位}|{property}
示例:
V|NotificationRow@3f2c|alpha → 1000, 920, 780, 580, 380, 220, 80, 0
V|NotificationRow@3f2c|transY → 0, -12, -28, -45, -67, -82, -93, -100
V|QSTileView@ab12|scaleX → 1000, 980, 920, 880, 920, 980, 1000
在 Perfetto UI 中,每个 Counter track 显示为折线图,直接可视化动画曲线形状。
15.6 使用方法
基本使用
# 开启(仅 SystemUI)
adb shell setprop debug.view.prop.trace com.android.systemui
adb shell kill $(adb shell pidof com.android.systemui)
# 查看逐帧属性日志
adb logcat -s ViewPropTrace:I联合 AnimatorTracer 使用(推荐)
# 同时开启动画生命周期 + 逐帧属性追踪
adb shell setprop debug.animator.trace com.android.systemui
adb shell setprop debug.view.prop.trace com.android.systemui
adb shell kill $(adb shell pidof com.android.systemui)
# 查看完整日志
adb logcat -s AnimatorTracer:I DynAnimTracer:I ViewPropTrace:I联合所有 Tracer
# 全量追踪(Animator + DynamicAnimation + Folme + View 属性)
adb shell setprop debug.animator.trace com.android.systemui
adb shell setprop debug.view.prop.trace com.android.systemui
adb shell setprop debug.folme.trace 1
adb shell kill $(adb shell pidof com.android.systemui)
adb logcat -s AnimatorTracer:I DynAnimTracer:I FolmeTracer:I ViewPropTrace:I抓取 Perfetto Trace(含 Counter Track)
adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/trace.perfetto-trace \
<<EOF
buffers: { size_kb: 65536 }
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
atrace_categories: "view"
atrace_categories: "app"
atrace_apps: "com.android.systemui"
}
}
}
duration_ms: 10000
EOF在 Perfetto UI 中:
- 搜索
"V|"→ 过滤 View 属性 Counter tracks(折线图) - 搜索
"Anim|"→ 过滤 Animator async slices - 搜索
"Spring|"→ 过滤 Spring 动画 slices - Counter track 折线图 + Async slice 时间区间对齐观察
关闭追踪
adb shell setprop debug.view.prop.trace 0
adb shell kill $(adb shell pidof com.android.systemui)15.7 排查场景
场景 1: ValueAnimator + Listener 不知道改了什么属性
问题: AnimatorTracer 显示 Type: ValueAnimator | Properties: ?: 0→1,不知道实际修改了哪些 View 属性。
排查:
- AnimatorTracer Begin 日志给出代码位置(CallStack)
- ViewPropTrace 给出同一时间段内哪些 View 哪些属性在变化
- 通过时间对齐确认:ID=42 的 Animator 运行 300ms 期间,
NotifRow@3f2c的 alpha 从 1 变到 0
场景 2: 动画曲线不对(过冲、抖动、线性段)
排查:
- Perfetto 中找到对应的 Counter track 折线图
- 直接观察曲线形状:是否有预期的加速减速?是否有不该出现的线性段?
- 对比 AnimatorTracer Begin 日志中的 Interpolator 名称,确认是否配置正确
场景 3: 两个动画同时修改同一 View 属性(冲突)
排查:
- ViewPropTrace 日志中同一帧内看到
View@hash alpha=0.5然后又出现View@hash alpha=0.8 - 说明同一帧有两次 setAlpha 调用(两个动画在打架)
- Perfetto Counter track 会显示锯齿状曲线(交替设值)
- 结合 AnimatorTracer 的 Begin 日志堆栈定位两个冲突源
场景 4: 属性设置了但视觉上看不到
排查:
- ViewPropTrace 确认属性值确实在变化
- 检查 View 是否可见(日志中
[GONE]标记表示 View 不可见) - 检查是否被其他 View 遮挡(需配合 Layout Inspector)
15.8 性能影响
| 场景 | 开销 |
|---|---|
| 关闭时 | isEnabled() volatile 读(false)+ sInAnimationFrame 布尔判断 → ~2ns |
| 开启时 — 非动画帧 | !sInAnimationFrame 跳过 → ~2ns |
| 开启时 — 动画帧 | Trace.setCounter() ~50ns + StringBuilder append ~100ns,每属性 ~150ns |
| 开启时 — endFrame | 一次 Log.i() 批量输出,异步 I/O |
典型场景:一帧内 10 个属性变化 → 增加 ~1.5us(占 16.6ms 帧预算的 0.01%),可忽略。
15.9 实现文件
| 文件 | 操作 | 改动 |
|---|---|---|
core/java/android/view/ViewPropertyTracer.java | 新增 | 189 行 |
core/java/android/view/View.java | 修改 | +1 字段 (mTraceCounterPrefix),10 个 setter 各 +1 行 |
core/java/android/animation/AnimationHandler.java | 修改 | +1 import,+3 行 (init/beginFrame/endFrame) |
十六、五层追踪体系总览
| 层级 | System Property | Tracer | 追踪内容 | Log Tag |
|---|---|---|---|---|
| 动画生命周期 | debug.animator.trace | AnimatorTracer | start/end、代码位置、interpolator、duration | AnimatorTracer |
| 物理动画 | debug.animator.trace | DynamicAnimationTracer | Spring/Fling 参数、start/end | DynAnimTracer |
| Folme 状态 | debug.folme.trace | FolmeTracer | Folme 状态转换 | FolmeTracer |
| View 属性逐帧 | debug.view.prop.trace | ViewPropertyTracer | 每帧具体属性值、曲线可视化 | ViewPropTrace |
| View 刷新链路 | debug.view.refresh.trace | ViewRefreshTracer | invalidate/requestLayout/scheduleVsync + 堆栈 | ViewRefresh |
快速开启全量追踪:
adb shell setprop debug.animator.trace com.android.systemui
adb shell setprop debug.view.prop.trace com.android.systemui
adb shell setprop debug.view.refresh.trace com.android.systemui
adb shell setprop debug.folme.trace 1
adb shell kill $(adb shell pidof com.android.systemui)
adb logcat -s AnimatorTracer:I DynAnimTracer:I FolmeTracer:I ViewPropTrace:I ViewRefresh:D十七、ViewRefreshTracer:View 刷新链路追踪
17.1 背景
动画追踪解决了”哪个动画在运行”的问题,但无法回答:
- 谁触发了
View.invalidate()(重绘)? - 谁触发了
View.requestLayout()(重布局)? - 谁请求了
scheduleVsync()(下一帧)?
这些问题在排查刷新风暴、掉帧、无效重绘时至关重要。
17.2 System Property
| Property | 用途 | 默认值 |
|---|---|---|
debug.view.refresh.trace | ViewRefreshTracer 独立开关 | "" (关闭) |
值格式与其他 Tracer 完全一致("0" / "1" / 进程名 / 多进程逗号分隔)。
17.3 Hook 点
| Hook | 方法 | 位置 | 触发条件 |
|---|---|---|---|
| A | View.invalidate(boolean) | 方法入口 | 每次 invalidate 调用 |
| B | View.requestLayout() | mViewRequestingLayout == null 分支内 | 仅根发起者(过滤递归传播) |
| C | DisplayEventReceiver.scheduleVsync() | 方法入口 | 每次 Vsync 请求 |
requestLayout 仅追踪根发起者的设计:requestLayout() 会通过 mParent.requestLayout() 递归向上传播,每一级父 View 都会执行该方法。但 mViewRequestingLayout 在首次进入时为 null,之后被设为发起者,因此只有最初触发的 View 会输出日志。
17.4 输出格式
D/ViewRefresh: invalidate NotificationRow
D/ViewRefresh: java.lang.Exception
D/ViewRefresh: at android.view.ViewRefreshTracer.traceInvalidate(ViewRefreshTracer.java:63)
D/ViewRefresh: at android.view.View.invalidate(View.java:22019)
D/ViewRefresh: at com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.onStateUpdated(...)
D/ViewRefresh: requestLayout QSPanel
D/ViewRefresh: java.lang.Exception
D/ViewRefresh: at android.view.ViewRefreshTracer.traceRequestLayout(ViewRefreshTracer.java:69)
D/ViewRefresh: at android.view.View.requestLayout(View.java:30693)
D/ViewRefresh: at android.widget.TextView.checkForRelayout(TextView.java:9876)
D/ViewRefresh: scheduleVsync FrameDisplayEventReceiver
D/ViewRefresh: java.lang.Exception
D/ViewRefresh: at android.view.ViewRefreshTracer.traceScheduleVsync(ViewRefreshTracer.java:75)
D/ViewRefresh: at android.view.DisplayEventReceiver.scheduleVsync(DisplayEventReceiver.java:356)
D/ViewRefresh: at android.view.Choreographer.scheduleVsyncLocked(Choreographer.java:698)
17.5 使用方法
# 开启(仅 SystemUI)
adb shell setprop debug.view.refresh.trace com.android.systemui
adb shell kill $(adb shell pidof com.android.systemui)
# 查看全部刷新日志
adb logcat -s ViewRefresh:D
# 按类型过滤
adb logcat -s ViewRefresh:D | grep "invalidate"
adb logcat -s ViewRefresh:D | grep "requestLayout"
adb logcat -s ViewRefresh:D | grep "scheduleVsync"
# 按 View 类名过滤
adb logcat -s ViewRefresh:D | grep "NotificationRow"
# 关闭
adb shell setprop debug.view.refresh.trace 0
adb shell kill $(adb shell pidof com.android.systemui)17.6 排查场景
场景 1: invalidate 风暴导致掉帧
- 开启 ViewRefreshTracer,复现掉帧
grep "invalidate"查看哪些 View 在高频重绘- 堆栈定位触发源,判断是否为不必要的刷新
场景 2: requestLayout 风暴
grep "requestLayout"查看短时间内的根发起者- 常见原因:
TextView.setText()触发文本 relayout、动态添加 View - 优化方向:缓存布局结果、避免频繁 setText
场景 3: 联合 AnimatorTracer 定位动画触发的刷新
- AnimatorTracer 给出动画 ID + 目标 + 属性
- ViewRefreshTracer 给出每次 invalidate 的 View + 堆栈
- 通过时间戳对齐确认:动画
onAnimationUpdate→view.invalidate()链路
17.7 性能影响
| 场景 | 开销 |
|---|---|
| 关闭时 | 两次 volatile 读(sInited + sEnabled)→ ~2-3ns |
| 开启时 | new Exception() 堆栈构建 ~0.05ms + Log.d() 异步 I/O |
注意: invalidate 可能每帧被多个 View 调用多次,开启时有可感知的性能影响。严格限调试阶段使用。
17.8 实现文件
| 文件 | 操作 | 改动 |
|---|---|---|
core/java/android/view/ViewRefreshTracer.java | 新增 | 79 行 |
core/java/android/view/View.java | 修改 | +1 行 (invalidate) +1 行 (requestLayout) |
core/java/android/view/DisplayEventReceiver.java | 修改 | +1 行 (scheduleVsync) |
17.9 与其他 Tracer 的互补关系
问题排查流向:
"界面为什么卡?"
│
├── ViewRefreshTracer → 谁在触发 invalidate/requestLayout?(根因)
│ │
│ ├── 是动画触发的? → AnimatorTracer → 哪个动画?谁启动的?
│ │ → ViewPropertyTracer → 属性曲线正常吗?
│ │
│ ├── 是 Folme 动画触发的? → FolmeTracer → 哪个状态转换?
│ │
│ └── 是业务代码直接触发的? → 堆栈直接定位代码行
│
└── scheduleVsync 堆栈 → 确认帧调度链路是否正常
十八、未覆盖动画体系
| 状态 | 体系 | 覆盖情况 |
|---|---|---|
| 已覆盖 | ValueAnimator / ObjectAnimator / AnimatorSet / ViewPropertyAnimator | AnimatorTracer |
| 已覆盖 | SpringAnimation / FlingAnimation / DynamicAnimation | DynamicAnimationTracer |
| 已覆盖 | Folme 状态动画 | FolmeTracer |
| 已覆盖 | View 属性逐帧值 + 曲线 | ViewPropertyTracer |
| 已覆盖 | View 刷新链路 (invalidate / requestLayout / scheduleVsync) | ViewRefreshTracer |
| 间接覆盖 | Transition / AVD / Lottie(底层走 ValueAnimator) | AnimatorTracer(信息不够直观) |
| 未覆盖 | Jetpack Compose 动画 / Legacy View Animation / Scroller | 待评估 |
TODO
-
P1: 调研 Jetpack Compose 动画 tracing 可行性
-
P2: 评估 Legacy View Animation 是否值得追踪
-
改进间接覆盖:为 Lottie / Transition 入口打额外 marker 关联底层 Animator ID