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 条 importFolmeTracer 已覆盖(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 风格统一的追踪能力:

  1. Perfetto Trace: 丰富的异步 section 名称 "Anim|42|Obj|Button@3f2c|alpha:1.0->0.0|AccelDecel|300ms"(包含 ID,方便与 Logcat 对应)
  2. Logcat Log: 结构化框线格式,包含完整调用堆栈,一步定位触发动画的 SystemUI 代码行
  3. 零侵入: 通过 system property 控制,关闭时接近零开销
  4. 全覆盖: 自动追踪 ValueAnimator、ObjectAnimator、AnimatorSet 子动画、ViewPropertyAnimator、SpringAnimation、FlingAnimation
  5. 全链路 ID: start → init → end 通过同一个 mTraceId 串联
  6. 结束原因追踪: 区分 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 property
    • AnimatorTracer.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 + DynamicAnimationTracer
  • debug.view.prop.trace — ViewPropertyTracer
  • debug.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 使用缓存的 sectionNamePerfetto 要求 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 格式

动画类型格式示例
SpringAnimationSpring|{id}|{target}|{prop}|s={stiffness},d={damping}|{start}->{end}Spring|105|NotifRow@3f2c|translationY|s=200,d=0.8|0->-100
FlingAnimationFling|{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.javacore/java/android/animation/新增327 行
ValueAnimator.javacore/java/android/animation/修改+4 字段 +14 行插桩
DynamicAnimationTracer.javacore/java/com/android/internal/dynamicanimation/animation/新增162 行
DynamicAnimation.javacore/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 前缀格式示例
ObjectAnimatorAnim|Anim|{id}|Obj|{class}@{hash}|{props}|{interp}|{dur}msAnim|42|Obj|NotifRow@3f2c|alpha:1->0|AccelDecel|300ms
ValueAnimatorAnim|Anim|{id}|Val|{props}|{interp}|{dur}msAnim|43|Val|?:0->1|Linear|200ms
SpringAnimationSpring|Spring|{id}|{target}|{prop}|s={s},d={d}|{from}->{to}Spring|105|NotifRow@3f2c|translationY|s=200,d=0.8|0->-100
FlingAnimationFling|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()AnimatorTracerObjectAnimator 继承 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() 时命中
SpringAnimationDynamicAnimationTracer独立 hook start() / endAnimationInternal()
FlingAnimationDynamicAnimationTracer同上
Folme 动画(由 FolmeTracer 覆盖)FolmeTracer走 miuix.animation 独立管线
View 属性逐帧值ViewPropertyTracerView setter 层面捕获,含自定义 ValueAnimator+Listener 模式

八、五个 Tracer 对比

维度AnimatorTracerDynamicAnimationTracerViewPropertyTracerFolmeTracerViewRefreshTracer
追踪目标ValueAnimator/ObjectAnimatorSpringAnimation/FlingAnimationView 属性逐帧值Folme 状态动画invalidate/requestLayout/scheduleVsync
Property 控制debug.animator.trace同左(共享)debug.view.prop.tracedebug.folme.tracedebug.view.refresh.trace
Perfetto 输出Async SectionAsync SectionCounter Track(折线图)Async Section无(纯 Logcat)
Log TagAnimatorTracerDynAnimTracerViewPropTraceFolmeTracerViewRefresh
Log LevelLog.iLog.iLog.iLog.iLog.d
输出频率start/end 时start/end 时每帧start/end 时每次 invalidate/requestLayout/scheduleVsync
End 原因cancel / force_end / completecancel / completeN/Acancel / complete / replacedN/A
堆栈start 时 + cancel/end 时start 时start 时每次调用都有new Exception()
实现位置AOSP frameworkAOSP framework (internal)AOSP framework (view)folme AAR (miuix)AOSP framework (view)
性能影响极低中等(仅限调试)
ID 空间全局 AtomicInteger同左(共享)无(按 View 实例区分)独立 TransitionInfo.id

九、线程安全

  • ValueAnimatorstart() / startAnimation() / endAnimation() 均在 UI 线程执行
  • DynamicAnimationstart() / cancel() / endAnimationInternal() 同样限制在创建线程
  • 所有 trace 字段(mTraceIdmTraceSectionNamemTraceCallStackmTraceEndReason)仅单线程读写,无需同步
  • 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
开启时 — TracebeginAsyncSection / 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 中:

  1. 找到 com.android.systemui 进程的异步 track
  2. 搜索 "Anim|" → 过滤原生 Animator slice
  3. 搜索 "Spring|" → 过滤 SpringAnimation slice
  4. 搜索 "Fling|" → 过滤 FlingAnimation slice
  5. 搜索 "Folme|" → 过滤 Folme 动画 slice
  6. 关联 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=-45

11.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: 动画闪烁 — 刚开始就结束

  1. Perfetto 中看到 ID = 58,slice 仅 16ms
  2. Logcat 搜索 "ID: 58"
  3. 找到 Begin 日志 → CallStack 知道谁创建了动画
  4. 找到 End 日志 → Reason: cancel + EndCallStack 知道谁取消了它
  5. 结论:两段代码在同一帧内先后调用了 start 和 cancel

场景 2: 动画卡在某处不结束

  1. Perfetto 中看到 Anim|42|... slice 一直延伸到 trace 结束
  2. 有 Begin 日志,无对应 End 日志
  3. 检查 Begin 日志的 repeat: INFINITE — 是否设置了无限循环?
  4. 如果不是无限循环,说明动画被泄漏

场景 3: Spring 动画不收敛

  1. Perfetto 中看到 Spring|105|... slice 异常长(>2s)
  2. Logcat 搜索 "ID: 105" → Begin 日志显示 DampingRatio 和 Stiffness
  3. 如果 DampingRatio < 0.5 且 Stiffness < 100 → 参数配置不当导致振荡过久

场景 4: 动画被意外替换

  1. 搜索目标对象 hash(如 @3f2c1a8
  2. 找到同一对象同一属性的两个 Begin 日志
  3. 第一个的 End 显示 Reason: cancel — 被新动画覆盖
  4. 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.traceViewPropertyTracer 独立开关"" (关闭)

值格式(与 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 属性

属性setterTrace 方法Counter 值
alphasetAlpha(float)traceFloatvalue * 1000
scaleXsetScaleX(float)traceFloatvalue * 1000
scaleYsetScaleY(float)traceFloatvalue * 1000
rotationsetRotation(float)traceFloatvalue * 1000
rotationXsetRotationX(float)traceFloatvalue * 1000
rotationYsetRotationY(float)traceFloatvalue * 1000
translationXsetTranslationX(float)tracePositionMath.round(value)
translationYsetTranslationY(float)tracePositionMath.round(value)
translationZsetTranslationZ(float)tracePositionMath.round(value)
elevationsetElevation(float)tracePositionMath.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 中:

  1. 搜索 "V|" → 过滤 View 属性 Counter tracks(折线图)
  2. 搜索 "Anim|" → 过滤 Animator async slices
  3. 搜索 "Spring|" → 过滤 Spring 动画 slices
  4. 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 属性。

排查:

  1. AnimatorTracer Begin 日志给出代码位置(CallStack)
  2. ViewPropTrace 给出同一时间段内哪些 View 哪些属性在变化
  3. 通过时间对齐确认:ID=42 的 Animator 运行 300ms 期间,NotifRow@3f2c 的 alpha 从 1 变到 0

场景 2: 动画曲线不对(过冲、抖动、线性段)

排查:

  1. Perfetto 中找到对应的 Counter track 折线图
  2. 直接观察曲线形状:是否有预期的加速减速?是否有不该出现的线性段?
  3. 对比 AnimatorTracer Begin 日志中的 Interpolator 名称,确认是否配置正确

场景 3: 两个动画同时修改同一 View 属性(冲突)

排查:

  1. ViewPropTrace 日志中同一帧内看到 View@hash alpha=0.5 然后又出现 View@hash alpha=0.8
  2. 说明同一帧有两次 setAlpha 调用(两个动画在打架)
  3. Perfetto Counter track 会显示锯齿状曲线(交替设值)
  4. 结合 AnimatorTracer 的 Begin 日志堆栈定位两个冲突源

场景 4: 属性设置了但视觉上看不到

排查:

  1. ViewPropTrace 确认属性值确实在变化
  2. 检查 View 是否可见(日志中 [GONE] 标记表示 View 不可见)
  3. 检查是否被其他 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 PropertyTracer追踪内容Log Tag
动画生命周期debug.animator.traceAnimatorTracerstart/end、代码位置、interpolator、durationAnimatorTracer
物理动画debug.animator.traceDynamicAnimationTracerSpring/Fling 参数、start/endDynAnimTracer
Folme 状态debug.folme.traceFolmeTracerFolme 状态转换FolmeTracer
View 属性逐帧debug.view.prop.traceViewPropertyTracer每帧具体属性值、曲线可视化ViewPropTrace
View 刷新链路debug.view.refresh.traceViewRefreshTracerinvalidate/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.traceViewRefreshTracer 独立开关"" (关闭)

值格式与其他 Tracer 完全一致("0" / "1" / 进程名 / 多进程逗号分隔)。

17.3 Hook 点

Hook方法位置触发条件
AView.invalidate(boolean)方法入口每次 invalidate 调用
BView.requestLayout()mViewRequestingLayout == null 分支内仅根发起者(过滤递归传播)
CDisplayEventReceiver.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 风暴导致掉帧

  1. 开启 ViewRefreshTracer,复现掉帧
  2. grep "invalidate" 查看哪些 View 在高频重绘
  3. 堆栈定位触发源,判断是否为不必要的刷新

场景 2: requestLayout 风暴

  1. grep "requestLayout" 查看短时间内的根发起者
  2. 常见原因:TextView.setText() 触发文本 relayout、动态添加 View
  3. 优化方向:缓存布局结果、避免频繁 setText

场景 3: 联合 AnimatorTracer 定位动画触发的刷新

  1. AnimatorTracer 给出动画 ID + 目标 + 属性
  2. ViewRefreshTracer 给出每次 invalidate 的 View + 堆栈
  3. 通过时间戳对齐确认:动画 onAnimationUpdateview.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 / ViewPropertyAnimatorAnimatorTracer
已覆盖SpringAnimation / FlingAnimation / DynamicAnimationDynamicAnimationTracer
已覆盖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