28、性能优化:内存优化、CPU优化、GPU优化、功耗优化、ANR预防

性能优化,说白了就是让App跑得更快、更稳、更省电。很多开发者觉得优化是后期的事,先把功能做出来再说。我个人习惯是,从写第一行代码开始,脑子里就要有优化的弦。否则等到项目快上线了,发现卡顿、闪退、发热,那时候再回头改,成本就高了去了。

今天咱们把Android音视频开发中常见的五个优化方向捋一遍。内存、CPU、GPU、功耗、ANR,每个都是硬骨头。我踩过的坑不少,希望能帮你少走弯路。

核心观点:性能优化不是单一维度的,内存、CPU、GPU、功耗、ANR这五者相互影响。比如内存泄漏会导致频繁GC,GC会吃掉CPU,CPU高负载又加剧功耗,功耗高引发降频,降频后界面卡顿就容易ANR。所以优化要系统性地看。

Android音视频性能优化知识体系 性能优化 内存优化 CPU优化 GPU优化 功耗优化 ANR预防 Bitmap复用 对象池 Native内存监控 编解码线程模型 帧率控制 避免频繁GC 过度绘制 Shader优化 纹理压缩 唤醒锁管理 网络请求合并 传感器采样率 异步化 消息队列监控 StrictMode

一、内存优化:别让App变成内存黑洞

音视频应用是内存消耗大户。一张1080p的图片解码成Bitmap,就要吃掉6MB左右。要是你同时缓存几十帧,内存直接爆掉。我见过一个直播App,因为没做内存优化,在低端机上打开直播间就闪退,用户骂声一片。

内存优化的核心思路:减少分配、及时释放、复用资源。

1.1 Bitmap内存管理

Bitmap是音视频开发中最常见的内存杀手。我建议你养成几个习惯:

  • 使用inSampleSize进行采样加载:不要直接加载原图,根据ImageView的尺寸计算采样率。比如显示区域只有200x200,就没必要加载4000x3000的原图。
  • 复用Bitmap内存:使用BitmapFactory.Options.inBitmap,让多个Bitmap共享同一块内存区域。这在列表滑动场景下特别有用。
  • 及时recycle():在Android 3.0之前,Bitmap内存分配在Native层,需要手动回收。虽然现在GC会处理,但主动释放仍然是个好习惯。

我的经验:曾经有个视频编辑项目,用户连续导入多张高清图片后,App直接OOM。后来我改用采样加载+LRU缓存,内存占用从300MB降到了80MB。你想想看,这差距有多大。

1.2 对象池与缓冲区复用

音视频处理中,频繁创建和销毁对象会导致GC频繁触发,引起卡顿。对象池是个好办法。

// 一个简单的ByteBuffer对象池
public class ByteBufferPool {
    private final Queue<ByteBuffer> pool = new ConcurrentLinkedQueue<>();
    private final int bufferSize;
    
    public ByteBufferPool(int bufferSize) {
        this.bufferSize = bufferSize;
    }
    
    public ByteBuffer acquire() {
        ByteBuffer buffer = pool.poll();
        if (buffer == null) {
            buffer = ByteBuffer.allocateDirect(bufferSize);
        }
        buffer.clear();
        return buffer;
    }
    
    public void release(ByteBuffer buffer) {
        pool.offer(buffer);
    }
}

嗯,这里要注意:对象池的大小要有限制,否则内存泄漏的风险反而增加了。我一般会设置一个最大容量,超过上限就直接丢弃。

1.3 Native内存监控

Android的Native层内存泄漏很难排查。我建议使用AddressSanitizer或者自己封装malloc/free的hook。在音视频开发中,MediaCodec、OpenGL、FFmpeg这些底层库都在Native层分配内存,一旦泄漏,你很难用常规的MAT工具抓到。

避坑指南:我曾经在一个项目中,MediaCodec的buffer没及时释放,导致Native内存持续增长。用户播放视频半小时后,App被系统杀掉。排查了整整两天,最后发现是dequeueOutputBuffer返回的buffer没有调用release()。这种低级错误,犯一次就记住了。

二、CPU优化:别让手机变成暖手宝

CPU优化说白了就是让计算资源用在刀刃上。音视频编解码、图像处理、音频重采样,这些都是CPU密集型操作。如果处理不好,手机发热、掉电快,用户分分钟卸载你的App。

2.1 线程模型设计

音视频处理通常需要多个线程协作:采集线程、编码线程、渲染线程、网络线程。我建议使用HandlerThread或者更现代的Coroutine来管理。

  • 避免主线程做耗时操作:解码、编码、文件IO这些统统扔到子线程。
  • 控制线程数量:不是线程越多越好。CPU核心数就那么几个,线程多了反而因为上下文切换降低效率。我一般用线程池,核心线程数设为CPU核心数+1。
  • 优先级设置:渲染线程优先级最高,后台任务用低优先级。Android提供了Process.setThreadPriority(),别嫌麻烦,该设就设。

2.2 帧率控制与丢帧策略

在视频播放或录制中,帧率控制很关键。如果解码速度跟不上,是选择丢帧还是等待?我个人习惯是:

  • 实时场景(直播):丢帧。保证实时性比保证每一帧都渲染更重要。
  • 非实时场景(本地播放):等待。用户希望看到完整的画面。
// 一个简单的帧率控制逻辑
long lastFrameTime = 0;
long frameInterval = 1000 / targetFps; // 目标帧率33ms

while (isPlaying) {
    long currentTime = System.currentTimeMillis();
    long elapsed = currentTime - lastFrameTime;
    
    if (elapsed < frameInterval) {
        // 还没到时间,休眠一会儿
        Thread.sleep(frameInterval - elapsed);
    } else if (elapsed > frameInterval * 2) {
        // 落后太多,丢帧
        skipFrame();
    }
    
    renderFrame();
    lastFrameTime = System.currentTimeMillis();
}

2.3 避免频繁GC

GC会暂停所有线程(Stop-The-World),在音视频渲染中这是致命的。一次GC可能耗时几十毫秒,足够让画面卡顿好几帧。

怎么避免?少创建临时对象。比如在onDraw()里不要new对象,用成员变量复用。循环里能用基本类型就别用包装类。这些细节积累起来,效果很明显。

三、GPU优化:让渲染更丝滑

GPU主要负责图形渲染。在音视频应用中,视频画面的渲染、滤镜效果、UI动画都依赖GPU。GPU优化不好,画面撕裂、掉帧、卡顿就来了。

3.1 减少过度绘制

过度绘制就是同一像素被多次绘制。你可以用开发者选项里的"显示过度绘制"来检查。红色区域越多,问题越严重。

  • 移除不必要的背景:如果父布局已经有背景,子布局就别再设了。
  • 使用ClipRect裁剪:只绘制可见区域,不可见的部分直接裁剪掉。
  • ViewStub延迟加载:不常用的布局,用ViewStub占位,需要时再inflate。

3.2 Shader优化

如果你用OpenGL或Vulkan做滤镜渲染,Shader的性能直接影响帧率。我建议:

  • 减少纹理采样次数:能一次采样搞定的,别分两次。
  • 使用精度修饰符:移动端用mediump或lowp,别用highp,精度够用就行。
  • 避免条件分支:GPU的并行架构对分支不友好,能用数学运算代替if-else就尽量代替。

我的经验:曾经写一个美颜滤镜,用了大量的if-else判断皮肤区域,结果在骁龙660上只有15帧。后来改成用LUT查找表,直接飙到60帧。有时候换个思路,效果天差地别。

3.3 纹理压缩

纹理内存是GPU的宝贵资源。使用ETC2、ASTC等压缩格式,可以大幅减少纹理占用的显存。Android从4.3开始支持ETC2,从5.0开始支持ASTC。如果你的App最低版本支持这些格式,就别用RGBA8888了。

四、功耗优化:别让用户一天三充

功耗优化经常被忽视,但用户体验影响很大。一个视频App如果看半小时就掉电20%,用户肯定有意见。

4.1 WakeLock管理

音视频播放时,屏幕可能熄灭,但播放不能停。这时候需要WakeLock。但一定要记得释放,否则手机永远不锁屏,电量哗哗地掉。

PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(
    PowerManager.PARTIAL_WAKE_LOCK, "MyApp:VideoPlayback");
wl.acquire(10 * 60 * 1000L); // 最多持锁10分钟

// 播放结束时
if (wl.isHeld()) {
    wl.release();
}

嗯,这里要注意:持锁时间不要太长。我一般会设置一个超时时间,防止异常情况下锁没释放。

4.2 网络请求合并

网络模块是耗电大户。每次网络请求都会唤醒射频模块,而射频模块的功耗很高。我建议:

  • 批量上报:把多个日志或统计信息合并成一次请求。
  • 使用JobScheduler:在WiFi环境下或者充电时再执行大流量任务。
  • 压缩数据:使用Protocol Buffers或者MessagePack,比JSON更省流量。

4.3 传感器采样率

如果你用传感器做交互(比如旋转屏幕、手势控制),采样率不要设太高。SENSOR_DELAY_NORMAL就够了,别用SENSOR_DELAY_FASTEST。高采样率意味着CPU频繁被唤醒,功耗直线上升。

五、ANR预防:别让用户等太久

ANR(Application Not Responding)是Android系统对用户体验的最后一道防线。主线程阻塞超过5秒(输入事件)或10秒(广播接收),系统就会弹ANR对话框。在音视频开发中,ANR的常见原因有:

  • 在主线程做MediaCodec的同步操作
  • 主线程等待锁,而锁被子线程持有
  • Binder调用超时

5.1 异步化改造

所有可能耗时的操作,都别放在主线程。我建议:

  • 使用HandlerThread:专门处理音视频编解码的回调。
  • 使用AsyncTask或Coroutine:处理网络请求和文件IO。
  • 使用LiveData或RxJava:把结果post回主线程更新UI。

5.2 消息队列监控

你可以定期检查主线程的Looper消息队列。如果某个消息处理时间超过200ms,就记录下来。这样能提前发现潜在的ANR风险。

Looper.getMainLooper().setMessageLogging(new Printer() {
    @Override
    public void println(String x) {
        if (x.startsWith(">>> Dispatching to")) {
            startTime = System.currentTimeMillis();
        } else if (x.startsWith("<<< Finished to")) {
            long cost = System.currentTimeMillis() - startTime;
            if (cost > 200) {
                Log.w("ANRMonitor", "主线程耗时: " + cost + "ms");
            }
        }
    }
});

5.3 使用StrictMode

StrictMode是Android提供的开发者工具,可以在开发阶段就发现主线程的违规操作。我建议在Debug模式下开启:

if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
            .detectDiskReads()
            .detectDiskWrites()
            .detectNetwork()
            .penaltyLog()
            .build());
}

避坑指南:我曾经在一个项目中,因为MediaCodec的start()方法在主线程调用,导致ANR。当时没注意,MediaCodec.start()在某些设备上会同步等待硬件初始化,耗时可能超过5秒。后来我把它移到子线程,问题解决。这种坑,踩过一次就长记性了。

总结

性能优化没有银弹。内存、CPU、GPU、功耗、ANR这五个维度,每个都需要你深入理解底层原理,结合具体场景做权衡。我的建议是:

  • 用工具说话:Profiler、Systrace、Perfetto,这些工具要熟练使用。
  • 从用户视角出发:低端机、弱网环境、长时间使用,这些场景都要覆盖。
  • 持续优化:性能优化不是一锤子买卖,每次版本迭代都要关注。

好了,这一章的内容就到这里。记住,优化不是炫技,而是让用户用得舒服。你想想看,一个流畅、省电、不卡顿的App,用户怎么会不爱呢?


公众号:蓝海资料掘金营,微信deep3321