动画帧率稳定性测试工具设计与实现 原创

进修的泡芙
发布于 2025-6-17 20:46
浏览
0收藏

动画帧率稳定性测试工具设计与实现

一、项目概述

基于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占用等),并与自动化测试框架集成实现持续性能监测。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐