12. 线程调度优化:Camera专用线程池、设置线程优先级、避免主线程阻塞
相机启动优化做到后面,你会发现一个扎心的事实——代码写得再漂亮,线程调度一乱,一切白搭。我见过太多团队,把相机启动卡顿归咎于硬件,结果一查日志,全是主线程被各种杂活堵死的惨案。
说白了,相机启动是个“多线程协作”的活儿。预览流、拍照流、算法处理、UI更新……哪个环节掉链子,用户感知到的就是“黑屏”、“卡顿”、“闪退”。今天咱们就聊聊,怎么把线程调度这块硬骨头啃下来。
12.1 为什么相机启动需要专用线程池?
Android系统默认的线程池,比如AsyncTask、线程池Executors.newCachedThreadPool(),其实不太适合相机场景。为什么?
- 不可控:系统线程池的线程数、优先级,你说了不算。
- 容易被干扰:App里其他模块的任务,可能把线程池占满。
- 缺乏隔离:相机是高实时任务,跟网络请求、数据库读写混在一起,迟早出事。
我个人习惯,给相机模块单独搞一个线程池。这样,相机启动的各个阶段——打开CameraDevice、创建Session、配置流、启动预览——都能在可控的线程上执行。
核心原则:相机专用线程池,只服务于相机模块。其他业务,哪怕是主线程,也别想借用。
12.2 如何设计Camera专用线程池?
先看代码。这是我项目里常用的一个配置:
// Camera专用线程池
public class CameraThreadPool {
// 核心线程数:2,足够处理预览和拍照
// 最大线程数:4,应对突发的高负载场景
// 存活时间:60秒,避免频繁创建销毁
private static final int CORE_POOL_SIZE = 2;
private static final int MAX_POOL_SIZE = 4;
private static final long KEEP_ALIVE_TIME = 60L;
// 使用有界队列,防止任务堆积导致OOM
private static final BlockingQueue<Runnable> WORK_QUEUE =
new LinkedBlockingQueue<>(128);
// 自定义线程工厂,给线程起个名字,方便调试
private static final ThreadFactory THREAD_FACTORY = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "CameraThread#" + mCount.getAndIncrement());
// 设置线程优先级为后台,避免抢占主线程资源
thread.setPriority(Thread.MAX_PRIORITY - 2);
return thread;
}
};
private static final ThreadPoolExecutor EXECUTOR =
new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
WORK_QUEUE,
THREAD_FACTORY,
new ThreadPoolExecutor.DiscardOldestPolicy()
);
public static void execute(Runnable task) {
EXECUTOR.execute(task);
}
}
这里有几个关键点,我踩过坑,得跟你说道说道:
- 核心线程数设为2:一个处理预览流,一个处理拍照/算法。别贪多,线程切换也是有开销的。
- 有界队列:队列大小128,超过就丢弃最旧的任务。相机是高实时场景,旧任务没必要执行。
- 线程命名:给线程起个“CameraThread”的名字,抓取日志时一眼就能定位。
注意:千万不要用无界队列!我曾经在一个项目里用了LinkedBlockingQueue无参构造,结果相机启动时任务堆积了几千个,直接OOM。教训深刻。
12.3 线程优先级怎么设?
线程优先级,说白了就是告诉系统:“我这个任务急不急”。相机启动时,UI线程的优先级最高,因为它负责渲染界面。相机处理线程,优先级可以设得比普通后台线程高一点,但别超过UI线程。
我建议这样设置:
| 线程类型 | 优先级 | 说明 |
|---|---|---|
| 主线程(UI线程) | MAX_PRIORITY(10) | 系统默认,不要改 |
| Camera预览线程 | MAX_PRIORITY - 1(9) | 预览帧必须及时处理 |
| Camera拍照线程 | MAX_PRIORITY - 2(8) | 拍照可以稍微容忍延迟 |
| 后台算法线程 | NORM_PRIORITY(5) | 比如美颜、HDR合成 |
你可能会问:“设这么高,会不会影响系统稳定性?” 嗯,这里有个平衡。相机是高优先级任务,但别设成MAX_PRIORITY,否则可能抢占系统关键服务的资源。我个人习惯,预览线程设9,拍照线程设8,足够了。
12.4 如何避免主线程阻塞?
这是相机启动优化的核心痛点。主线程一旦阻塞,用户看到的就是“黑屏”或“卡死”。我总结了几条铁律:
- 所有Camera API调用,都扔到专用线程池。包括openCamera、createCaptureSession、setRepeatingRequest等。
- 回调里别做耗时操作。Camera的回调(比如onImageAvailable)是在Binder线程里执行的,你如果在里面做图像处理,会阻塞整个Camera HAL。
- UI更新必须切回主线程。但别用Handler.post,建议用View.postOnAnimation,它跟VSync对齐,更流畅。
举个例子,我之前遇到一个坑:在onPreviewFrame回调里直接做YUV转Bitmap,然后更新ImageView。结果预览帧率从30fps掉到15fps,用户反馈“相机卡得像幻灯片”。后来我把图像处理扔到专用线程池,UI更新用postOnAnimation,问题解决。
小技巧:如果你不确定某个操作是否阻塞主线程,可以用StrictMode检测。在Application的onCreate里开启:
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
这样,任何主线程上的磁盘IO、网络请求、长耗时操作,都会在logcat里打印警告。
12.5 线程调度流程图
下面这张图,是我根据项目经验画的。它展示了相机启动时,各个任务如何分配到不同线程上:
从图里可以看得很清楚:主线程只负责UI更新,Camera的API调用全部扔到专用线程池,后台算法(美颜、HDR等)则使用更低优先级的线程。这样,即使算法处理慢,也不会影响预览的流畅度。
12.6 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 别在Camera回调里加锁。我曾经在onImageAvailable里加了一个synchronized块,结果导致预览线程和拍照线程死锁,相机直接黑屏。后来改用无锁队列(ConcurrentLinkedQueue)解决。
- 线程池拒绝策略要谨慎。DiscardOldestPolicy适合相机场景,但如果你用的是CallerRunsPolicy,任务会回退到调用线程执行——万一调用线程是主线程,那就炸了。
- 别忘了处理线程泄漏。相机销毁时,记得调用线程池的shutdown(),否则线程会一直存活,导致内存泄漏。我习惯在CameraManager的close()方法里统一清理。
总结一句话:相机启动优化的本质,就是把主线程上的“脏活累活”全部剥离出去,让主线程只做它最擅长的事——渲染UI。线程调度做得好,相机启动速度至少能提升30%。