之前写过一篇关于从零开始把小番茄图片混淆做成安卓App的文章自己使用过程中需要处理多张图。 确实上一版只能一张一张选。想混淆一堆图的时候要重复选图、处理、保存很多轮来回折腾效率很低。这次把批量模式加上了。这篇文章记录的不是最终效果而是怎么加的、踩了什么坑、怎么填的。如果也在做类似的功能迭代或者刚入门安卓开发想看看一个功能是怎么一步步落地的这篇应该能提供一些参考。批量功能做了什么先明确功能边界一次选多张图统一加密或解密批量保存操作流程点击选择图片弹窗提示选择多张图片进入系统相册长按任意图片进入多选模式勾选需要的图片选完返回App加载图片列表显示第一张预览和当前序号比如1/10调节混淆次数1-99次点击加密或解密后台逐张处理进度条显示当前进度和文件名全部处理完后出现一键保存按钮点击后一次性保存所有结果到相册通过上一张/下一张浏览处理结果可以随时还原某张图回到原始状态处理完一批后点击清空列表释放资源退出批量模式流程看起来顺滑但实际开发中踩了不少坑遇到的主要问题如下:1. 多图内存爆炸OOM单图处理时一张4000x3000的照片加载到内存大约占48MB4000×3000×4字节。Android设备给单个App的内存上限一般在128MB-256MB之间单图能扛住。批量模式如果每张图都全尺寸加载连续处理几张后内存峰值可能超过200MB低端手机直接闪退。解决思路第一逐张处理不保留历史。 处理循环里每张图加载→处理→保存缓存→立即回收Bitmap然后加载下一张。同一时刻内存里最多只有一张原图和一张结果图。第二预览图采样加载。 预览时不显示原图尺寸只加载缩略图。用inSampleSize控制采样率把图片缩放到屏幕宽高范围内。val optionsBitmapFactory.Options().apply{inSampleSizecalculateInSampleSize(options, reqWidth, reqHeight)}BitmapFactory.decodeFile(path, options)第三处理结果存文件不存内存。 处理完的图片以PNG格式缓存在cacheDir里预览时再从文件加载同样采样加载。不管处理多少张内存里永远只有当前预览的一张。bitmap?.recycle()这三层做完后一次处理30张图内存也能稳定在安全范围内。2. 保存后相册里看不到图片刚开始保存逻辑用的是传统写法直接把文件写到Pictures/NoiseVault目录下。文件确实写进去了文件管理器能看得到但系统相册刷不出来。查了文档才发现Android 10以后分区存储机制变了。直接写文件系统媒体库不会自动扫描必须用MediaStore API通知系统。解决思路改用MediaStore API。流程是创建ContentValues指定文件名、MIME类型、存储路径通过ContentResolver插入记录得到Uri通过Uri打开输出流写入图片数据关闭流后通知系统更新val contentValuesContentValues().apply{put(MediaStore.Images.Media.DISPLAY_NAME, filename)put(MediaStore.Images.Media.MIME_TYPE,image/png)put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES /NoiseVault)}val uricontentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)uri?.let{contentResolver.openOutputStream(it)?.use{stream -bitmap.compress(Bitmap.CompressFormat.PNG,100, stream)}contentResolver.notifyChange(it, null)}这样保存后相册里会立刻出现Pictures/NoiseVault文件夹图片正常显示且保留了原始拍摄时间、设备信息等EXIF数据。还有一个细节 IS_PENDING机制。写入过程中把IS_PENDING设为1表示文件正在写入其他应用不会读取到半成品。写入完成后设为0系统才显示这个文件。批量保存多张图时能防止相册出现损坏的缩略图。3. 防重复保存和防连点测试时发现一个问题处理完成前多次点击一键保存或者处理过程中点击其他按钮会出现重复保存、数据错乱甚至崩溃。解决思路加三个状态锁isProcessing正在处理中禁止其他操作isSaving正在保存中禁止再次保存batchSaved当前批次已保存不能再存一次if(isProcessing||isSaving||batchSaved){Toast.makeText(this,当前批次已完成请清空列表重新操作, Toast.LENGTH_SHORT).show()return}处理开始时禁用所有操作按钮处理完成后才恢复。狂点也不会触发二次执行。4. 叠加混淆的数据源切换用户可以在已有结果上继续加密或解密叠加混淆。比如一张图加密一次后还可以再加密一次次数会累积。问题在于第一次处理从原始URI读取图片叠加处理要从缓存文件读取不能再从原始URI读。解决思路用一个batchHasResult标志判断当前数据源batchHasResult false从原始URI列表读取batchHasResult true从缓存文件列表读取val bmpif(batchHasResultcurrentIndextempResultFiles.size){// 有处理结果 → 从缓存读取 BitmapUtils.loadBitmapSampled(tempResultFiles[currentIndex].absolutePath,...)}elseif(currentIndeximageUris.size){// 无处理结果 → 从原始 URI 读取 BitmapUtils.loadBitmapFromUriSampled(contentResolver, imageUris[currentIndex],...)}else{null}首次处理时把原始URI对应的图片缓存到batchOriginalFiles用于后续还原操作。叠加处理时不再覆盖这批原始缓存。5. 单图与批量模式的切换冲突单图模式和批量模式共用同一个界面、同一个预览区域、同一套按钮。切换时数据没清干净会出现空指针或残留数据。解决思路每次切换模式时调用统一的clearBatch()方法把所有相关变量全部清空private funclearBatch(){imageUris.clear()tempResultFiles.forEach{if(it.exists())it.delete()}tempResultFiles.clear()batchOriginalFiles.forEach{if(it.exists())it.delete()}batchOriginalFiles.clear()currentDisplayFilenull currentBitmap?.recycle()currentBitmapnull batchHasResultfalsebatchSavedfalsebatchModefalsecurrentIndex0}切回单图模式时不会残留批量模式的数据反之亦然。6. 进度反馈要具体只显示处理中…不知道进度到哪里了也不确定有没有卡死。解决思路进度回调里同时传递当前索引、总数和当前处理的文件名onProgress:(current: Int, total: Int, fileName: String)-Unit界面上显示处理中3/20 IMG_20260801.jpg。能清楚知道进度心里有数。数据结构批量模式涉及的数据比单图多如果做规划容易越写越乱。变量名 类型 作用batchMode Boolean 当前是否处于批量模式imageUris List 原始图片的URI列表currentIndex Int 当前预览的图片索引tempResultFiles List 处理结果的缓存文件列表PNG格式batchOriginalFiles List 原始图片的缓存文件用于还原isProcessing Boolean 是否正在处理中防连点isSaving Boolean 是否正在保存中防连点batchHasResult Boolean 当前批次是否有处理结果batchSaved Boolean 当前批次是否已保存currentDisplayFile File? 当前预览的文件用于判断还原各自承担明确的职责互不重叠。调试时检查对应的变量值能快速定位问题。实际数据对比:单图版:约4-6分钟含反复操作处理10张图的操作次数 选图10次 处理10次 保存10次 30步 选图1次 处理1次 保存1次 3步统一次数每次单独调每张独立处理批量版约1-2分钟自动运行统一次数 一次设定全部生效批量叠加一次完成效率提升明显这也是这次迭代的核心价值。源码地址:项目已开源包含单图版和批量版功能github项目地址Releases里有最新APK。补充说明关于单图版本的基础实现和核心算法原理上一篇《从JavaScript到Kotlin移植Hilbert曲线图片混淆到安卓的经历》里已经详细写过可以翻回去看看。