Android图片全文搜索应用优化:大量图片处理进程终止问题
问题背景
我计划用Android Studio + Java开发一款支持图片全文搜索的Android应用,当前实现逻辑如下:
- 获取所有图片路径
private ArrayList<String> getAllImagesPath(){ Uri uri; Cursor cursor; int columIndexData; ArrayList<String> imagesPath=new ArrayList<>(); uri= MediaStore.Images.Media.EXTERNAL_CONTENT_URI; String[] projection ={MediaStore.MediaColumns.DATA, MediaStore.Images.Media.BUCKET_DISPLAY_NAME}; cursor=MainActivity.this.getContentResolver().query(uri,projection,null,null,null); columIndexData=cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA); try { while (cursor.moveToNext()) { String imagePath = cursor.getString(columIndexData); File imgFile = new File(imagePath); if (imgFile.exists()) { imagesPath.add(imagePath); Log.d("ImagePath", imagePath); } } Log.d("ImagePath", String.valueOf(cursor.getCount())); } catch (Exception e) { e.printStackTrace(); } finally { cursor.close(); } return imagesPath; }
- 提取图片文本(使用Google ML Kit的OCR)
private String getTextFromImage(Bitmap bitmap){ TextRecognizer recognizer=new TextRecognizer.Builder(MainActivity.this).build(); if (!recognizer.isOperational()){ Log.d("ImagePath", "Error getTextFromImage"); return null; } else { Frame frame=new Frame.Builder().setBitmap(bitmap).build(); SparseArray<TextBlock> textBlockSparseArray=recognizer.detect(frame); StringBuilder stringBuilder=new StringBuilder(); for (int i=0; i<textBlockSparseArray.size();i++){ TextBlock textBlock=textBlockSparseArray.valueAt(i); stringBuilder.append(textBlock.getValue()); stringBuilder.append("\n"); } Log.d("ImagePath", stringBuilder.toString()); return stringBuilder.toString(); } }
- 后台线程处理任务
ExecutorService executor = Executors.newSingleThreadExecutor(); ArrayList<String> imagesPaths = getAllImagesPath(); executor.submit(new Runnable() { @Override public void run() { for (String imagePath : imagesPaths) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); String text = getTextFromImage(bitmap); bitmap.recycle(); AddToDataBase(imagePath, text); } } });
当前逻辑处理200张以内图片正常,但处理大量图片时应用进程会直接终止,需要优化代码以支持无中断处理大量图片。
优化方案
1. 压缩Bitmap,避免内存溢出
直接加载原图会占用大量内存,尤其是高分辨率图片,必须通过采样压缩减少内存占用:
private Bitmap getCompressedBitmap(String imagePath) { BitmapFactory.Options options = new BitmapFactory.Options(); // 先读取图片尺寸,不加载到内存 options.inJustDecodeBounds = true; BitmapFactory.decodeFile(imagePath, options); // 计算采样率,目标宽高不超过1024,可按需调整 int maxSize = 1024; int width = options.outWidth; int height = options.outHeight; int inSampleSize = 1; if (height > maxSize || width > maxSize) { int halfHeight = height / 2; int halfWidth = width / 2; while ((halfHeight / inSampleSize) >= maxSize && (halfWidth / inSampleSize) >= maxSize) { inSampleSize *= 2; } } options.inJustDecodeBounds = false; options.inSampleSize = inSampleSize; options.inPreferredConfig = Bitmap.Config.RGB_565; // 减少内存占用 return BitmapFactory.decodeFile(imagePath, options); }
替换原代码中的Bitmap bitmap = BitmapFactory.decodeFile(imagePath);为Bitmap bitmap = getCompressedBitmap(imagePath);。
2. 复用TextRecognizer实例
每次调用getTextFromImage新建实例会重复初始化模型,浪费资源,改为全局复用:
// Activity中声明全局变量 private TextRecognizer textRecognizer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textRecognizer = new TextRecognizer.Builder(this).build(); if (!textRecognizer.isOperational()) { Log.d("ImagePath", "OCR模型加载失败,请检查权限或网络"); } } // 修改OCR方法,复用全局实例 private String getTextFromImage(Bitmap bitmap){ if (!textRecognizer.isOperational()){ Log.d("ImagePath", "OCR不可用"); return null; } Frame frame=new Frame.Builder().setBitmap(bitmap).build(); SparseArray<TextBlock> textBlockSparseArray=textRecognizer.detect(frame); StringBuilder stringBuilder=new StringBuilder(); for (int i=0; i<textBlockSparseArray.size();i++){ TextBlock textBlock=textBlockSparseArray.valueAt(i); stringBuilder.append(textBlock.getValue()); stringBuilder.append("\n"); } return stringBuilder.toString(); } @Override protected void onDestroy() { super.onDestroy(); if (textRecognizer != null) { textRecognizer.release(); // 释放资源 } }
3. 改用WorkManager处理后台任务
单线程ExecutorService在应用退到后台时易被系统杀死,WorkManager可保证任务后台持续执行,支持断点续传:
步骤1:添加依赖(build.gradle)
dependencies { implementation "androidx.work:work-runtime:2.8.1" }
步骤2:创建Worker类
public class ImageOcrWorker extends Worker { private TextRecognizer textRecognizer; public static final String KEY_IMAGE_PATHS = "image_paths"; public ImageOcrWorker(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); textRecognizer = new TextRecognizer.Builder(context).build(); } @NonNull @Override public Result doWork() { List<String> imagePaths = getInputData().getStringArrayList(KEY_IMAGE_PATHS); if (imagePaths == null || imagePaths.isEmpty()) { return Result.failure(); } // 读取已处理记录,避免重复处理 Set<String> processedPaths = getProcessedPaths(); List<ImageTextPair> batchList = new ArrayList<>(); int batchSize = 50; // 批量插入阈值 for (String path : imagePaths) { if (processedPaths.contains(path)) continue; try { Bitmap bitmap = getCompressedBitmap(path); String text = getTextFromImage(bitmap); bitmap.recycle(); batchList.add(new ImageTextPair(path, text)); saveProcessedPath(path); // 批量插入数据库 if (batchList.size() >= batchSize) { batchInsertToDatabase(batchList); batchList.clear(); } } catch (Exception e) { Log.e("ImageOcrWorker", "处理图片失败: " + path, e); continue; // 单个图片失败不终止任务 } } // 插入剩余数据 if (!batchList.isEmpty()) { batchInsertToDatabase(batchList); } textRecognizer.release(); return Result.success(); } // 复用压缩、OCR方法(同之前实现) private Bitmap getCompressedBitmap(String imagePath) {} private String getTextFromImage(Bitmap bitmap) {} // 记录已处理路径(用SharedPreferences存储) private Set<String> getProcessedPaths() { SharedPreferences sp = getApplicationContext().getSharedPreferences("OCR_PROCESS", Context.MODE_PRIVATE); return sp.getStringSet("processed_paths", new HashSet<>()); } private void saveProcessedPath(String path) { SharedPreferences sp = getApplicationContext().getSharedPreferences("OCR_PROCESS", Context.MODE_PRIVATE); Set<String> processed = new HashSet<>(getProcessedPaths()); processed.add(path); sp.edit().putStringSet("processed_paths", processed).apply(); } // 批量插入数据库 private void batchInsertToDatabase(List<ImageTextPair> pairs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); try { db.beginTransaction(); for (ImageTextPair pair : pairs) { ContentValues values = new ContentValues(); values.put("image_path", pair.path); values.put("extracted_text", pair.text); db.insert("image_text", null, values); } db.setTransactionSuccessful(); } finally { db.endTransaction(); db.close(); } } static class ImageTextPair { String path; String text; ImageTextPair(String path, String text) { this.path = path; this.text = text; } } }
步骤3:启动任务
// 拆分图片路径为批次,避免单次传递过大数据 List<String> imagePaths = getAllImagesPath(); List<List<String>> splitPaths = splitList(imagePaths, 100); for (List<String> batch : splitPaths) { Data inputData = new Data.Builder() .putStringArrayList(ImageOcrWorker.KEY_IMAGE_PATHS, new ArrayList<>(batch)) .build(); OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(ImageOcrWorker.class) .setInputData(inputData) .setConstraints(new Constraints.Builder() .setRequiresBatteryNotLow(true) .setRequiresStorageNotLow(true) .build()) .build(); WorkManager.getInstance(this).enqueue(workRequest); } // 列表拆分工具方法 private <T> List<List<T>> splitList(List<T> list, int batchSize) { List<List<T>> result = new ArrayList<>(); for (int i = 0; i < list.size(); i += batchSize) { int end = Math.min(i + batchSize, list.size()); result.add(list.subList(i, end)); } return result; }
4. 适配Android Q+的图片路径获取
MediaStore.MediaColumns.DATA在API 29后废弃,改用Content URI获取图片:
private List<String> getAllImagesPath() { List<String> imageUris = new ArrayList<>(); Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; String[] projection = {MediaStore.Images.Media._ID}; Cursor cursor = getContentResolver().query(uri, projection, null, null, null); if (cursor != null) { int idIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID); while (cursor.moveToNext()) { long id = cursor.getLong(idIndex); Uri imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); imageUris.add(imageUri.toString()); } cursor.close(); } return imageUris; } // 加载Bitmap时改用ContentResolver打开流 private Bitmap getCompressedBitmap(String imageUriStr) { Uri imageUri = Uri.parse(imageUriStr); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; try (InputStream is = getContentResolver().openInputStream(imageUri)) { BitmapFactory.decodeStream(is, null, options); } catch (IOException e) { e.printStackTrace(); return null; } // 计算采样率(同之前逻辑) int maxSize = 1024; int width = options.outWidth; int height = options.outHeight; int inSampleSize = 1; if (height > maxSize || width > maxSize) { int halfHeight = height / 2; int halfWidth = width / 2; while ((halfHeight / inSampleSize) >= maxSize && (halfWidth / inSampleSize) >= maxSize) { inSampleSize *= 2; } } options.inJustDecodeBounds = false; options.inSampleSize = inSampleSize; options.inPreferredConfig = Bitmap.Config.RGB_565; try (InputStream is = getContentResolver().openInputStream(imageUri)) { return BitmapFactory.decodeStream(is, null, options); } catch (IOException e) { e.printStackTrace(); return null; } }
5. 内存监控与主动GC
在循环处理中加入内存监控,避免占用过高:
private void checkMemory() { Runtime runtime = Runtime.getRuntime(); long usedMem = runtime.totalMemory() - runtime.freeMemory(); long maxMem = runtime.maxMemory(); float usagePercent = (float) usedMem / maxMem * 100; if (usagePercent > 80) { System.gc(); try { Thread.sleep(100); // 等待GC完成 } catch (InterruptedException e) { e.printStackTrace(); } } } // 在处理循环中调用 for (String path : imagePaths) { checkMemory(); // ...处理图片逻辑... }
内容的提问来源于stack exchange,提问作者Ryah AL-aidy
相关产品推荐
相关产品推荐

