Android 11(API30)下Documents外部存储日志写入及重装应用问题
Android 11下Documents目录日志持久化方案(卸载重装后可访问)
问题根源
Android 11及以上的Scoped Storage机制中,应用在公共Documents目录下创建的文件/目录会被标记为应用私有,文件访问权限绑定到应用的UID。卸载应用后文件虽保留,但新安装的应用UID不同,无法直接访问旧文件,也不能在原私有子目录下创建新文件。
解决方案
1. 使用存储访问框架(SAF)获取持久目录权限
这是官方推荐的合规方案,通过用户手动授权目录访问,获得持久URI权限,卸载重装后可恢复访问:
步骤1:发起目录选择请求
private fun requestDocumentsDirectory() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) // 引导用户定位到Documents下的目标目录,不存在则提示用户创建 val initialUri = DocumentsContract.buildRootUri("com.android.providers.downloads.documents", "documents_root") intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri) startActivityForResult(intent, REQUEST_CODE_PICK_DIR) }
步骤2:处理授权结果并保存权限
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE_PICK_DIR && resultCode == RESULT_OK) { data?.data?.let { treeUri -> // 持久化URI权限,卸载重装后仍有效 contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) // 将URI保存到SharedPreferences供后续使用 getSharedPreferences("AppPrefs", MODE_PRIVATE) .edit() .putString("LOG_DIR_URI", treeUri.toString()) .apply() } } }
2. 适配logcat日志写入
直接用logcat -f命令写入SAF授权目录会失败(logcat进程无SAF权限),需改为应用读取logcat输出并写入文件:
private fun startLogcatLogging(logDirUri: Uri) { // 构建日志文件的SAF URI val logFileUri = DocumentsContract.buildDocumentUriUsingTree( logDirUri, DocumentsContract.getDocumentId(logDirUri) + "/mylog.log" ) val process = Runtime.getRuntime().exec("logcat *:debug") val inputStream = process.inputStream val reader = BufferedReader(InputStreamReader(inputStream)) // 开启线程读取logcat输出并写入SAF文件 Thread { var line: String? contentResolver.openOutputStream(logFileUri, "wa")?.bufferedWriter()?.use { writer -> while (reader.readLine().also { line = it } != null) { writer.write(line) writer.newLine() writer.flush() } } reader.close() inputStream.close() }.start() }
3. 适配logback-android日志存储
修改logback配置,用SAF输出流替代直接文件路径:
自定义Logback Appender
class SafAppender : OutputStreamAppender<ILoggingEvent>() { private var logFileUri: Uri? = null fun setLogFileUri(uriStr: String) { logFileUri = Uri.parse(uriStr) } override fun openOutputStream(): OutputStream { val context = ContextHolder.getApplicationContext() return context.contentResolver.openOutputStream(logFileUri!!, "wa") ?: throw IOException("Failed to open SAF output stream") } }
配置logback.xml
<configuration> <appender name="SAF_APPENDER" class="com.yourpackage.SafAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="debug"> <appender-ref ref="SAF_APPENDER" /> </root> </configuration>
初始化时绑定SAF URI
val prefs = getSharedPreferences("AppPrefs", MODE_PRIVATE) val logDirUriStr = prefs.getString("LOG_DIR_URI", null) if (logDirUriStr != null) { val logDirUri = Uri.parse(logDirUriStr) val logFileUri = DocumentsContract.buildDocumentUriUsingTree( logDirUri, DocumentsContract.getDocumentId(logDirUri) + "/logback.log" ) val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext val safAppender = loggerContext.getLogger("ROOT").getAppender("SAF_APPENDER") as SafAppender safAppender.setLogFileUri(logFileUri.toString()) safAppender.start() }
4. 替代方案:内部存储+手动导出(可选)
若不想依赖用户授权,可先将日志写入应用内部存储(context.filesDir),再提供导出功能,通过SAF将日志复制到Documents目录:
private fun exportLogs() { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.type = "text/plain" intent.putExtra(Intent.EXTRA_TITLE, "app_logs_${System.currentTimeMillis()}.log") startActivityForResult(intent, REQUEST_CODE_EXPORT) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE_EXPORT && resultCode == RESULT_OK) { data?.data?.let { uri -> contentResolver.openOutputStream(uri)?.use { outputStream -> val internalLog = File(filesDir, "mylog.log") internalLog.inputStream().use { it.copyTo(outputStream) } } } } }
关键注意事项
- 首次启动需引导用户完成目录授权,否则无法写入Documents目录。
- 持久化URI权限会在应用卸载后保留,但用户可在系统设置中撤销。
- 避免直接使用文件路径操作公共目录,所有读写需通过SAF的URI完成。
内容的提问来源于stack exchange,提问作者Shannah
相关产品推荐
相关产品推荐

