You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

Android应用基于OpenGL实现高效PNG逐帧动画的技术求助

用OpenGL优化Android逐帧动画的指南

为什么Animation List内存占用这么高?

Animation List会把所有帧图片一次性解码并加载到内存里——90张PNG每张平均1.3MB左右,加起来就占了120MB。而OpenGL方案可以做到按需加载、即时释放:我们只保留当前显示的帧,以及前后各1帧(预加载),内存占用直接降到几MB级别,完美解决内存过高的问题。

下面是针对零基础开发者的分步实现指南:


第一步:替换布局,引入OpenGL容器

把原来的ImageView换成GLSurfaceView,它是Android官方提供的OpenGL渲染容器,已经封装了基础的OpenGL初始化逻辑:

<android.opengl.GLSurfaceView
    android:id="@+id/gl_surface_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

第二步:实现OpenGL渲染核心(Renderer类)

我们需要写一个GLSurfaceView.Renderer的实现类,这是动画逻辑的核心。它负责加载帧纹理、控制帧切换、绘制到屏幕上。

基础Renderer框架(含完整注释)

public class FrameAnimationRenderer implements GLSurfaceView.Renderer {
    private Context mContext;
    // 只缓存3帧纹理(当前帧、前一帧、后一帧),避免内存堆积
    private int[] mTextureIds = new int[3];
    private int mCurrentFrameIndex = 0;
    private long mLastFrameSwitchTime = 0;
    private static final long FRAME_DURATION = 100; // 每帧停留100ms,可按需调整

    // 动画帧的资源ID列表(比如frame_0到frame_89)
    private int[] mFrameResIds;
    // 缓存着色器程序和变量位置,避免重复编译提升性能
    private int mShaderProgram;
    private int mPositionHandle;
    private int mTexCoordHandle;
    private int mTextureHandle;

    public FrameAnimationRenderer(Context context, int[] frameResIds) {
        mContext = context;
        mFrameResIds = frameResIds;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // 初始化OpenGL环境,设置背景透明(如果需要)
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        // 启用2D纹理支持
        GLES20.glEnable(GLES20.GL_TEXTURE_2D);
        // 生成3个纹理ID
        GLES20.glGenTextures(3, mTextureIds, 0);
        // 预加载前3帧(循环播放的话最后一帧也会被覆盖)
        loadFrameToTexture(mCurrentFrameIndex, mTextureIds[0]);
        loadFrameToTexture(mCurrentFrameIndex + 1, mTextureIds[1]);
        loadFrameToTexture(mCurrentFrameIndex + 2, mTextureIds[2]);
        // 编译并缓存着色器程序
        initShaderProgram();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        // 设置渲染视口大小,匹配SurfaceView的尺寸
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // 清除屏幕缓存
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        
        // 判断是否需要切换帧
        long currentTime = System.currentTimeMillis();
        if (currentTime - mLastFrameSwitchTime >= FRAME_DURATION) {
            mLastFrameSwitchTime = currentTime;
            switchToNextFrame();
        }

        // 绘制当前帧
        drawCurrentFrame();
    }

    // 切换到下一帧,同时加载新帧、释放旧帧
    private void switchToNextFrame() {
        mCurrentFrameIndex++;
        // 循环播放
        if (mCurrentFrameIndex >= mFrameResIds.length) {
            mCurrentFrameIndex = 0;
        }
        // 计算要替换的旧纹理索引(循环利用3个纹理ID)
        int oldTextureIndex = (mCurrentFrameIndex - 3 + 3) % 3;
        // 计算要加载的新帧索引
        int newFrameIndex = (mCurrentFrameIndex + 2) % mFrameResIds.length;
        
        // 释放旧纹理内存
        GLES20.glDeleteTextures(1, mTextureIds, oldTextureIndex);
        // 生成新的纹理ID
        GLES20.glGenTextures(1, mTextureIds, oldTextureIndex);
        // 加载新帧到旧纹理的位置
        loadFrameToTexture(newFrameIndex, mTextureIds[oldTextureIndex]);
    }

    // 将指定帧的图片加载为OpenGL纹理
    private void loadFrameToTexture(int frameIndex, int textureId) {
        if (frameIndex < 0 || frameIndex >= mFrameResIds.length) return;
        // 从资源加载Bitmap
        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), mFrameResIds[frameIndex]);
        // 绑定纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
        // 设置纹理过滤参数(保证缩放时的清晰度)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        // 设置纹理环绕参数(避免边缘拉伸)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
        // 将Bitmap数据上传到GPU纹理
        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
        // 释放Bitmap内存(数据已经在GPU里了)
        bitmap.recycle();
    }

    // 初始化着色器程序(OpenGL绘制必须的核心代码)
    private void initShaderProgram() {
        // 顶点着色器:负责定位纹理在屏幕上的位置
        String vertexShaderCode =
            "attribute vec4 vPosition;" +
            "attribute vec2 vTexCoord;" +
            "varying vec2 texCoord;" +
            "void main() {" +
            "  gl_Position = vPosition;" +
            "  texCoord = vTexCoord;" +
            "}";

        // 片段着色器:负责渲染纹理的颜色
        String fragmentShaderCode =
            "precision mediump float;" +
            "varying vec2 texCoord;" +
            "uniform sampler2D texture;" +
            "void main() {" +
            "  gl_FragColor = texture2D(texture, texCoord);" +
            "}";

        // 编译着色器
        int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
        int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

        // 创建并链接着色器程序
        mShaderProgram = GLES20.glCreateProgram();
        GLES20.glAttachShader(mShaderProgram, vertexShader);
        GLES20.glAttachShader(mShaderProgram, fragmentShader);
        GLES20.glLinkProgram(mShaderProgram);

        // 获取着色器中的变量位置(缓存起来复用)
        mPositionHandle = GLES20.glGetAttribLocation(mShaderProgram, "vPosition");
        mTexCoordHandle = GLES20.glGetAttribLocation(mShaderProgram, "vTexCoord");
        mTextureHandle = GLES20.glGetUniformLocation(mShaderProgram, "texture");

        // 删除临时着色器对象
        GLES20.glDeleteShader(vertexShader);
        GLES20.glDeleteShader(fragmentShader);
    }

    // 绘制当前帧的纹理到屏幕
    private void drawCurrentFrame() {
        GLES20.glUseProgram(mShaderProgram);

        // 设置顶点坐标(覆盖整个屏幕)
        float[] vertices = {
            -1.0f, 1.0f,  // 左上
            -1.0f, -1.0f, // 左下
            1.0f, 1.0f,   // 右上
            1.0f, -1.0f   // 右下
        };
        FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(vertices);
        vertexBuffer.position(0);
        GLES20.glVertexAttribPointer(mPositionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
        GLES20.glEnableVertexAttribArray(mPositionHandle);

        // 设置纹理坐标(注意:OpenGL纹理原点在左下角,这里翻转y轴匹配Android图片)
        float[] texCoords = {
            0.0f, 1.0f,  // 左上
            0.0f, 0.0f,  // 左下
            1.0f, 1.0f,  // 右上
            1.0f, 0.0f   // 右下
        };
        FloatBuffer texCoordBuffer = ByteBuffer.allocateDirect(texCoords.length * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(texCoords);
        texCoordBuffer.position(0);
        GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer);
        GLES20.glEnableVertexAttribArray(mTexCoordHandle);

        // 绑定当前帧的纹理并绘制
        int currentTextureIndex = mCurrentFrameIndex % 3;
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIds[currentTextureIndex]);
        GLES20.glUniform1i(mTextureHandle, 0);

        // 绘制四边形(覆盖整个屏幕)
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }

    // 编译着色器的工具方法
    private int compileShader(int type, String shaderCode) {
        int shader = GLES20.glCreateShader(type);
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);
        return shader;
    }
}

第三步:在MainActivity中初始化GLSurfaceView

替换原来的ImageView初始化代码,把Renderer绑定到GLSurfaceView上:

public class MainActivity extends AppCompatActivity {
    private GLSurfaceView mGLSurfaceView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mGLSurfaceView = findViewById(R.id.gl_surface_view);
        // 设置OpenGL版本为2.0(兼容绝大多数Android设备)
        mGLSurfaceView.setEGLContextClientVersion(2);
        
        // 准备所有帧的资源ID列表(假设你的图片命名为frame_0到frame_89)
        int[] frameResIds = new int[90];
        for (int i = 0; i < 90; i++) {
            String resName = "frame_" + i;
            frameResIds[i] = getResources().getIdentifier(resName, "drawable", getPackageName());
        }
        
        // 创建并设置Renderer
        FrameAnimationRenderer renderer = new FrameAnimationRenderer(this, frameResIds);
        mGLSurfaceView.setRenderer(renderer);
        // 设置为连续渲染模式(适合动画)
        mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
    }

    // 必须处理GLSurfaceView的生命周期
    @Override
    protected void onPause() {
        super.onPause();
        mGLSurfaceView.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mGLSurfaceView.onResume();
    }
}

额外优化建议

  1. 改用WebP格式:相同质量下WebP图片体积比PNG小50%以上,进一步降低内存占用和加载时间。
  2. 添加播放/暂停控制:在Renderer中添加setPlaying(boolean)方法,通过mGLSurfaceView.queueEvent()调用(注意OpenGL线程安全)。
  3. 调整预加载数量:如果动画有卡顿,可以把缓存的纹理数量从3增加到5,平衡内存和流畅度。
  4. 监控内存:用Android Studio的Profiler查看内存占用,确认优化效果(应该能降到10MB以内)。

内容的提问来源于stack exchange,提问作者Soroosh

火山引擎 最新活动