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

Android图片全文搜索应用优化:大量图片处理进程终止问题

问题背景

我计划用Android Studio + Java开发一款支持图片全文搜索的Android应用,当前实现逻辑如下:

  1. 获取所有图片路径
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;
}
  1. 提取图片文本(使用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();
    }
}
  1. 后台线程处理任务
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 05:27:03