Android中使用Uri上传录屏视频时出现空指针异常的问题排查与解决
我看到你遇到的问题了——录屏后通过Uri上传视频时,因为获取的File对象为null导致空指针崩溃,日志里显示File path null,最后在file!!.asRequestBody这行代码直接炸了。咱们一步步来排查和解决这个问题:
一、先定位空指针的直接原因
你的崩溃日志指向file!!.asRequestBody这行,这说明getFileFromContentUri5(this, uri)返回了null,而你用!!强制非空触发了空指针异常。
为什么这个方法会返回null?大概率是从ContentUri复制文件到缓存目录的过程中出了问题——可能是权限不足、录屏生成的文件不存在、流复制时出错,或者复制后的文件大小为0,导致方法返回null。
二、紧急修复:先避免崩溃
在解决根本问题前,先给代码加个null判断,至少不会直接闪退:
private fun uploadVideo(uri: Uri) { val file = getFileFromContentUri5(this, uri) ?: run { Log.e("uploadVideo", "无法从Uri获取有效文件") // 这里可以给用户弹个提示,比如"上传失败:文件获取异常" return } // 后面的原有代码保持不变 }
三、排查getFileFromContentUri5的问题
给这个工具方法加详细日志,就能精准找到哪里出错了:
fun getFileFromContentUri5(context: Context, contentUri: Uri): File? { val contentResolver = context.contentResolver val file = File(context.cacheDir, "temp_video.mp4") Log.e("getFileFromUri", "准备复制文件到临时目录:${file.path}") try { contentResolver.openInputStream(contentUri)?.use { inputStream -> Log.e("getFileFromUri", "成功打开Uri对应的输入流") FileOutputStream(file).use { outputStream -> val bytesCopied = inputStream.copyTo(outputStream) Log.e("getFileFromUri", "共复制了$bytesCopied 字节到临时文件") } } } catch (e: Exception) { Log.e("getFileFromUri", "复制文件出错:${e.message}", e) return null } if (file.length() > 0) { Log.e("getFileFromUri", "临时文件大小:${file.length()} 字节") return file } else { Log.e("Error", "复制后临时文件为空") return null } }
运行后看日志,就能知道是:
- 打开输入流失败(比如权限不足、Uri无效)
- 复制的字节数为0(文件本身为空)
- 还是抛出了其他异常(比如IO错误)
常见问题的解决方向
权限问题
在AndroidManifest.xml中添加对应权限,并在运行时请求用户授权:<!-- Android 12及以下需要这个 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <!-- Android 13+需要这个 --> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" android:minSdkVersion="33" />Uri有效性问题
先验证HBRecorder生成的Uri是否有效:fun isUriValid(context: Context, uri: Uri): Boolean { return try { context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> cursor.moveToFirst() } ?: false } catch (e: Exception) { false } }在
uploadVideo开头调用这个方法,确认Uri是否指向真实存在的文件。
四、更优雅的长期解决方案:跳过File直接用InputStream上传
在Android 10+的Scoped Storage机制下,很多ContentUri并没有对应的真实File路径,强行转File很容易踩坑。更推荐直接通过Uri获取InputStream创建RequestBody,既适配Scoped Storage,又节省磁盘空间:
1. 先写个获取文件名的工具方法
fun getFileNameFromUri(context: Context, uri: Uri): String { val contentResolver = context.contentResolver // 优先从系统媒体库获取文件名 contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor -> if (cursor.moveToFirst()) { val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameIndex != -1) { return cursor.getString(nameIndex) } } } // 如果获取不到,用Uri的最后一段作为文件名 return uri.lastPathSegment ?: "unknown_video.mp4" }
2. 修改uploadVideo方法,直接用InputStream上传
private fun uploadVideo(uri: Uri) { val contentResolver = this.contentResolver val fileName = getFileNameFromUri(this, uri) // 直接从Uri创建RequestBody,不需要转成File val videoRequestBody = object : RequestBody() { override fun contentType(): MediaType? { return "video/*".toMediaTypeOrNull() } override fun writeTo(sink: BufferedSink) { contentResolver.openInputStream(uri)?.use { inputStream -> sink.writeAll(inputStream.source()) } ?: throw IOException("无法打开Uri对应的输入流:$uri") } } val bodyBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) bodyBuilder.addFormDataPart("file", fileName, videoRequestBody) val requestBody = bodyBuilder.build() val requestBodyWithProgress = RequestBodyWithProgress(requestBody) { progress -> runOnUiThread { binding.apply { uploadProgress.progress = progress uploadPercentageTxt.text = "$progress%" } } } // 调用上传接口 viewModel.callPostUploadVideo(testId, requestBodyWithProgress) }
这种方式完全绕开了File转换的问题,适配Android 10+的存储机制更友好,还能避免临时文件占用磁盘空间。
备注:内容来源于stack exchange,提问作者Mohsen Mohebi

