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

Android 11下WebView加载SD卡存储图片实现方案咨询

问题描述

我正使用Android Studio,通过开发者模式以USB连接搭载Android 11系统的手机,借助Android的WebView动态生成网页。

尽管已有大量相关问题的讨论,我始终无法加载外部存储中的文件。如下最小可复现示例表明,我可以正常加载assets文件夹内的图片:

package com.example.hellowebview
    
import android.os.Bundle
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
    
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val myWebView = WebView(this)
        val myImage = "file:///android_asset/MonaLisa.jpg"
        val myHtml = "<HTML><BODY><h3>Hello, WebView!</h3><img src='$myImage'></BODY></HTML>"
        myWebView.loadDataWithBaseURL(null, myHtml, "text/html", "utf-8", null);
        setContentView(myWebView)
    }
}

但我无法加载SD卡中的图片,以下是我尝试过多种路径写法的代码:

package com.example.hellowebview
    
import android.Manifest
import android.os.Bundle
import android.os.Environment
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
    
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val myWebView = WebView(this)

        val PERMISSION_EXTERNAL_STORAGE = 1
        requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_EXTERNAL_STORAGE)
        myWebView.getSettings().setAllowFileAccess(true);
        
        val sdCard = Environment.getExternalStorageDirectory().absolutePath.toString()
        
        val myImage = "file://$sdCard/Download/MonaLisa.jpg"
        //val myImage = "file:///$sdCard/Download/MonaLisa.jpg"
        //val myImage = "$sdCard/Download/MonaLisa.jpg"
        //val myImage = "file:///Download/MonaLisa.jpg"
        //val myImage = "file:///sdcard/Download/MonaLisa.jpg"
        //val myImage = "file://sdcard/Download/MonaLisa.jpg"

        val myHtml = "<HTML><BODY><h3>Hello, WebView!</h3><img src='$myImage'></BODY></HTML>"
        
        //myWebView.loadDataWithBaseURL(sdCard, myHtml, "text/html", "utf-8", null)
        myWebView.loadDataWithBaseURL(null, myHtml, "text/html", "utf-8", null)
        
        setContentView(myWebView)
    }
}

我同时在AndroidManifest.xml中添加了如下两行权限声明:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

应用已发起权限申请且我已授予对应权限,sdCard变量的取值为/storage/emulated/0,目标图片文件确实存在:
文件存在截图

我到底遗漏了什么配置,或是哪里编写有误?


我可以手动在SD卡上创建任意文件夹,通过USB手动从PC拷贝文件,也可以通过其他Android应用查看这些文件,为什么我的应用无法加载这些文件?


悬赏说明

我知晓Google为提升安全性在Android 11中调整了存储权限策略,但有公开示例证明从SD卡读取图片是可行的:该示例使用Java而非Kotlin实现,但这不影响参考:它可以展示可访问文件夹内的图片,我在手机上创建了对应文件夹并从PC拷贝了图片,该示例可以正常运行(不过不知为何无法二次启动,必须卸载后重装)。它没有使用WebView,我暂时无法复刻其实现逻辑。

该示例的Manifest中没有配置MANAGE_EXTERNAL_STORAGE权限,仅声明了READ_EXTERNAL_STORAGE权限。

我不需要应用将assets内的图片拷贝到其他文件夹,希望可以直接读取从PC拷贝到SD卡的图片,无需将图片放入assets目录后重新编译应用。

因此我希望得到该问题的解答:

在Android 11系统中,如何在WebView加载的HTML的<img>标签内使用SD卡可访问文件夹中的图片?


问题解答

代码存在3个核心错误,修复后即可正常加载外部存储图片,不需要申请MANAGE_EXTERNAL_STORAGE权限:

  • 权限请求是异步操作,授权完成前就执行了加载逻辑:requestPermissions不是同步阻塞方法,调用后会立刻执行后续代码,此时用户还没完成授权,应用根本没有文件读取权限,自然加载失败。需要等权限回调确认授权后再初始化WebView加载内容。
  • WebView安全配置不全:仅开启setAllowFileAccess(true)不足以加载本地文件,还需要开启setAllowContentAccess(true)允许WebView访问content provider资源、setAllowFileAccessFromFileURLs(true)允许页面加载本地file路径资源。使用loadDataWithBaseURL(null, ...)加载的页面源为about:blank,默认会拦截所有本地file资源请求。
  • 文件路径拼接错误:Environment.getExternalStorageDirectory().absolutePath返回的绝对路径格式为/storage/emulated/0,拼接file协议头的正确格式为file://+绝对路径,最终图片路径应为file:///storage/emulated/0/Download/MonaLisa.jpg,斜杠数量错误会导致路径解析失败。

可直接运行的修复代码

第一步:确认Manifest配置

除已添加的读写权限外,不需要额外加requestLegacyExternalStorage属性,该属性在Android 11上已失效。如果项目targetSdkVersion>=33,将READ_EXTERNAL_STORAGE替换为READ_MEDIA_IMAGES权限即可。

<!-- targetSdkVersion <=32 用这两个权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- targetSdkVersion >=33 用下面的权限替换上面的读权限
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
-->

第二步:修复Activity逻辑

package com.example.hellowebview

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat

class MainActivity : AppCompatActivity() {
    private val PERMISSION_CODE = 1001
    private lateinit var myWebView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        myWebView = WebView(this)
        setContentView(myWebView)

        // 适配不同版本的读图片权限
        val readPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            Manifest.permission.READ_MEDIA_IMAGES
        } else {
            Manifest.permission.READ_EXTERNAL_STORAGE
        }

        // 已授权直接加载,未授权才发起申请
        if (ContextCompat.checkSelfPermission(this, readPermission) == PackageManager.PERMISSION_GRANTED) {
            loadPageWithSdImage()
        } else {
            ActivityCompat.requestPermissions(this, arrayOf(readPermission), PERMISSION_CODE)
        }
    }

    // 权限申请结果回调
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == PERMISSION_CODE && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            loadPageWithSdImage()
        }
    }

    private fun loadPageWithSdImage() {
        // 完整配置WebView本地访问权限
        val webSettings = myWebView.settings
        webSettings.allowFileAccess = true
        webSettings.allowContentAccess = true
        webSettings.allowFileAccessFromFileURLs = true

        // 正确拼接图片路径
        val sdRoot = Environment.getExternalStorageDirectory().absolutePath
        val targetImage = "file://$sdRoot/Download/MonaLisa.jpg"
        val pageHtml = """
            <HTML>
                <BODY>
                    <h3>Hello, WebView!</h3>
                    <img src="$targetImage">
                </BODY>
            </HTML>
        """.trimIndent()

        // 传入本地根路径作为baseUrl,避免跨源拦截
        myWebView.loadDataWithBaseURL("file://$sdRoot/", pageHtml, "text/html", "utf-8", null)
    }
}

补充说明

如果配置完成后仍加载失败,大概率是USB拷贝的图片还未被系统媒体库扫描识别,重启手机或手动触发一次媒体扫描即可正常访问。
如果不想处理WebView跨源和路径兼容问题,更稳妥的方案是自定义WebViewClient重写shouldInterceptRequest方法,拦截img标签的资源请求,原生读取文件流返回给WebView,这种方式完全绕开WebView自带的文件访问限制,兼容性更高。

内容的提问来源于stack exchange,提问作者Weather Vane

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 11:01:23