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

