You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android应用InputStream.read触发NullPointerException的排查求助

咱们来好好分析下这个在三星Galaxy J2(Android 5.1)上出现的NullPointerException问题。从堆栈信息来看,异常出现在InputStream.read的调用链里,你怀疑的多线程并发问题完全正确——下面我来拆解根本原因,再给你可行的修复方案。

根本原因分析

  1. 无同步的多线程共享资源操作
    你的startRecording和stopRecording分别在独立线程中操作inputStream、fileOutputStream这些成员变量,完全没有同步控制。这就会出现竞态条件:当录制线程正在执行inputStream.read(tmp)时,停止线程可能已经调用了inputStream.close()。而Android 5.1搭载的OkHttp/Okio旧版本中,对已关闭的流执行read操作会直接抛出NullPointerException,这就是你看到的堆栈异常的来源。

  2. 共享变量的可见性问题
    isRecording、inputStream这些成员变量没有用volatile修饰,在多线程环境下,一个线程对变量的修改无法及时被另一个线程感知。比如停止线程已经把isRecording设为false,但录制线程可能还在继续执行循环,尝试读取已经被关闭的流。

  3. 流关闭时机不合理
    停止线程直接关闭流,没有先通知录制线程主动退出循环,导致录制线程在毫无准备的情况下,对已关闭的流进行读写操作,触发底层的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:46:21