Android应用InputStream.read触发NullPointerException的排查求助
咱们来好好分析下这个在三星Galaxy J2(Android 5.1)上出现的NullPointerException问题。从堆栈信息来看,异常出现在InputStream.read的调用链里,你怀疑的多线程并发问题完全正确——下面我来拆解根本原因,再给你可行的修复方案。
根本原因分析
无同步的多线程共享资源操作
你的startRecording和stopRecording分别在独立线程中操作inputStream、fileOutputStream这些成员变量,完全没有同步控制。这就会出现竞态条件:当录制线程正在执行inputStream.read(tmp)时,停止线程可能已经调用了inputStream.close()。而Android 5.1搭载的OkHttp/Okio旧版本中,对已关闭的流执行read操作会直接抛出NullPointerException,这就是你看到的堆栈异常的来源。共享变量的可见性问题
isRecording、inputStream这些成员变量没有用volatile修饰,在多线程环境下,一个线程对变量的修改无法及时被另一个线程感知。比如停止线程已经把isRecording设为false,但录制线程可能还在继续执行循环,尝试读取已经被关闭的流。流关闭时机不合理
停止线程直接关闭流,没有先通知录制线程主动退出循环,导致录制线程在毫无准备的情况下,对已关闭的流进行读写操作,触发底层的NPE。
修复方案
我们需要通过同步控制、可见性保证和合理的线程协作来解决这个问题,具体修改如下:
1. 优化成员变量定义
首先给共享变量加上volatile保证可见性,同时创建一个全局锁对象用于同步操作:
private volatile boolean isRecording = false; private volatile InputStream inputStream = null; private volatile FileOutputStream fileOutputStream = null; private final Object lock = new Object(); private Thread recordingThread = null; private String filename = null;
2. 重构startRecording方法
让录制线程优先使用局部变量持有流,减少共享变量的直接操作;在循环中检查isRecording状态,主动退出;用同步锁保护共享变量的赋值和清理:
private void startRecording() { synchronized (lock) { if (isRecording) return; // 避免重复启动录制 isRecording = true; } recordingThread = new Thread(new Runnable() { @Override public void run() { filename = getFilename(); InputStream localInputStream = null; FileOutputStream localFileOutputStream = null; try { URL url = new URL(selectedRadio); localInputStream = url.openStream(); localFileOutputStream = new FileOutputStream(filename, true); // 同步更新共享变量 synchronized (lock) { inputStream = localInputStream; fileOutputStream = localFileOutputStream; } int c; final byte[] tmp = new byte[1024]; // 循环中检查isRecording状态,主动退出 while (isRecording) { c = localInputStream.read(tmp); if (c == -1) break; // 数据流结束 localFileOutputStream.write(tmp, 0, c); localFileOutputStream.getFD().sync(); } } catch (IOException | NullPointerException e) { e.printStackTrace(); } finally { // 确保流资源被正确清理 synchronized (lock) { try { if (localInputStream != null) { localInputStream.close(); inputStream = null; } if (localFileOutputStream != null) { localFileOutputStream.getFD().sync(); localFileOutputStream.close(); fileOutputStream = null; } isRecording = false; } catch (IOException e) { e.printStackTrace(); } // 发送媒体扫描广播,确保文件能被系统识别 if (filename != null) { File savedFile = new File(filename); sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(savedFile))); } } } } }, "AudioRecorder Thread"); recordingThread.start(); }
3. 重构stopRecording方法
先通知录制线程退出循环,再等待线程结束,最后再清理流资源,避免直接关闭流导致的竞态:
public void stopRecording() { new Thread(new Runnable() { @Override public void run() { synchronized (lock) { if (!isRecording) return; // 已经停止,无需操作 isRecording = false; // 通知录制线程退出循环 } // 等待录制线程结束,最多等待2秒防止线程挂起 try { if (recordingThread != null) { recordingThread.join(2000); } } catch (InterruptedException e) { e.printStackTrace(); } // 最后清理流资源 synchronized (lock) { try { if (inputStream != null) { inputStream.close(); inputStream = null; } if (fileOutputStream != null) { fileOutputStream.getFD().sync(); fileOutputStream.close(); fileOutputStream = null; } isRecording = false; // 再次发送广播,确保万无一失 if (filename != null) { File savedFile = new File(filename); sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(savedFile))); } } catch (IOException e) { e.printStackTrace(); } } } }).start(); }
为什么这些修改能解决问题?
- 同步锁:用
synchronized(lock)保护所有对共享变量的操作,彻底避免多线程竞态条件。 - volatile修饰符:确保
isRecording等变量的修改能立即被所有线程感知,防止录制线程在停止指令发出后还继续执行。 - 局部变量持有流:减少对共享流对象的直接依赖,降低流被意外关闭时的NPE风险。
- 主动退出循环:停止时先设置
isRecording为false,让录制线程主动结束循环,而不是直接关闭流,避免在read操作中途流被关闭。 - 线程等待:给录制线程足够的时间清理资源,再做最后的流关闭,进一步降低并发冲突。
另外补充一点:三星Galaxy J2的Android 5.1使用的是较旧版本的OkHttp(通常是2.x系列),这个版本在处理已关闭流的read操作时确实会抛出NPE,而更高版本的OkHttp已经修复了这个问题,所以这个异常在旧设备上更容易触发。
内容的提问来源于stack exchange,提问作者Daiwik Dan

