从相册加载多张图片引发OutOfMemoryError,求内存优化方案
内存溢出问题分析与优化方案
你的应用在选择超过5张图片时崩溃,根源是一次性将所有原图转换为Base64字符串并存储在内存中:
- 手机拍摄的原图通常有数MB大小,转换为Base64后体积会增加约33%
- 多张图片的Base64字符串和JSON数据同时驻留内存,直接触发
OutOfMemoryError
以下是针对性的优化方案,按优先级从高到低排序:
1. 彻底规避Base64:用FileProvider共享文件给WebView
这是最有效的解决方案,完全避免将图片数据加载到内存中。核心思路是通过FileProvider将本地图片Uri共享给WebView,让JS直接读取文件内容,而非传递Base64字符串。
步骤:
(1)配置FileProvider
在AndroidManifest.xml中添加:
<application> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> </application>
在res/xml下创建file_paths.xml:
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-files-path name="my_images" path="Pictures" /> <!-- 根据你的图片存储路径调整,这里以应用外部存储的Pictures目录为例 --> </paths>
(2)修改图片处理逻辑
不需要转Base64,直接获取可共享的Uri:
// 替换原有的convertUriToBase64逻辑,获取可共享的Uri private fun getShareableUri(uri: Uri): Uri? { return try { // 将Uri对应的文件复制到应用可共享目录(避免权限问题) val inputStream = contentResolver.openInputStream(uri) ?: return null val fileName = getFileNameWithExtension(uri) val outputFile = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), fileName) FileOutputStream(outputFile).use { outputStream -> inputStream.copyTo(outputStream) } // 生成FileProvider的Uri FileProvider.getUriForFile(this, "${packageName}.fileprovider", outputFile) } catch (e: Exception) { e.printStackTrace() null } }
(3)修改JSBridge传递数据
传递共享Uri而非Base64:
fun sendData(imageFiles: List<Triple<Uri, String, String>>) { val jsonArray = JSONArray() for (imageFile in imageFiles) { val jsonObject = JSONObject() jsonObject.put("type", imageFile.second) jsonObject.put("name", imageFile.third) jsonObject.put("uri", imageFile.first.toString()) // 传递Uri字符串 jsonArray.put(jsonObject) } val jsonData = """{ "method": "upload_files_from_app", "result": { "imageFiles": $jsonArray } }""".trimIndent() webView.evaluateJavascript("javascript: window.mobile2web.onMessage($jsonData)", null) }
(4)JS端读取文件
JS通过Uri读取文件:
function onMessage(data) { data.result.imageFiles.forEach(async (fileInfo) => { const response = await fetch(fileInfo.uri); const blob = await response.blob(); // 后续处理blob(如上传) }); }
2. 压缩图片后再转换Base64
如果必须使用Base64,先对图片进行采样压缩,大幅降低内存占用。
替换原有的convertUriToBase64函数:
private fun convertUriToCompressedBase64(uri: Uri, maxWidth: Int = 1024, maxHeight: Int = 1024): String? { return try { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true // 先获取图片尺寸,不加载到内存 contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, this) } // 计算采样率 inSampleSize = calculateInSampleSize(this, maxWidth, maxHeight) inJustDecodeBounds = false // 准备加载压缩后的图片 } val bitmap = contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, options) } ?: return null // 将Bitmap转成字节数组(可选指定质量) val byteArrayOutputStream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream) // 80为压缩质量(0-100) val byteArray = byteArrayOutputStream.toByteArray() bitmap.recycle() // 及时回收Bitmap内存 Base64.encodeToString(byteArray, Base64.DEFAULT) } catch (e: Exception) { e.printStackTrace() null } } private fun calculateInSampleSize(options: BitmapFactory.Options, maxWidth: Int, maxHeight: Int): Int { val height = options.outHeight val width = options.outWidth var inSampleSize = 1 if (height > maxHeight || width > maxWidth) { val halfHeight = height / 2 val halfWidth = width / 2 while (halfHeight / inSampleSize >= maxHeight && halfWidth / inSampleSize >= maxWidth) { inSampleSize *= 2 } } return inSampleSize }
3. 分批处理图片,避免一次性加载全部
修改onActivityResult,分批处理并传递图片,减少内存峰值:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == PICK_IMAGE_REQUEST_CODE && resultCode == RESULT_OK) { val uris = mutableListOf<Uri>() // 先收集所有Uri,不立即处理 if (data?.clipData != null) { val clipData = data.clipData for (i in 0 until clipData!!.itemCount) { uris.add(clipData.getItemAt(i).uri) } } else { data?.data?.let { uris.add(it) } } // 分批处理,比如每次处理2张 val batchSize = 2 for (i in uris.indices step batchSize) { val batch = uris.subList(i, minOf(i + batchSize, uris.size)) val selectedImageBlobs = mutableListOf<Triple<String, String, String>>() batch.forEach { uri -> val base64Image = convertUriToCompressedBase64(uri) // 使用压缩后的转换函数 val mimeType = contentResolver.getType(uri) val fileName = getFileNameWithExtension(uri) if (base64Image != null && mimeType != null && fileName.isNotEmpty()) { selectedImageBlobs.add(Triple(base64Image, mimeType, fileName)) } } jsBridge.sendData(selectedImageBlobs) // 清空当前批次的列表,帮助GC回收 selectedImageBlobs.clear() } } }
4. 优化Base64转换的内存占用
原convertUriToBase64会将整个文件读入ByteArray,再转Base64,内存中同时存在原字节数组和Base64字符串。可以直接通过Base64OutputStream边读边转,减少内存峰值:
private fun convertUriToBase64Stream(uri: Uri): String? { return try { val inputStream = contentResolver.openInputStream(uri) ?: return null val outputStream = ByteArrayOutputStream() val base64OutputStream = Base64OutputStream(outputStream, Base64.DEFAULT) inputStream.use { input -> base64OutputStream.use { base64Output -> input.copyTo(base64Output) } } outputStream.toString() } catch (e: Exception) { e.printStackTrace() null } }
5. 基础内存优化措施
- 及时释放资源:在使用完
InputStream、Bitmap等资源后,务必调用close()或recycle(),避免内存泄漏 - 调整WebView设置:关闭不必要的WebView功能,减少内存占用:
settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.allowFileAccess = true // 如果需要访问本地文件 settings.cacheMode = WebSettings.LOAD_NO_CACHE // 根据需求调整缓存策略 - 在Activity销毁时清理WebView:
override fun onDestroy() { myWebView.removeJavascriptInterface("androidBridge") myWebView.loadUrl("about:blank") myWebView.stopLoading() myWebView.webViewClient = null myWebView.webChromeClient = null myWebView.destroy() super.onDestroy() }
内容的提问来源于stack exchange,提问作者Roman Kozulia
相关产品推荐
相关产品推荐

