AnimatorTracer:Android 原生 Animator Perfetto Trace + 详细 Log 集成文档

日期: 2026-04-29(更新: 2026-05-07)
作者: 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 处调用仅有框架层简陋 Trace,缺少详细信息

框架层 ValueAnimator 已有 Trace.asyncTraceBegin/End,但 section 名称极其简陋:

ValueAnimator → "animator"
ObjectAnimator → "animator:alpha"

无法提供:调用堆栈、属性起止值、interpolator 详情、目标对象信息。在 Perfetto 中看到一堆 "animator" slice 时完全无法区分是哪个动画。

1.2 目标

在 AOSP 框架层 ValueAnimator 中插桩,添加与 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 内部动画

二、设计思路

2.1 为什么选择框架层 Hook

方案优点缺点
A. 框架层 Hook(采用)自动覆盖所有 Animator,零遗漏需修改 AOSP 源码
B. SystemUI 层包装不改框架需修改 417 处调用点,不现实
C. 反射 Hook不改任何源码性能差,维护困难

整机源码编译环境下,直接修改框架是最干净的方案。

2.2 Hook 点选择

Android 原生动画的核心生命周期:

用户调用:
  ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).start()
    │
    ▼
ValueAnimator.start()                   ← 公开方法,在调用方线程
  │                                         【Hook A: 捕获调用堆栈】
  ├── start(boolean playBackwards)       ← 私有方法
  │     ├── addAnimationCallback()       ← 注册到 Choreographer
  │     └── startAnimation()             ← 无 startDelay 时直接调用
  │
  ▼ (Choreographer 帧回调)
ValueAnimator.startAnimation()           ← 动画实际开始
  ├── initAnimation()                    ← PropertyValuesHolder 初始化
  │                                         【Hook B: Trace begin + 详细 Log】
  │                                         (此时 mValues[] 已填充,startValue/endValue 可用)
  ├── mRunning = true
  └── notifyStartListeners()
    │
    ▼ (每帧执行)
doAnimationFrame() → animateValue()
    │
    ▼
ValueAnimator.endAnimation()             ← 动画结束
  ├── notifyEndListeners()
  │                                         【Hook C: Trace end + 简短 Log】
  └── Trace.asyncTraceEnd() (原有)

关键设计决策

  1. 堆栈在 start() 捕获:此时仍运行在调用方线程(SystemUI 主线程),Thread.currentThread().getStackTrace() 能追溯到 Folme.use(view).state().to(...)ObjectAnimator.start() 的具体调用位置。到 startAnimation() 时堆栈已被 Choreographer 帧调度替换。

  2. Trace + Log 在 startAnimation()initAnimation() 之后输出initAnimation() 负责初始化 PropertyValuesHolder 的 keyframes,只有在它执行完之后 mKeyframes.getValue(0f) / getValue(1f) 才能返回正确的 startValue/endValue。

  3. 堆栈通过字段桥接mTraceCallStack 字段在 start() 中写入,在 startAnimation() 中读取后置 null 释放引用。

2.3 Trace 与 Log 的 ID 关联

问题: Perfetto UI 中看到一个动画 slice,想查看它的完整堆栈信息,需要在 logcat 中找到对应日志。如果 Trace section name 中没有 ID,只能靠时间戳和属性名模糊匹配,效率低下。

方案: 将全局递增的 traceId 同时写入:

  1. Section name: "Anim|42|Obj|View@ab12|alpha:1->0|AccelDecel|300ms" — Perfetto UI 直接可见
  2. Logcat Begin: "│ ID: 42" — 结构化日志中的第一行信息
  3. Logcat End: "Anim End | ID: 42 | ..." — 结束日志也带 ID

使用流程:

  1. 在 Perfetto UI 看到 Anim|42|... slice
  2. 在 logcat 中搜索 "ID: 42" → 立即找到完整堆栈和属性详情
  3. 反之亦然:logcat 中看到异常动画 → 用 ID 在 Perfetto 中定位其时间线位置

与 FolmeTracer 风格统一:FolmeTracer 的 section name 同样包含 ID 字段。

2.4 进程过滤

框架层修改影响所有 app。通过 debug.animator.trace 的值实现精确过滤:

"0" / "" / "false"              → 关闭(默认)
"1" / "true"                    → 所有进程
"com.android.systemui"          → 仅 SystemUI
"com.android.systemui,com.miui.home" → 多个进程

使用 ActivityThread.currentProcessName() 获取当前进程名,在 init() 中一次性比对。

2.5 线程安全

  • start() / startAnimation() / endAnimation() 均在 UI 线程执行(start()Looper.myLooper() == null 检查,否则抛异常)
  • 3 个新字段(mTraceIdmTraceSectionNamemTraceCallStack)仅 UI 线程读写,无需同步
  • init() 使用 volatile + synchronized 双重检查(与 FolmeTracer 一致)

2.6 不需要修改的文件及原因

文件原因
ObjectAnimator.java继承 ValueAnimator,start()/startAnimation()/endAnimation() 全部走父类 hook 点。AnimatorTracer 通过 instanceof ObjectAnimator 提取 getTarget() / getPropertyName()
AnimatorSet.java子动画通过 node.mAnimation.start() 启动,最终走 ValueAnimator.start(boolean) 的 hook 点
PropertyValuesHolder.javaAnimatorTracer 直接访问其 package-private 字段 mPropertyNamemKeyframes(同包可访问)
ViewPropertyAnimator内部创建 ValueAnimator 实例,自动被追踪

2.7 性能影响

场景开销
关闭时(默认)init(): 一次 volatile 读(sInited)立即返回;isEnabled(): 一次 volatile 读(sEnabled)为 false 跳过。JIT 内联后接近零开销
开启时 — 堆栈捕获Thread.getStackTrace() 约 0.1ms/次,仅在 start() 触发一次,且自动过滤 android.animation.*java.lang.reflect.*
开启时 — Log 输出走 Logcat 异步 I/O,不阻塞动画线程
开启时 — TracebeginAsyncSection / endAsyncSection 微秒级开销

三、修改清单

3.1 文件总览

文件操作路径
AnimatorTracer.java新增 (~270行)aospframeworks/base/core/java/android/animation/AnimatorTracer.java
ValueAnimator.java修改 (+3字段, +11行)aospframeworks/base/core/java/android/animation/ValueAnimator.java

3.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 "debug.animator.trace":
 *   - "0" or unset: disabled (default)
 *   - "1" or "true": trace all processes
 *   - "com.android.systemui": trace only the named process
 *   - "com.android.systemui,com.miui.home": comma-separated process list
 *
 * @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;
    }
 
    public static String captureCallStack() {
        if (!sEnabled) return null;
        StackTraceElement[] traces = Thread.currentThread().getStackTrace();
        StringBuilder sb = new StringBuilder(512);
        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;
            }
            sb.append("│   at ").append(traces[i]).append('\n');
        }
        return sb.toString();
    }
 
    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);
    }
 
    public static void onAnimationEnd(ValueAnimator animator) {
        if (animator.mTraceSectionName == null) return;
        Trace.endAsyncSection(animator.mTraceSectionName, animator.mTraceId);
        logEnd(animator);
        animator.mTraceSectionName = null;
    }
 
    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();
    }
 
    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) {
        StringBuilder sb = new StringBuilder(128);
        sb.append("Anim End | ID: ").append(animator.mTraceId);
        if (animator instanceof ObjectAnimator) {
            sb.append(" | Obj");
            Object target = ((ObjectAnimator) animator).getTarget();
            if (target != null) {
                sb.append(" | ").append(target.getClass().getSimpleName());
            }
            String propName = ((ObjectAnimator) animator).getPropertyName();
            if (propName != null) {
                sb.append(" | ").append(propName);
            }
        } else {
            sb.append(" | Val");
        }
        sb.append(" | ").append(animator.getDuration()).append("ms");
        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;
    }
 
    private 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);
    }
 
    private static String currentProcessName() {
        try {
            return ActivityThread.currentProcessName();
        } catch (Exception e) {
            return null;
        }
    }
}

3.3 修改文件:ValueAnimator.java

改动 A:新增 3 个字段(Line 255-257)

位置: mValuesMap 字段之后

    HashMap<String, PropertyValuesHolder> mValuesMap;
 
    /** @hide */ int mTraceId;             // AnimatorTracer 分配的唯一 ID
    /** @hide */ String mTraceSectionName; // Perfetto section 名称(null 表示未追踪)
    /** @hide */ String mTraceCallStack;   // start() 中捕获的堆栈,startAnimation() 中消费
 
    /**
     * If set to non-negative value, this will override {@link #sDurationScale}.
     */
    private float mDurationScale = -1f;

改动 B:start(boolean) 开头插桩(Line 1108-1111)

位置: Looper 检查之后,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;
        // ... 原有代码不变 ...

改动 C:startAnimation()initAnimation() 之后插桩(Line 1344-1347)

位置: initAnimation() 之后,mRunning = true 之前

    private void startAnimation() {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                    System.identityHashCode(this));
        }
 
        mAnimationEndRequested = false;
        initAnimation();
        if (AnimatorTracer.isEnabled()) {                                   // ← 新增
            AnimatorTracer.onAnimationStart(this, mTraceCallStack);         // ← 新增:Trace begin + Log
            mTraceCallStack = null;                                         // ← 新增:释放引用
        }
        mRunning = true;
        // ... 原有代码不变 ...

改动 D:endAnimation() 中结束追踪(Line 1313)

位置: notifyEndListenersFromEndAnimation() 之后,原有 Trace.asyncTraceEnd 之前

        notifyEndListenersFromEndAnimation(mReversing, postNotifyEndListener);
        AnimatorTracer.onAnimationEnd(this);                                // ← 新增:Trace end + Log
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                    System.identityHashCode(this));
        }
    }

onAnimationEnd() 内部以 mTraceSectionName != null 为守卫,无需外层 isEnabled() 检查。即使在 start 和 end 之间动态关闭追踪,也能正确收尾。


四、输出格式详解

4.1 Perfetto Trace Section

格式: "Anim|<ID>|<type>|<target>|<properties>|<interpolator>|<duration>"

各字段说明:

ObjectAnimatorValueAnimator
前缀Anim|Anim|
ID42(与 Logcat 中 │ ID: 42 一致)同左
typeObj|ClassName@hexHashVal
propertiesalpha:1.0->0.0?:0.0->1.0
interpolatorAccelDecel / Path / Linear同左
duration300ms200ms

完整示例:

Anim|1|Obj|NotificationRow@3f2c1a8|alpha:1->0|AccelDecel|300ms
Anim|2|Obj|QSTileView@ab120f3|scaleX:1->0.8,scaleY:1->0.8|OvershootDecel|200ms
Anim|3|Val|?:0->1|Linear|350ms
Anim|4|Obj|KeyguardBottomArea@7e3f|translationY:0->-200,alpha:1->0,scaleX:1->0.9,+1|Path|500ms

ID 关联: Section name 中的 ID 与 Logcat │ ID: N 完全对应,在 Perfetto UI 中看到某个 slice 后可直接用 ID 在 logcat 中搜索对应的详细堆栈信息。
属性超过 3 个时显示 +N,section 总长度限制 200 字符防止 Perfetto 截断。

4.2 Logcat — 动画开始(详细)

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: │   translationY: 0 → -100.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(NotificationStackScrollLayout.java:1256)
AnimatorTracer: │   at com.android.systemui.statusbar.NotificationStackScrollLayout$3.onViewRemoved(NotificationStackScrollLayout.java:418)
AnimatorTracer: └───────────────────────────────────────────────────────

关键信息:

  • Properties: 每个属性的 startValue → targetValue,整数颜色值以 0xAARRGGBB 格式显示
  • CallStack: 自动过滤 android.animation.*java.lang.reflect.* 帧,只保留业务代码

4.3 Logcat — 动画结束(简短一行)

AnimatorTracer: Anim End | ID: 42 | Obj | NotificationRow | alpha | 300ms

五、与 FolmeTracer 的对比

维度FolmeTracerAnimatorTracer
追踪目标Folme 动画(Folme.use(view).state().to(...))Android 原生 Animator
Property 控制debug.folme.tracedebug.animator.trace
Section 前缀Folme|Anim|
Section 中带 ID是(Folme|42|...是(Anim|42|...
Log TagFolmeTracerAnimatorTracer
实现位置folme AAR (miuix.animation.internal)AOSP 框架 (android.animation)
堆栈捕获点FolmeEngine.fromTo()ValueAnimator.start()
信息输出点AnimManager.onStart()ValueAnimator.startAnimation()
Trace ↔ Log 关联Section name 中的 ID 对应 Log 中的 ID同左,统一风格
进程过滤无(仅限使用 Folme 的 app)支持进程名过滤

Perfetto 中同时查看两种动画:两者都使用 Trace.beginAsyncSection / Trace.endAsyncSection,出现在同一进程的异步 track 上。搜索 "Folme|" 过滤 Folme 动画,搜索 "Anim|" 过滤原生动画。


六、覆盖范围

动画类型是否自动覆盖原理
ObjectAnimator.ofFloat(view, "alpha", 0, 1).start()ObjectAnimator 继承 ValueAnimator,start() 走父类
ValueAnimator.ofFloat(0, 1).start()直接命中 hook 点
AnimatorSet 中的子动画子动画通过 node.mAnimation.start() 启动
view.animate().alpha(0).start() (ViewPropertyAnimator)内部创建 ValueAnimator
XML 动画 (AnimatorInflater.loadAnimator())加载后得到的 Animator 调用 start() 时命中
SpringAnimation (AndroidX DynamicAnimation)不继承 ValueAnimator,走独立管线
Folme 动画(由 FolmeTracer 覆盖)走 miuix.animation 独立管线

七、编译

7.1 增量编译框架

# 在整机源码根目录
source build/envsetup.sh
lunch missi-final_phone_xring_cn_only64_private_build-user
 
# 增量编译 framework core(包含 android.animation 包)
mmm frameworks/base/core

7.2 完整编译 SystemUI(如需同时验证 Folme AAR 升级)

make MiuiSystemUI -j40

八、验证方法

8.1 开启追踪

# 仅追踪 SystemUI 进程
adb shell setprop debug.animator.trace com.android.systemui
 
# 重启 SystemUI 使配置生效(property 在进程启动时读取一次)
adb shell kill $(adb shell pidof com.android.systemui)

8.2 验证 Logcat 输出

# 仅看 AnimatorTracer 日志
adb logcat -s AnimatorTracer
 
# 同时看 Folme 和原生 Animator 日志
adb shell setprop debug.folme.trace 1
adb shell kill $(adb shell pidof com.android.systemui)
adb logcat -s AnimatorTracer:I FolmeTracer:I

操作手机触发动画:

  • 下拉通知栏 → 通知展开/收起动画
  • 切换快捷开关 → QS tile 动画
  • 锁屏输入密码 → 数字键盘动画
  • 充电界面 → 充电动画

预期输出:

AnimatorTracer: AnimatorTracer enabled for process: com.android.systemui

AnimatorTracer: ┌── Native Animator Begin ─────────────────────────────
AnimatorTracer: │ ID: 1
AnimatorTracer: │ Type: ObjectAnimator
AnimatorTracer: │ Target: ...
AnimatorTracer: │ Duration: 300ms
AnimatorTracer: │ Properties:
AnimatorTracer: │   alpha: 1 → 0
AnimatorTracer: │ Interpolator: AccelerateDecelerate
AnimatorTracer: │ CallStack:
AnimatorTracer: │   at com.android.systemui.xxx.yyy(yyy.java:123)
AnimatorTracer: └───────────────────────────────────────────────────────

AnimatorTracer: Anim End | ID: 1 | Obj | ... | alpha | 300ms

8.3 验证 Perfetto Trace

# 录制 10 秒 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
 
# 操作手机触发动画...
 
# 拉取 trace 文件
adb pull /data/misc/perfetto-traces/trace.perfetto-trace

Perfetto UI 中打开:

  1. 找到 com.android.systemui 进程的异步 track
  2. 搜索 "Anim|" → 过滤原生动画 slice
  3. 搜索 "Folme|" → 过滤 Folme 动画 slice
  4. 对比两种动画的时间线和重叠情况
  5. 关联 Log: 点击某个 slice 看到 Anim|42|Obj|...,在 logcat 中执行 grep "ID: 42" 即可找到完整堆栈

8.4 验证关闭时零开销

adb shell setprop debug.animator.trace 0
adb shell kill $(adb shell pidof com.android.systemui)
adb logcat -s AnimatorTracer
# 预期:无任何输出(包括 "enabled" 信息也不应出现)

8.5 边界场景验证

场景操作预期
0 时长动画触发 duration=0 的动画Begin 和 End 日志紧邻出现
无限循环启动 repeatCount=INFINITE 的动画Begin 日志显示 repeat: INFINITE,cancel 时 End 日志出现
AnimatorSet触发包含多个子动画的 AnimatorSet每个子动画独立输出 Begin/End
快速 start/cancel启动后立即 cancelBegin 和 End 都正常输出
多进程过滤debug.animator.trace=com.android.systemui,com.miui.home仅两个进程有输出

九、Git Diff 汇总

 新增: aospframeworks/base/core/java/android/animation/AnimatorTracer.java  (270 行)
 修改: aospframeworks/base/core/java/android/animation/ValueAnimator.java
       - Line 255-257:  +3 行  (mTraceId, mTraceSectionName, mTraceCallStack 字段)
       - Line 1108-1110: +3 行  (start() 中 init + 堆栈捕获)
       - Line 1313:      +1 行  (endAnimation() 中 onAnimationEnd)
       - Line 1344-1346: +3 行  (startAnimation() 中 onAnimationStart)

总计:1 个新文件 + 1 个文件修改(+10 行插桩代码 + 3 行字段声明)。


十、实现细节:逐步复现指南

本节面向在新 AOSP 仓库中复现此功能的 AI 或工程师。提供每一步的精确定位方法、上下文代码和操作指令。

10.1 Step 1: 创建 AnimatorTracer.java

文件路径: frameworks/base/core/java/android/animation/AnimatorTracer.java

为什么放在 android.animation: 需要直接访问以下 package-private 字段:

  • ValueAnimator.mValues (PropertyValuesHolder[])
  • ValueAnimator.mTraceId / mTraceSectionName / mTraceCallStack (新增字段)
  • PropertyValuesHolder.mPropertyName (String)
  • PropertyValuesHolder.mKeyframes (Keyframes)

完整实现 (当前最终版本):

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;
    }
 
    public static String captureCallStack() {
        if (!sEnabled) return null;
        StackTraceElement[] traces = Thread.currentThread().getStackTrace();
        StringBuilder sb = new StringBuilder(512);
        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;
            }
            sb.append("│   at ").append(traces[i]).append('\n');
        }
        return sb.toString();
    }
 
    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);
    }
 
    public static void onAnimationEnd(ValueAnimator animator) {
        if (animator.mTraceSectionName == null) return;
        Trace.endAsyncSection(animator.mTraceSectionName, animator.mTraceId);
        logEnd(animator);
        animator.mTraceSectionName = null;
    }
 
    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();
    }
 
    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) {
        StringBuilder sb = new StringBuilder(128);
        sb.append("Anim End | ID: ").append(animator.mTraceId);
        if (animator instanceof ObjectAnimator) {
            sb.append(" | Obj");
            Object target = ((ObjectAnimator) animator).getTarget();
            if (target != null) {
                sb.append(" | ").append(target.getClass().getSimpleName());
            }
            String propName = ((ObjectAnimator) animator).getPropertyName();
            if (propName != null) {
                sb.append(" | ").append(propName);
            }
        } else {
            sb.append(" | Val");
        }
        sb.append(" | ").append(animator.getDuration()).append("ms");
        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;
    }
 
    private 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);
    }
 
    private static String currentProcessName() {
        try {
            return ActivityThread.currentProcessName();
        } catch (Exception e) {
            return null;
        }
    }
}

10.2 Step 2: 修改 ValueAnimator.java — 定位与操作

文件路径: frameworks/base/core/java/android/animation/ValueAnimator.java

Change A: 新增 3 个字段

定位方法: 搜索 HashMap<String, PropertyValuesHolder> mValuesMap,在其声明行之后插入。

上下文 (修改后的完整代码段):

    /**
     * A hashmap of the PropertyValuesHolder objects. This map is used to lookup animated values
     * by property name during calls to getAnimatedValue(String).
     */
    HashMap<String, PropertyValuesHolder> mValuesMap;
 
    /** @hide */ int mTraceId;
    /** @hide */ String mTraceSectionName;
    /** @hide */ String mTraceCallStack;
 
    /**
     * If set to non-negative value, this will override {@link #sDurationScale}.
     */
    private float mDurationScale = -1f;

字段用途:

  • mTraceId: 全局递增 ID,同时作为 Perfetto async section cookie 和 Logcat 关联 ID
  • mTraceSectionName: 缓存 section name 字符串,endAsyncSection() 必须传入与 beginAsyncSection() 完全相同的 name
  • mTraceCallStack: start() 中捕获的堆栈字符串,startAnimation() 中消费后置 null 释放引用

Change B: start(boolean) 中插桩

定位方法: 搜索 private void start(boolean playBackwards),在 Looper null 检查 (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;
        mSelfPulse = !mSuppressSelfPulseRequested;
        // Special case: reversing from seek-to-0 should act as if not seeked at all.
        if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {

为什么在这里:

  • init() 幂等,volatile 读 sInited 为 true 时直接返回,约 1ns 开销
  • 堆栈必须在此处捕获:此时 Thread.currentThread().getStackTrace() 包含 SystemUI 业务代码调用点。到 startAnimation() 时堆栈已被 Choreographer doFrame() 替换
  • 在 Looper 检查之后:确保已在有 Looper 的线程(通常是 UI 线程)

Change C: startAnimation() 中插桩

定位方法: 搜索 private void startAnimation(),找到 initAnimation(); 这一行,在其之后、mRunning = true 之前插入。

上下文 (修改后的完整代码段):

    private void startAnimation() {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                    System.identityHashCode(this));
        }
 
        mAnimationEndRequested = false;
        initAnimation();
        if (AnimatorTracer.isEnabled()) {
            AnimatorTracer.onAnimationStart(this, mTraceCallStack);
            mTraceCallStack = null;
        }
        mRunning = true;
        if (mSeekFraction >= 0) {
            mOverallFraction = mSeekFraction;
        } else {
            mOverallFraction = 0f;
        }

为什么在 initAnimation() 之后:

  • initAnimation() 调用 PropertyValuesHolder.setupStartValue()setupEndValue(),初始化 mKeyframes
  • 只有在 initAnimation() 执行完毕后,mKeyframes.getValue(0f)getValue(1f) 才能返回正确的 startValue/endValue
  • 如果放在 initAnimation() 之前,buildSectionName() 中获取属性值会得到 null 或错误值

mTraceCallStack = null 的作用:

  • 释放 String 引用,避免长期持有(动画对象可能被复用)
  • 明确表示堆栈信息已消费完毕

Change D: endAnimation() 中插桩

定位方法: 搜索 private void endAnimation()(注意不是 completeEndAnimation),找到 notifyEndListenersFromEndAnimation(mReversing, postNotifyEndListener); 这一行,在其之后、原有 if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) 之前插入。

上下文 (修改后的完整代码段):

        mLastFrameTime = -1;
        mFirstFrameTime = -1;
        mStartTime = -1;
        // If postNotifyEndListener is false (most cases), then it is the same as calling
        // completeEndAnimation directly.
        notifyEndListenersFromEndAnimation(mReversing, postNotifyEndListener);
        AnimatorTracer.onAnimationEnd(this);
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                    System.identityHashCode(this));
        }
    }

为什么不需要 isEnabled() 守卫:

  • onAnimationEnd() 内部以 animator.mTraceSectionName != null 为守卫
  • 只有曾经调用过 onAnimationStart() 的动画才会有非 null 的 mTraceSectionName
  • 即使追踪被中途关闭(进程内修改 prop),已开始的动画仍能正确 endAsyncSection

10.3 关键设计点总结

设计决策选择原因
ID 放入 section name"Anim|42|Obj|..."Perfetto UI 直接可见 ID,无需对照时间戳去 logcat 找日志
堆栈桥接字段mTraceCallStackstart() 在调用方线程(有业务堆栈),startAnimation() 在 Choreographer 回调中(堆栈已被替换)
initAnimation() 后输出确保 mValues 已初始化否则 mKeyframes.getValue() 返回 null,无法显示属性起止值
AtomicInteger 生成 ID即使多线程也保证唯一虽然当前仅 UI 线程使用,但 AtomicInteger 无额外开销且更安全
endAsyncSection 使用缓存的 sectionNamePerfetto 要求 begin/end name 完全相同动画运行期间 target 状态可能变化(被 GC、属性改变),重新 build 可能不匹配
Interpolator 名称裁剪 “Interpolator” 后缀节省 section name 长度AccelerateDecelerateInterpolatorAccelerateDecelerate,200 字符限制内信息更密
颜色值用 0xAARRGGBB 格式整数高位有值时自动识别为颜色-16777216 可读性高得多
属性最多显示 3 个防止 section name 超长超出部分显示 +N,详细信息在 logcat 中完整显示

十一、FolmeTracer:miuix 库的修改

仓库: /home/zbc/micode/miuix(MiuiX 组件库)
模块: library/folme
设计文档: library/folme/FOLME_TRACER_GUIDE.md

11.1 背景

AnimatorTracer 追踪 Android 原生 Animator,而 SystemUI 中另一套动画体系 —— Folme(状态驱动的弹簧物理动画)—— 需要独立的追踪方案。FolmeTracer 作为 Folme 库的内部模块,随 folme AAR 发布。

11.2 设计思路

与 AnimatorTracer 完全对称的设计,区别在于适配 Folme 引擎的内部数据流:

维度AnimatorTracer (AOSP)FolmeTracer (miuix)
堆栈捕获时机ValueAnimator.start()FolmeEngine.fromTo()
信息输出时机startAnimation()initAnimation()AnimManager.onStart()
结束追踪时机endAnimation()AnimManager.onEnd() / onReplaced()
堆栈桥接载体ValueAnimator.mTraceCallStack 字段TransitionInfo.traceCallStack 字段
Section name 缓存ValueAnimator.mTraceSectionNameTransitionInfo.traceSectionName
ID 来源AtomicInteger 全局递增TransitionInfo.id(已有的全局递增 ID)

核心数据流:

SystemUI 调用:
  Folme.use(view).state().to(pressed, config)
    │
    ▼
FolmeEngine.fromTo()                    ← 【1. 捕获调用堆栈 → info.traceCallStack】
  ├── new TransitionInfo(target, from, to, config)
  ├── info.traceCallStack = FolmeTracer.captureCallStack()
  └── toAnim(info)
         │
         ▼ (Choreographer 帧回调)
AnimManager.onStart()                   ← 【2. Trace.beginAsyncSection + 详细 Log】
    │                                        (此时 updateList 已填充,属性值可用)
    ▼ (动画执行...)
AnimManager.onEnd() / onReplaced()      ← 【3. Trace.endAsyncSection + 简短 Log】

11.3 修改文件清单

文件操作改动
library/folme/src/main/java/miuix/animation/internal/FolmeTracer.java新增 (229行)核心追踪类
library/folme/src/main/java/miuix/animation/internal/TransitionInfo.java修改 (+3行)新增 2 个字段
library/folme/src/main/java/miuix/animation/internal/FolmeEngine.java修改 (+1行)捕获堆栈
library/folme/src/main/java/miuix/animation/internal/AnimManager.java修改 (+3行)3 个插桩点
library/folme/src/main/java/miuix/animation/Folme.java修改 (+2行)初始化

11.4 逐步实现细节

Change 1: 新增 FolmeTracer.java

路径: library/folme/src/main/java/miuix/animation/internal/FolmeTracer.java

为什么放在 miuix.animation.internal:

  • 需要直接访问 TransitionInfo(package-private 类)
  • 需要访问 UpdateInfo.animInfo.startValue/targetValue(内部字段)
  • AnimManager 调用 FolmeTracer 无需跨包
  • 对缓动名称自建映射表 getEaseName(),不依赖跨包的 FolmeEase

完整源码:

package miuix.animation.internal;
 
import android.os.Trace;
import android.util.Log;
 
import java.util.Arrays;
 
import miuix.animation.base.AnimConfig;
import miuix.animation.listener.UpdateInfo;
import miuix.animation.property.FloatProperty;
import miuix.animation.utils.CommonUtils;
import miuix.animation.utils.EaseManager;
 
public class FolmeTracer {
 
    private static final String TAG = "FolmeTracer";
    private static volatile boolean sEnabled;
    private static volatile boolean sInited;
 
    public static void init() {
        if (sInited) return;
        sInited = true;
        try {
            String val = CommonUtils.readProp("debug.folme.trace");
            sEnabled = "1".equals(val) || "true".equalsIgnoreCase(val);
        } catch (Exception e) {
            sEnabled = false;
        }
        if (sEnabled) {
            Log.i(TAG, "FolmeTracer enabled via debug.folme.trace");
        }
    }
 
    public static boolean isEnabled() {
        return sEnabled;
    }
 
    public static String captureCallStack() {
        if (!sEnabled) return null;
        StackTraceElement[] traces = Thread.currentThread().getStackTrace();
        StringBuilder sb = new StringBuilder(512);
        for (int i = 3; i < Math.min(traces.length, 18); i++) {
            sb.append("│   at ").append(traces[i]).append('\n');
        }
        return sb.toString();
    }
 
    public static void beginTransition(TransitionInfo info) {
        if (!sEnabled) return;
        String sectionName = buildSectionName(info);
        info.traceSectionName = sectionName;
        Trace.beginAsyncSection(sectionName, info.id);
        logBegin(info);
    }
 
    public static void endTransition(TransitionInfo info, String reason) {
        if (!sEnabled || info.traceSectionName == null) return;
        Trace.endAsyncSection(info.traceSectionName, info.id);
        Log.i(TAG, "Folme Animation End   | ID: " + info.id
                + " | Tag: " + info.tag
                + " | reason: " + reason);
        info.traceSectionName = null;
    }
 
    static String buildSectionName(TransitionInfo info) {
        StringBuilder sb = new StringBuilder(128);
        sb.append("Folme|");
        sb.append(info.id);          // ← ID 直接嵌入 section name
        sb.append('|');
        sb.append(info.tag);
        sb.append('|');
        sb.append(info.target.toString());
        sb.append('|');
        appendProps(sb, info);
        sb.append('|');
        appendEase(sb, info.config.ease);
        return sb.toString();
    }
 
    private static void appendProps(StringBuilder sb, TransitionInfo info) {
        boolean first = true;
        if (info.updateList != null && !info.updateList.isEmpty()) {
            for (UpdateInfo ui : info.updateList) {
                if (!first) sb.append(',');
                sb.append(ui.property.getName());
                first = false;
            }
        } else {
            for (Object key : info.to.keySet()) {
                if (!first) sb.append(',');
                if (key instanceof FloatProperty) {
                    sb.append(((FloatProperty) key).getName());
                } else {
                    sb.append(key);
                }
                first = false;
            }
        }
    }
 
    private static void appendEase(StringBuilder sb, EaseManager.EaseStyle ease) {
        if (ease == null) {
            sb.append("default");
            return;
        }
        sb.append(getEaseName(ease.style));
        if (ease.factors != null && ease.factors.length > 0) {
            sb.append('(');
            for (int i = 0; i < ease.factors.length; i++) {
                if (i > 0) sb.append(',');
                double f = ease.factors[i];
                if (f == (long) f) {
                    sb.append((long) f);
                } else {
                    sb.append((float) f);
                }
            }
            sb.append(')');
        }
    }
 
    private static String getEaseName(int style) {
        switch (style) {
            case EaseManager.EaseStyleDef.SPRING_PHY: return "spring_phy";
            case EaseManager.EaseStyleDef.FRICTION:   return "friction";
            case EaseManager.EaseStyleDef.ACCELERATE: return "accelerate";
            case EaseManager.EaseStyleDef.REBOUND:    return "rebound";
            case EaseManager.EaseStyleDef.STOP:       return "stop";
            case EaseManager.EaseStyleDef.DURATION:   return "duration";
            case EaseManager.EaseStyleDef.SPRING:     return "spring";
            case EaseManager.EaseStyleDef.LINEAR:     return "linear";
            case EaseManager.EaseStyleDef.CUBIC_IN:   return "cubic_in";
            case EaseManager.EaseStyleDef.CUBIC_OUT:  return "cubic_out";
            case EaseManager.EaseStyleDef.CUBIC_INOUT:return "cubic_inout";
            case EaseManager.EaseStyleDef.QUAD_IN:    return "quad_in";
            case EaseManager.EaseStyleDef.QUAD_OUT:   return "quad_out";
            case EaseManager.EaseStyleDef.QUAD_INOUT: return "quad_inout";
            case EaseManager.EaseStyleDef.QUART_IN:   return "quart_in";
            case EaseManager.EaseStyleDef.QUART_OUT:  return "quart_out";
            case EaseManager.EaseStyleDef.QUART_INOUT:return "quart_inout";
            case EaseManager.EaseStyleDef.QUINT_IN:   return "quint_in";
            case EaseManager.EaseStyleDef.QUINT_OUT:  return "quint_out";
            case EaseManager.EaseStyleDef.QUINT_INOUT:return "quint_inout";
            case EaseManager.EaseStyleDef.SINE_IN:    return "sine_in";
            case EaseManager.EaseStyleDef.SINE_OUT:   return "sine_out";
            case EaseManager.EaseStyleDef.SINE_INOUT: return "sine_inout";
            case EaseManager.EaseStyleDef.EXPO_IN:    return "expo_in";
            case EaseManager.EaseStyleDef.EXPO_OUT:   return "expo_out";
            case EaseManager.EaseStyleDef.EXPO_INOUT: return "expo_inout";
            case EaseManager.EaseStyleDef.DECELERATE: return "decelerate";
            case EaseManager.EaseStyleDef.ACCELERATE_DECELERATE: return "accel_decel";
            case EaseManager.EaseStyleDef.ACCELERATE_INTERPOLATOR: return "accel_interp";
            case EaseManager.EaseStyleDef.BOUNCE:     return "bounce";
            case EaseManager.EaseStyleDef.BOUNCE_EASE_IN:    return "bounce_in";
            case EaseManager.EaseStyleDef.BOUNCE_EASE_OUT:   return "bounce_out";
            case EaseManager.EaseStyleDef.BOUNCE_EASE_INOUT: return "bounce_inout";
            case EaseManager.EaseStyleDef.BEZIER:     return "bezier";
            case EaseManager.EaseStyleDef.SPRING_GRAVITY:    return "spring_gravity";
            case EaseManager.EaseStyleDef.SPRING_FUNCTION:   return "spring_func";
            case EaseManager.EaseStyleDef.DAMPING:    return "damping";
            case EaseManager.EaseStyleDef.PERLIN:     return "perlin";
            case EaseManager.EaseStyleDef.PERLIN2:    return "perlin2";
            default: return "ease_" + style;
        }
    }
 
    private static void logBegin(TransitionInfo info) {
        StringBuilder sb = new StringBuilder(512);
        sb.append("┌── Folme Animation Begin ──────────────────────────\n");
        sb.append("│ ID: ").append(info.id).append('\n');
        sb.append("│ Tag: ").append(info.tag).append('\n');
        sb.append("│ Target: ").append(info.target.toString()).append('\n');
 
        sb.append("│ Properties:\n");
        if (info.updateList != null && !info.updateList.isEmpty()) {
            for (UpdateInfo ui : info.updateList) {
                sb.append("│   ").append(ui.property.getName())
                  .append(": ").append(formatValue(ui, ui.animInfo.startValue))
                  .append(" → ").append(formatValue(ui, ui.animInfo.targetValue))
                  .append('\n');
            }
        } else {
            for (Object key : info.to.keySet()) {
                String name = key instanceof FloatProperty
                        ? ((FloatProperty) key).getName() : key.toString();
                sb.append("│   ").append(name).append('\n');
            }
        }
 
        AnimConfig config = info.config;
        sb.append("│ Ease: ");
        if (config.ease != null) {
            sb.append(getEaseName(config.ease.style));
            if (config.ease.factors != null) {
                sb.append(", factors=").append(Arrays.toString(config.ease.factors));
            }
        } else {
            sb.append("default (spring_phy, 0.95, 0.35)");
        }
        sb.append('\n');
 
        sb.append("│ Delay: ").append(config.delay).append("ms\n");
 
        if (info.from != null) {
            sb.append("│ From: ").append(info.from.getTag()).append('\n');
        }
 
        if (info.traceCallStack != null) {
            sb.append("│ CallStack:\n");
            sb.append(info.traceCallStack);
        }
 
        sb.append("└──────────────────────────────────────────────────");
        Log.i(TAG, sb.toString());
    }
 
    private static String formatValue(UpdateInfo ui, double value) {
        if (ui.useInt) {
            return "0x" + Integer.toHexString((int) value) + "(" + (int) value + ")";
        }
        if (value == Double.MAX_VALUE) {
            return "MAX";
        }
        if (value == (long) value) {
            return String.valueOf((long) value);
        }
        return String.valueOf((float) value);
    }
}

Change 2: 修改 TransitionInfo.java — 新增 2 个字段

定位方法: 搜索 public List<AnimTask> animTasks,在其声明之后插入。

Diff:

     public List<AnimTask> animTasks = new ArrayList<>();
 
+    public String traceSectionName;
+    public String traceCallStack;
+
     private final AnimStats mInfoAnimStats = new AnimStats();

字段用途:

  • traceSectionName: 缓存 section name,保证 beginAsyncSectionendAsyncSection 使用完全相同的字符串
  • traceCallStack: 在 FolmeEngine.fromTo() 捕获堆栈后暂存,AnimManager.onStart() 中消费输出到 log

Change 3: 修改 FolmeEngine.java — 捕获调用堆栈

定位方法: 搜索 final TransitionInfo info = new TransitionInfo(target, from, to, config);,在此行之后立即插入。

Diff:

     final TransitionInfo info = new TransitionInfo(target, from, to, config);
+    info.traceCallStack = FolmeTracer.captureCallStack();
     if (LogUtils.isLogMainEnabled()) {

为什么在这里:

  • fromTo() 运行在调用方线程(SystemUI 主线程),Thread.getStackTrace() 能完整追溯到 Folme.use(view).state().to(...) 的调用位置
  • 后续 AnimManager.onStart() 被 Choreographer 帧调度到 target thread 时,原始堆栈已丢失

Change 4: 修改 AnimManager.java — 3 个插桩点

定位方法 & Diff:

4a. onStart() — 在 info.hasOnStart = true 之后:

         info.hasOnStart = true;
+        FolmeTracer.beginTransition(info);
         info.updateListForNotify.clear();

为什么在这里: 此时 info.updateList 已填充完毕,每个属性的 startValue / targetValue 可用。

4b. onEnd() — 方法开头:

     void onEnd(TransitionInfo info, int reason) {
+        FolmeTracer.endTransition(info, reason == AnimTask.OP_CANCEL ? "cancel" : "complete");
         boolean enableLogMain = LogUtils.isLogMainEnabled();

4c. onReplaced() — 方法开头:

     void onReplaced(TransitionInfo info) {
+        FolmeTracer.endTransition(info, "replaced");
         if (LogUtils.isLogMainEnabled()) {

onReplaced() 场景:新动画覆盖旧动画时触发(如快速连续按压),旧动画以 “replaced” 原因结束。

Change 5: 修改 Folme.java — 初始化

定位方法: 搜索 LogUtils.getLogEnableInfo();,在其之后插入。

Diff:

+import miuix.animation.internal.FolmeTracer;
 import miuix.animation.internal.TargetHandler;
             public void run() {
                 LogUtils.getLogEnableInfo();
+                FolmeTracer.init();
             }

为什么和 LogUtils 一起: 两者都需要读取 system property,放在同一个后台线程避免阻塞主线程。

11.5 输出格式

Perfetto Section Name: "Folme|{id}|{tag}|{target}|{props}|{ease}"

示例:

Folme|42|pressed|View{7f0a0123 app:id/btn_ok/Button}|SCALE_X,SCALE_Y,ALPHA|spring_phy(0.8,0.6)

Logcat Begin:

FolmeTracer: ┌── Folme Animation Begin ──────────────────────────
FolmeTracer: │ ID: 42
FolmeTracer: │ Tag: pressed
FolmeTracer: │ Target: View{7f0a0123 app:id/btn_ok/android.widget.Button}
FolmeTracer: │ Properties:
FolmeTracer: │   SCALE_X: 1.0 → 0.95
FolmeTracer: │   SCALE_Y: 1.0 → 0.95
FolmeTracer: │   ALPHA: 1.0 → 0.8
FolmeTracer: │ Ease: spring_phy, factors=[0.8, 0.6]
FolmeTracer: │ Delay: 0ms
FolmeTracer: │ CallStack:
FolmeTracer: │   at com.android.systemui.controlcenter.shade.XXXController.animatePress(XXXController.kt:156)
FolmeTracer: │   at com.android.systemui.controlcenter.shade.XXXController.onTouchEvent(XXXController.kt:89)
FolmeTracer: └──────────────────────────────────────────────────

Logcat End:

FolmeTracer: Folme Animation End   | ID: 42 | Tag: pressed | reason: complete

11.6 与 AnimatorTracer 的联合使用

# 同时开启两个追踪
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
 
# Perfetto 中:
#   搜索 "Folme|" → Folme 动画 slice
#   搜索 "Anim|"  → 原生 Animator slice
#   两者出现在同一进程的异步 track 上,可直观对比时间线

11.7 设计文档引用

详细设计方案见 miuix 仓库内的设计文档:

  • /home/zbc/micode/miuix/library/folme/FOLME_TRACER_GUIDE.md — 完整的技术方案文档,包含背景、控制开关、Section 格式、Log 格式、数据流、文件清单、可见性说明、验证方法、性能影响