ARCore实时帧捕获文本识别:是否需要使用信号量?
首先,你的APP崩溃的直接原因非常明确:你在子线程(executor.execute()执行的任务)里调用了captureImage(),而这个方法里弹出了Toast——Toast必须在UI线程(拥有Looper的线程)中创建和显示,子线程默认没有初始化Looper,所以抛出了Can't toast on a thread that has not called Looper.prepare()异常。
接下来,我们一步步解决问题,同时优化你的线程同步逻辑:
1. 修复Toast的UI线程执行问题
把captureImage()中的Toast显示代码移到UI线程执行。你可以用Activity的runOnUiThread()方法,或者复用你已经定义的handler:
// 原来在子线程中执行的错误写法 Toast.makeText(this, "捕获图像", Toast.LENGTH_SHORT).show(); // 修改为UI线程执行: runOnUiThread(() -> { Toast.makeText(this, "捕获图像", Toast.LENGTH_SHORT).show(); });
如果captureImage()不在Activity类中,就传递Handler实例过来,用handler.post()包裹Toast代码即可。
2. 修复locked变量的线程安全问题
你的locked是普通布尔变量,在UI线程(onUpdateFrame回调)和子线程(OCR任务)中都被读写,这会导致竞态条件(比如多个线程同时修改或读取,导致逻辑混乱)。
推荐用AtomicBoolean代替普通boolean,它是线程安全的原子类,不需要额外的同步块就能保证读写操作的原子性:
// 替换原来的private boolean locked = false; private AtomicBoolean locked = new AtomicBoolean(false);
然后修改onUpdateFrame中的判断逻辑,用原子操作确保同一时间只有一个OCR任务在执行:
public void onUpdateFrame(FrameTime frameTime) { Frame frame = arFragment.getArSceneView().getArFrame(); if (frame == null) { return; } // 用compareAndSet原子性判断并设置锁,避免多线程竞态 if(frame.getCamera().getTrackingState() == TrackingState.TRACKING && locked.compareAndSet(false, true)) { // 显示ProgressDialog要确保在UI线程(onUpdateFrame属于ARCore渲染线程,不是UI线程) handler.post(() -> { if (mProgressDialog == null) { mProgressDialog = ProgressDialog.show(this, "Processing", "OCR...", true); } else { mProgressDialog.show(); } }); executor.execute(() -> { Bitmap b = captureImage(); final String text = getOCRResult(b); handler.post(() -> { if(text != null && !text.isEmpty()){ doSomething(); // 识别成功后保持锁定,停止后续帧捕获 } else{ // 识别失败,解锁允许下一帧处理 locked.set(false); } // 别忘了隐藏进度弹窗 if(mProgressDialog != null && mProgressDialog.isShowing()){ mProgressDialog.dismiss(); } }); }); } }
compareAndSet方法会原子性地检查当前值是否为false,如果是则设置为true并返回true,否则返回false,完美避免了多个线程同时进入任务提交逻辑的问题。
3. 关于是否需要信号量
其实你当前的场景完全不需要使用信号量。信号量主要用于控制多个线程对有限资源的访问并发数(比如限制最多3个线程同时处理任务),而你的需求是“同一时间只允许一个线程执行OCR识别”,用AtomicBoolean的原子锁已经完全能满足需求,比信号量更简单直接。
额外优化建议
- 如果OCR识别耗时较长,建议给
getOCRResult()设置超时机制,避免UI长时间无响应。 - 注意在页面销毁时,要关闭
executor、销毁ProgressDialog和handler,避免内存泄漏。
内容的提问来源于stack exchange,提问作者nCoder

