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

从相册加载多张图片引发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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 20:27:01