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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 15:32:49