28、性能优化:内存优化、CPU优化、GPU优化、功耗优化、ANR预防
性能优化,说白了就是让App跑得更快、更稳、更省电。很多开发者觉得优化是后期的事,先把功能做出来再说。我个人习惯是,从写第一行代码开始,脑子里就要有优化的弦。否则等到项目快上线了,发现卡顿、闪退、发热,那时候再回头改,成本就高了去了。
今天咱们把Android音视频开发中常见的五个优化方向捋一遍。内存、CPU、GPU、功耗、ANR,每个都是硬骨头。我踩过的坑不少,希望能帮你少走弯路。
核心观点:性能优化不是单一维度的,内存、CPU、GPU、功耗、ANR这五者相互影响。比如内存泄漏会导致频繁GC,GC会吃掉CPU,CPU高负载又加剧功耗,功耗高引发降频,降频后界面卡顿就容易ANR。所以优化要系统性地看。
一、内存优化:别让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