
动画帧率稳定性测试工具设计与实现 原创
动画帧率稳定性测试工具设计与实现
一、项目概述
基于HarmonyOS的动画帧率稳定性测试工具,能够实时监测动画渲染性能并分析帧率波动情况。借鉴《鸿蒙跨端U同步》中的帧同步机制,实现多设备间动画性能数据采集与对比分析,帮助开发者优化动画性能。
二、架构设计
±--------------------+
动画渲染引擎
(Animation Engine)
±---------±---------+
±---------v----------+ ±--------------------+
帧率监测器 <—> 分布式数据同步
(Frame Monitor) (Data Sync)
±---------±---------+ ±--------------------+
±---------v----------+
数据分析可视化
(Data Visualization)
±--------------------+
三、核心代码实现
帧率监测服务
// 帧率监测服务
public class FrameRateMonitor {
private static final String TAG = “FrameRateMonitor”;
private long lastFrameTime;
private List<Long> frameIntervals = new ArrayList<>();
private FrameRateListener listener;
private boolean isMonitoring;
// 开始监测
public void startMonitoring(FrameRateListener listener) {
this.listener = listener;
this.isMonitoring = true;
this.lastFrameTime = System.nanoTime();
// 启动帧率计算定时器
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
calculateFrameRate();
}, 1000, 1000);
// 停止监测
public void stopMonitoring() {
this.isMonitoring = false;
// 记录一帧
public void recordFrame() {
if (!isMonitoring) return;
long currentTime = System.nanoTime();
long interval = currentTime - lastFrameTime;
lastFrameTime = currentTime;
synchronized (frameIntervals) {
frameIntervals.add(interval);
}
// 计算帧率
private void calculateFrameRate() {
List<Long> intervalsCopy;
synchronized (frameIntervals) {
intervalsCopy = new ArrayList<>(frameIntervals);
frameIntervals.clear();
if (intervalsCopy.isEmpty()) return;
// 计算平均帧间隔(纳秒)
long total = 0;
for (long interval : intervalsCopy) {
total += interval;
long avgInterval = total / intervalsCopy.size();
// 转换为FPS
double fps = 1_000_000_000.0 / avgInterval;
// 计算帧率稳定性(Jank百分比)
double jankPercent = calculateJankPercent(intervalsCopy, avgInterval);
if (listener != null) {
listener.onFrameRateUpdate(fps, jankPercent);
}
// 计算卡顿百分比
private double calculateJankPercent(List<Long> intervals, long avgInterval) {
int jankCount = 0;
long jankThreshold = (long)(avgInterval * 1.5); // 超过平均1.5倍视为卡顿
for (long interval : intervals) {
if (interval > jankThreshold) {
jankCount++;
}
return (jankCount * 100.0) / intervals.size();
public interface FrameRateListener {
void onFrameRateUpdate(double fps, double jankPercent);
}
测试动画实现
// 测试动画AbilitySlice
public class AnimationTestSlice extends AbilitySlice
implements FrameRateMonitor.FrameRateListener {
private XComponent xComponent;
private FrameRateMonitor frameRateMonitor;
private Text fpsText, jankText;
private AnimationTimer animationTimer;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
initUI();
startTest();
private void initUI() {
DirectionalLayout layout = new DirectionalLayout(this);
layout.setOrientation(Component.VERTICAL);
// 动画渲染区域
xComponent = new XComponent(this);
xComponent.setWidth(ComponentContainer.LayoutConfig.MATCH_PARENT);
xComponent.setHeight(0);
xComponent.setWeight(1);
xComponent.setDrawer(new AnimationRenderer());
layout.addComponent(xComponent);
// 帧率显示区域
DirectionalLayout infoPanel = new DirectionalLayout(this);
infoPanel.setWidth(ComponentContainer.LayoutConfig.MATCH_PARENT);
infoPanel.setHeight(150);
infoPanel.setPadding(20, 20, 20, 20);
infoPanel.setBackground(new ShapeElement().setShape(ShapeElement.RECTANGLE)
.setRgbColor(RgbColor.fromArgbInt(0xEE000000)));
fpsText = new Text(this);
fpsText.setTextSize(30);
fpsText.setTextColor(Color.WHITE);
infoPanel.addComponent(fpsText);
jankText = new Text(this);
jankText.setTextSize(30);
jankText.setTextColor(Color.WHITE);
jankText.setMarginLeft(50);
infoPanel.addComponent(jankText);
layout.addComponent(infoPanel);
setUIContent(layout);
private void startTest() {
// 初始化帧率监测
frameRateMonitor = new FrameRateMonitor();
frameRateMonitor.startMonitoring(this);
// 启动动画
animationTimer = new AnimationTimer();
animationTimer.start();
// 实现FrameRateListener
@Override
public void onFrameRateUpdate(double fps, double jankPercent) {
getUITaskDispatcher().asyncDispatch(() -> {
fpsText.setText(String.format("FPS: %.1f", fps));
jankText.setText(String.format("卡顿: %.1f%%", jankPercent));
});
// 动画渲染器
private class AnimationRenderer implements XComponent.Drawer {
private float rotation = 0;
@Override
public void onSurfaceCreated(XComponent component, int width, int height) {
// OpenGL初始化
GLES30.glClearColor(0.1f, 0.1f, 0.2f, 1.0f);
@Override
public void onSurfaceChanged(XComponent component, int width, int height) {
GLES30.glViewport(0, 0, width, height);
@Override
public void onDrawFrame(XComponent component) {
// 记录帧
frameRateMonitor.recordFrame();
// 清除画布
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
// 简单的旋转动画
rotation += 1.0f;
if (rotation >= 360) {
rotation = 0;
// 绘制旋转的三角形
drawRotatingTriangle(rotation);
private void drawRotatingTriangle(float angle) {
// 简化的OpenGL绘制代码
float[] vertices = {
0.0f, 0.5f, 0.0f, // 顶点
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f // 右下
};
// 创建顶点缓冲
FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertices);
vertexBuffer.position(0);
// 创建并编译着色器程序
int program = GLESUtils.createProgram(
GLESUtils.loadShader(GLES30.GL_VERTEX_SHADER, R.raw.vertex_shader),
GLESUtils.loadShader(GLES30.GL_FRAGMENT_SHADER, R.raw.fragment_shader)
);
// 使用着色器程序
GLES30.glUseProgram(program);
// 获取顶点属性位置
int positionHandle = GLES30.glGetAttribLocation(program, "vPosition");
// 启用顶点属性
GLES30.glEnableVertexAttribArray(positionHandle);
// 准备顶点坐标数据
GLES30.glVertexAttribPointer(
positionHandle, 3,
GLES30.GL_FLOAT, false,
12, vertexBuffer);
// 获取旋转矩阵uniform
int rotationHandle = GLES30.glGetUniformLocation(program, "uRotation");
// 创建旋转矩阵
float[] rotationMatrix = new float[16];
Matrix.setRotateM(rotationMatrix, 0, angle, 0, 0, 1.0f);
// 传递旋转矩阵
GLES30.glUniformMatrix4fv(rotationHandle, 1, false, rotationMatrix, 0);
// 绘制三角形
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 3);
// 禁用顶点数组
GLES30.glDisableVertexAttribArray(positionHandle);
}
// 动画定时器
private class AnimationTimer extends Timer {
public void start() {
scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
xComponent.invalidate(); // 触发重绘
}, 0, 16); // 约60FPS
}
多设备数据同步服务
// 帧率数据同步服务
public class FrameDataSyncService {
private static final String SYNC_CHANNEL = “frame_data_sync”;
private DistributedDataManager dataManager;
private String sessionId;
private FrameDataListener listener;
public FrameDataSyncService(Context context) {
this.dataManager = DistributedDataManagerFactory.getInstance()
.createDistributedDataManager(context);
// 创建同步会话
public void createSyncSession(String sessionId) {
this.sessionId = sessionId;
// 注册数据接收监听
dataManager.registerDataChangeListener(
SYNC_CHANNEL + "_" + sessionId,
new DataChangeListener() {
@Override
public void onDataChanged(String deviceId, String key, String value) {
FrameData data = FrameData.fromJson(value);
if (listener != null) {
listener.onFrameDataReceived(deviceId, data);
}
);
// 发送帧率数据
public void sendFrameData(FrameData data) {
dataManager.putString(
SYNC_CHANNEL + "_" + sessionId,
data.toJson()
);
// 帧率数据类
public static class FrameData {
public double fps;
public double jankPercent;
public long timestamp;
public String toJson() {
JSONObject json = new JSONObject();
try {
json.put("fps", fps);
json.put("jankPercent", jankPercent);
json.put("timestamp", timestamp);
catch (JSONException e) {
return "{}";
return json.toString();
public static FrameData fromJson(String jsonStr) {
try {
JSONObject json = new JSONObject(jsonStr);
FrameData data = new FrameData();
data.fps = json.getDouble("fps");
data.jankPercent = json.getDouble("jankPercent");
data.timestamp = json.getLong("timestamp");
return data;
catch (JSONException e) {
return null;
}
public interface FrameDataListener {
void onFrameDataReceived(String deviceId, FrameData data);
}
数据可视化实现
// 帧率对比分析AbilitySlice
public class FrameAnalysisSlice extends AbilitySlice
implements FrameDataSyncService.FrameDataListener {
private LineChartView fpsChart;
private LineChartView jankChart;
private Map<String, List<FrameData>> deviceDataMap = new HashMap<>();
@Override
public void onStart(Intent intent) {
super.onStart(intent);
setUIContent(ResourceTable.Layout_frame_analysis_layout);
// 初始化图表
fpsChart = (LineChartView) findComponentById(ResourceTable.Id_fps_chart);
jankChart = (LineChartView) findComponentById(ResourceTable.Id_jank_chart);
// 启动数据同步
String sessionId = intent.getStringParam("sessionId");
FrameDataSyncService syncService = new FrameDataSyncService(this);
syncService.createSyncSession(sessionId);
syncService.setFrameDataListener(this);
// 实现FrameDataListener
@Override
public void onFrameDataReceived(String deviceId, FrameDataSyncService.FrameData data) {
getUITaskDispatcher().asyncDispatch(() -> {
// 存储设备数据
List<FrameData> deviceData = deviceDataMap.get(deviceId);
if (deviceData == null) {
deviceData = new ArrayList<>();
deviceDataMap.put(deviceId, deviceData);
deviceData.add(data);
// 更新图表
updateCharts();
});
private void updateCharts() {
// 准备FPS图表数据
List<LineChartView.DataSet> fpsDataSets = new ArrayList<>();
List<LineChartView.DataSet> jankDataSets = new ArrayList<>();
for (Map.Entry<String, List<FrameData>> entry : deviceDataMap.entrySet()) {
String deviceName = DeviceInfo.getDeviceName(entry.getKey());
List<FrameData> dataList = entry.getValue();
// FPS数据
LineChartView.DataSet fpsDataSet = new LineChartView.DataSet();
fpsDataSet.setLabel(deviceName);
fpsDataSet.setColor(getDeviceColor(entry.getKey()));
// 卡顿数据
LineChartView.DataSet jankDataSet = new LineChartView.DataSet();
jankDataSet.setLabel(deviceName);
jankDataSet.setColor(getDeviceColor(entry.getKey()));
// 添加数据点
for (FrameData data : dataList) {
fpsDataSet.addValue(data.fps);
jankDataSet.addValue(data.jankPercent);
fpsDataSets.add(fpsDataSet);
jankDataSets.add(jankDataSet);
// 设置图表数据
fpsChart.setDataSets(fpsDataSets);
fpsChart.setYAxisLabel("FPS");
fpsChart.setMaxValue(60);
jankChart.setDataSets(jankDataSets);
jankChart.setYAxisLabel("卡顿(%)");
jankChart.setMaxValue(100);
// 刷新图表
fpsChart.invalidate();
jankChart.invalidate();
private int getDeviceColor(String deviceId) {
// 为不同设备分配不同颜色
int hash = deviceId.hashCode();
return Color.rgb(
(hash & 0xFF0000) >> 16,
(hash & 0x00FF00) >> 8,
hash & 0x0000FF
);
}
四、XML布局示例
<!-- 动画测试布局 animation_test_layout.xml -->
<DirectionalLayout
xmlns:ohos=“http://schemas.huawei.com/res/ohos”
ohos:width=“match_parent”
ohos:height=“match_parent”
ohos:orientation=“vertical”>
<XComponent
ohos:id="$+id/xcomponent"
ohos:width="match_parent"
ohos:height="0vp"
ohos:weight="1"/>
<DirectionalLayout
ohos:width="match_parent"
ohos:height="150vp"
ohos:orientation="horizontal"
ohos:background_element="#EE000000"
ohos:padding="20vp">
<Text
ohos:id="$+id/fps_text"
ohos:width="0vp"
ohos:height="match_parent"
ohos:weight="1"
ohos:text_size="30fp"
ohos:text_color="#FFFFFF"/>
<Text
ohos:id="$+id/jank_text"
ohos:width="0vp"
ohos:height="match_parent"
ohos:weight="1"
ohos:text_size="30fp"
ohos:text_color="#FFFFFF"
ohos:margin_left="50vp"/>
</DirectionalLayout>
</DirectionalLayout>
<!-- 帧率分析布局 frame_analysis_layout.xml -->
<DirectionalLayout
xmlns:ohos=“http://schemas.huawei.com/res/ohos”
ohos:width=“match_parent”
ohos:height=“match_parent”
ohos:orientation=“vertical”
ohos:padding=“16vp”>
<Text
ohos:width="match_parent"
ohos:height="wrap_content"
ohos:text="多设备帧率对比"
ohos:text_size="32fp"
ohos:margin_bottom="16vp"/>
<com.example.testtool.LineChartView
ohos:id="$+id/fps_chart"
ohos:width="match_parent"
ohos:height="300vp"
ohos:margin_bottom="16vp"/>
<com.example.testtool.LineChartView
ohos:id="$+id/jank_chart"
ohos:width="match_parent"
ohos:height="300vp"/>
</DirectionalLayout>
五、技术创新点
精准帧率监测:纳秒级精度测量帧间隔时间
卡顿分析:智能识别并量化动画卡顿情况
多设备对比:跨设备同步帧率数据并可视化对比
实时反馈:测试过程中实时显示性能指标
OpenGL集成:基于XComponent实现高性能动画渲染
六、总结
本动画帧率稳定性测试工具实现了以下核心价值:
性能可视化:直观展示动画渲染性能指标
问题定位:快速发现帧率波动和卡顿问题
多设备对比:比较不同设备的动画性能差异
优化指导:为动画性能优化提供数据支持
标准化测试:建立统一的动画性能测试方法
系统借鉴了《鸿蒙跨端U同步》中的帧同步技术,将游戏场景的性能监测机制应用于动画测试领域。未来可增加更多性能指标(如GPU负载、CPU占用等),并与自动化测试框架集成实现持续性能监测。
