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 条 import | FolmeTracer 已覆盖(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 风格统一的追踪能力:
- Perfetto Trace: 丰富的异步 section 名称
"Anim|42|Obj|Button@3f2c|alpha:1.0->0.0|AccelDecel|300ms"(包含 ID,方便与 Logcat 对应) - Logcat Log: 结构化框线格式,包含完整调用堆栈,一步定位触发动画的 SystemUI 代码行
- 零侵入: 通过 system property 控制,关闭时接近零开销
- 全覆盖: 自动追踪 ValueAnimator、ObjectAnimator、AnimatorSet 子动画、ViewPropertyAnimator 内部动画
二、设计思路
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() (原有)
关键设计决策:
-
堆栈在
start()捕获:此时仍运行在调用方线程(SystemUI 主线程),Thread.currentThread().getStackTrace()能追溯到Folme.use(view).state().to(...)或ObjectAnimator.start()的具体调用位置。到startAnimation()时堆栈已被 Choreographer 帧调度替换。 -
Trace + Log 在
startAnimation()的initAnimation()之后输出:initAnimation()负责初始化PropertyValuesHolder的 keyframes,只有在它执行完之后mKeyframes.getValue(0f)/getValue(1f)才能返回正确的 startValue/endValue。 -
堆栈通过字段桥接:
mTraceCallStack字段在start()中写入,在startAnimation()中读取后置 null 释放引用。
2.3 Trace 与 Log 的 ID 关联
问题: Perfetto UI 中看到一个动画 slice,想查看它的完整堆栈信息,需要在 logcat 中找到对应日志。如果 Trace section name 中没有 ID,只能靠时间戳和属性名模糊匹配,效率低下。
方案: 将全局递增的 traceId 同时写入:
- Section name:
"Anim|42|Obj|View@ab12|alpha:1->0|AccelDecel|300ms"— Perfetto UI 直接可见 - Logcat Begin:
"│ ID: 42"— 结构化日志中的第一行信息 - Logcat End:
"Anim End | ID: 42 | ..."— 结束日志也带 ID
使用流程:
- 在 Perfetto UI 看到
Anim|42|...slice - 在 logcat 中搜索
"ID: 42"→ 立即找到完整堆栈和属性详情 - 反之亦然: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 个新字段(
mTraceId、mTraceSectionName、mTraceCallStack)仅 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.java | AnimatorTracer 直接访问其 package-private 字段 mPropertyName、mKeyframes(同包可访问) |
| 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,不阻塞动画线程 |
| 开启时 — Trace | beginAsyncSection / 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>"
各字段说明:
| 段 | ObjectAnimator | ValueAnimator |
|---|---|---|
| 前缀 | Anim| | Anim| |
| ID | 42(与 Logcat 中 │ ID: 42 一致) | 同左 |
| type | Obj|ClassName@hexHash | Val |
| properties | alpha:1.0->0.0 | ?:0.0->1.0 |
| interpolator | AccelDecel / Path / Linear | 同左 |
| duration | 300ms | 200ms |
完整示例:
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 的对比
| 维度 | FolmeTracer | AnimatorTracer |
|---|---|---|
| 追踪目标 | Folme 动画(Folme.use(view).state().to(...)) | Android 原生 Animator |
| Property 控制 | debug.folme.trace | debug.animator.trace |
| Section 前缀 | Folme| | Anim| |
| Section 中带 ID | 是(Folme|42|...) | 是(Anim|42|...) |
| Log Tag | FolmeTracer | AnimatorTracer |
| 实现位置 | 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/core7.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 中打开:
- 找到
com.android.systemui进程的异步 track - 搜索
"Anim|"→ 过滤原生动画 slice - 搜索
"Folme|"→ 过滤 Folme 动画 slice - 对比两种动画的时间线和重叠情况
- 关联 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 | 启动后立即 cancel | Begin 和 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 关联 IDmTraceSectionName: 缓存 section name 字符串,endAsyncSection()必须传入与beginAsyncSection()完全相同的 namemTraceCallStack: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()时堆栈已被 ChoreographerdoFrame()替换 - 在 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 找日志 |
| 堆栈桥接字段 | mTraceCallStack | start() 在调用方线程(有业务堆栈),startAnimation() 在 Choreographer 回调中(堆栈已被替换) |
initAnimation() 后输出 | 确保 mValues 已初始化 | 否则 mKeyframes.getValue() 返回 null,无法显示属性起止值 |
| AtomicInteger 生成 ID | 即使多线程也保证唯一 | 虽然当前仅 UI 线程使用,但 AtomicInteger 无额外开销且更安全 |
endAsyncSection 使用缓存的 sectionName | Perfetto 要求 begin/end name 完全相同 | 动画运行期间 target 状态可能变化(被 GC、属性改变),重新 build 可能不匹配 |
| Interpolator 名称裁剪 “Interpolator” 后缀 | 节省 section name 长度 | AccelerateDecelerateInterpolator → AccelerateDecelerate,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.mTraceSectionName | TransitionInfo.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,保证beginAsyncSection和endAsyncSection使用完全相同的字符串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 格式、数据流、文件清单、可见性说明、验证方法、性能影响