Android 媒體庫讀寫優化

一、快速查詢手機中的圖片和視頻 

本方案適合通過媒體庫實現快速查詢視頻和圖片,對於SD卡掃描,也可以參考。

我們知道,媒體庫屬於數據庫,CURD數據庫屬於IO操作,但是數據的IO相對特殊,很難使用一次拷貝,共享內存方式去優化,因此往往都是通過多線程操作去處理。一次查詢所有圖片和視頻,極端情況下可能出現ANR或者查詢慢的問題,因爲媒體庫接口時通過ContentProvider暴露的,雖然可以Hack ActivityThread使得ContentProviderClient不去發送ANR Error,但這種方式畢竟不太好。

 

二、方案設計

可以採用分段查詢方式,創建初始任務時 圖片和視頻比與線程最大核心線程數相關,如果最大核心線程數爲N(N必須大於4),那麼圖片線程爲(N-2) : 視頻線程爲2,因爲絕大多數情況下,手機中的視頻數量比圖片少很多倍,線程數量按分段方式查詢,如果查詢數量等於分頁數目,那麼繼續創建線程查詢下一頁,最終所有線程收斂

public class MediaQueryTaskManager implements Runnable {

    private Context context;
    private ThreadPoolExecutor executor;
    private List<QueryTask> futureTasks;
    private Thread monitorThread;
    private Handler monitorHandler;
    private int imagePageIndex = 0;
    private int videoPageIndex = 0;
    private AtomicInteger taskCounter;
    private final int PAGE_SIZE = 1000;
    private QueryResultListener listener;
    private final int coreThreadNum = 5;

    public MediaQueryTaskManager(Context context) {
        this.context = context.getApplicationContext();
        this.futureTasks = new LinkedList<>();
        this.taskCounter = new AtomicInteger();
    }

    public ThenTask getTask() {
        ThenTask thenTask = new ThenTask(this);
        return thenTask;
    }

    @SuppressLint("NewApi")
    @Override
    public void run() {
        Looper.prepare();

        final List<QueryResult.MediaInfo> videoList = new ArrayList<>();
        final List<QueryResult.MediaInfo> imageList = new ArrayList<>();
        final long startQueryTime = System.currentTimeMillis();
        Handler.Callback callback = new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {

                if (msg.what == QueryTask.MSG_START) {
                    futureTasks.add((QueryTask) msg.obj);
                } else if (msg.what == QueryTask.MSG_FINISH) {
                    QueryTask queryTask = (QueryTask) msg.obj;
                    futureTasks.remove(queryTask);
                    taskCounter.decrementAndGet();
                    QueryResult queryResult = queryTask.getQueryResult();

                    if (queryTask.requestVideo) {
                        List<QueryResult.MediaInfo> videos = queryResult.getVideos();
                        if (videos != null) {
                            if (!videos.isEmpty()) {
                                videoList.addAll(videos);
                            }
                            int size = videos.size();
                            Log.d("QueryResult", "查詢結束 : 本次查詢視頻: " + size);
                            if (size >= PAGE_SIZE) {
                                startNextPageVideos(monitorHandler);
                            }
                        }
                    } else {
                        List<QueryResult.MediaInfo> images = queryResult.getImages();
                        if (!images.isEmpty()) {
                            imageList.addAll(images);
                        }
                        int size = images.size();
                        Log.d("QueryResult", "查詢結束 : 本次查詢圖片: " + size);
                        if (size >= PAGE_SIZE) {
                            startNextPageImages(monitorHandler);
                        }
                    }
                    Log.d("QueryResult", "當前查詢任務數量" + futureTasks.size() + " , " + taskCounter.get());
                    if (taskCounter.get() == 0) {
                        Message message = Message.obtain(monitorHandler);
                        message.what = QueryTask.MSG_ALL_FINISH;
                        message.sendToTarget();
                    }

                } else if (msg.what == QueryTask.MSG_ALL_FINISH) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                        monitorHandler.getLooper().quitSafely();
                    } else {
                        monitorHandler.getLooper().quit();
                    }
                }
                return false;
            }
        };
        monitorHandler = new Handler(Looper.myLooper(), callback);
        startQueryTaskOnReady(monitorHandler);

        Looper.loop();

        QueryResult queryResult = new QueryResult();
        synchronized (queryResult) {
            queryResult.videoIndex = videoPageIndex;
            queryResult.imageIndex = imagePageIndex;
        }
        queryResult.videos = videoList;
        queryResult.images = imageList;
        queryResult.costTime = (System.currentTimeMillis() - startQueryTime);
        Log.e("QueryResult", "查詢結束 : " + queryResult + ", 查詢耗時:" + queryResult.costTime + ", Looper exit ");
        this.listener.onResult(queryResult);

        if (this.executor != null) {
            this.executor.shutdown();
        }

    }

    private void startNextPageVideos(Handler handler) {
        QueryTask queryTask = new QueryTask(context, true, videoPageIndex, PAGE_SIZE, handler);
        startQueryTask(queryTask);
        videoPageIndex++;
    }

    private void startNextPageImages(Handler handler) {
        QueryTask queryTask = new QueryTask(context, false, imagePageIndex, PAGE_SIZE, handler);
        startQueryTask(queryTask);
        imagePageIndex++;
    }

    private void startQueryTaskOnReady(Handler handler) {

        startNextPageImages(handler);
        startNextPageImages(handler);
        startNextPageImages(handler);
        startNextPageVideos(handler);
        startNextPageVideos(handler);
    }

    private void startQueryTask(QueryTask task) {
        this.taskCounter.incrementAndGet();
        this.executor.execute(task);
    }

    static class QueryTask implements Runnable {

        private Handler handler;
        private boolean requestVideo;
        private int pageIndex;
        private Context context;
        private int pageSize;
        private static int MSG_FINISH = 2;
        private static int MSG_ALL_FINISH = 3;
        private static int MSG_START = 1;
        private QueryResult queryResult;

        public QueryResult getQueryResult() {
            return queryResult;
        }

        public QueryTask(Context context, boolean requestVideo, int pageIndex, int pageSize, Handler handler) {
            this.pageIndex = pageIndex;
            this.pageSize = pageSize;
            this.context = context;
            this.handler = handler;
            this.requestVideo = requestVideo;
        }

        @Override
        public void run() {
            try {
                onStartTask(this);
                MediaQuery fastMediaQuery = new MediaQuery(context, requestVideo, pageIndex, pageSize);
                long startQueryTime = System.currentTimeMillis();

                List<QueryResult.MediaInfo> result = fastMediaQuery.call();
                QueryResult queryResult = new QueryResult();
                queryResult.costTime = (System.currentTimeMillis() - startQueryTime);
                if (requestVideo) {
                    queryResult.videos = result;
                    queryResult.videoIndex = pageIndex;
                } else {
                    queryResult.images = result;
                    queryResult.imageIndex = pageIndex;
                }
                this.queryResult = queryResult;

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                onFinishTask(this);
            }
        }

        private void onFinishTask(QueryTask queryTask) {
            Message msg = Message.obtain(handler);
            msg.what = MSG_FINISH;
            msg.obj = queryTask;
            msg.sendToTarget();
        }

        private void onStartTask(QueryTask queryTask) {
            Message msg = Message.obtain(handler);
            msg.what = MSG_START;
            msg.obj = queryTask;
            msg.sendToTarget();
        }
    }

    public static class ThenTask {
        private MediaQueryTaskManager initTask;

        public ThenTask(MediaQueryTaskManager initTask) {
            this.initTask = initTask;
        }

        public void doThen(QueryResultListener listener) {
            if (listener == null) return;
            initTask.start(listener);
        }
    }

    private void start(QueryResultListener listener) {
        this.listener = listener;
        this.monitorThread = new Thread(this);

        int maxCores = Math.max(Runtime.getRuntime().availableProcessors(), coreThreadNum) + 1;
        this.executor = new ThreadPoolExecutor(coreThreadNum, maxCores, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                Log.d("QueryResult", "查詢失敗 : rejectedExecution = " + r);
            }
        });
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            this.executor.allowCoreThreadTimeOut(true);
        }
        this.monitorThread.start();
    }

    public interface QueryResultListener {
        public void onResult(QueryResult queryResult);
    }

}

 

【2】創建查詢任務

public class MediaQuery implements Callable<List<QueryResult.MediaInfo>> {

    private final int pageIndex;
    private final boolean requestVideo;
    private Context context;
    private int PAGE_SIZE = 500;

    public MediaQuery(Context context, boolean requestVideo, int pageIndex, int pageSize) {
        this.context = context;
        this.pageIndex = pageIndex;
        this.PAGE_SIZE = pageSize;
        this.requestVideo = requestVideo;
    }

    private List<QueryResult.MediaInfo> queryMediaStoreFiles(int pageIndex, boolean isVideo, String orderBy, String[] likeStatements) {
        List<QueryResult.MediaInfo> mediaInfos = null;
        Uri MediaStoreUri = isVideo ? MediaStore.Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        Cursor cursor = null;
        // 獲得圖片
        try {
            String[] QUERY_FIELDS = getQueryFields(isVideo); //修改時間
            String[] selectionArgs = null;
            String whereStatement = null;

            if (likeStatements != null  && likeStatements.length > 0) {

                File dirDirectory = Environment.getExternalStorageDirectory();
                selectionArgs = new String[likeStatements.length];
                StringBuilder whereLikeBuilder = new StringBuilder();
                int i = 0;
                for ( String path : likeStatements ) {
                    if(whereLikeBuilder.length()>0){
                        whereLikeBuilder.append(" OR ");
                    }
                    whereLikeBuilder.append(" ").append(MediaStore.Images.Media.DATA).append(" ").append(" LIKE ").append(" ?");
                    selectionArgs[i++] = dirDirectory.getAbsolutePath()+"/" + path +"%";
                }

                whereStatement = whereLikeBuilder.toString();
            }

            StringBuilder orderByWhere = new StringBuilder();
            String LIMIT = " LIMIT " + (pageIndex * PAGE_SIZE) + "," + PAGE_SIZE;

            if (!TextUtils.isEmpty(orderBy)) {
                orderByWhere.append(" ").append(orderBy).append(" ");
            }
            orderByWhere.append(LIMIT);
            String groupOrderLimit = orderByWhere.toString();

            cursor = context.getContentResolver().query(MediaStoreUri, QUERY_FIELDS,
                    whereStatement, selectionArgs, groupOrderLimit);
            mediaInfos = readMediaInfo(cursor, isVideo);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return mediaInfos;
    }

    private String[] getQueryFields(boolean isVideo) {
        if (isVideo) {
            return new String[]{
                    MediaStore.Video.Media.DATA,
                    MediaStore.Video.Media.SIZE,
                    MediaStore.Video.Media.DURATION,
                    MediaStore.Video.Media.DATE_MODIFIED,
                    MediaStore.Video.Media.DATE_TAKEN,
                    MediaStore.Video.Media.DISPLAY_NAME,
                    MediaStore.Video.Media.MIME_TYPE
            };
        }

        return new String[]{
                MediaStore.Images.Media.DATA    //路徑
                , MediaStore.Images.Media.SIZE    //大小
                , MediaStore.Images.Media.DISPLAY_NAME //名稱
                , MediaStore.Images.Media.DATE_TAKEN //生成時間
                , MediaStore.Images.Media.MIME_TYPE //類型
                , MediaStore.Images.Media.DATE_MODIFIED};
    }

    private List<QueryResult.MediaInfo> readMediaInfo(Cursor cursor, boolean isVideo) {

        List<QueryResult.MediaInfo> mediaInfos = null;
        if (cursor != null && cursor.moveToFirst()) {
            do {
                String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                String displayName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));
                String mimeType = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE));
                long createTime = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN));
                long modifiedTime = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED));
                long size = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.SIZE));

                if (TextUtils.isEmpty(displayName)) {
                    displayName = "";
                }

                QueryResult.MediaInfo media = new QueryResult.MediaInfo();
                if (isVideo) {
                    long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
                    media.setDuration(duration);
                }

                boolean isExist = isExistFile(path);
                media.setExist(isExist);
                media.setDateTaken(createTime);
                media.setDateModified(modifiedTime);
                media.setSize(size);
                media.setDisplayName(displayName);
                media.setMimeType(mimeType);
                if (isVideo) {
                    media.setMediaType(QueryResult.MediaInfo.MEDIA_TYPE_VIDEO);
                } else {
                    media.setMediaType(QueryResult.MediaInfo.MEDIA_TYPE_IMAGE);
                }
                media.setPath(path);
                if (mediaInfos == null) {
                    mediaInfos = new ArrayList<>();
                }
                mediaInfos.add(media);

            } while (cursor.moveToNext());
        }
        return mediaInfos;
    }

    private boolean isExistFile(String path) {
        try {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                File file = new File(path);
                return file.exists();
            }
            return Os.access(path, OsConstants.F_OK);
        } catch (Exception e) {
            e.printStackTrace();
            File file = new File(path);
            return file.exists();
        }
    }


    @Override
    public List<QueryResult.MediaInfo> call() throws Exception {

        String[] LIKE_PATHNAMES = new String[]{
                "DCIM/Camera","DCIM/相機","相機","Movies"
        };

        List<QueryResult.MediaInfo> result = new ArrayList<>();
        List<QueryResult.MediaInfo> mediaInfos = queryMediaStoreFiles(this.pageIndex, requestVideo, MediaStore.Video.Media.DATE_TAKEN, LIKE_PATHNAMES);
        if (mediaInfos != null) {
            result.addAll(mediaInfos);
        }
        return result;
    }
}

 

三、圖片更新優化

雖然Sqlite支持replace SQL,但是Android的ContentProvider並沒有提供相應的接口,因此想更新存入圖庫中的圖片和圖片信息,可能常規的是delete->insert ,但是這種方式會觸發部分ROM 回收站通知,因此最好是 query->update/insert

public class MediaStoreHelper {
    public static final String PIC_DIR_NAME = "天空雲相冊";
    //在系統的圖片文件夾下創建了一個相冊文件夾,名爲PIC_DIR_NAME,所有的圖片都保存在該文件夾下。

    private static boolean doSavePhotoToMediaStore(Context context, long photoId, String path) {

        boolean isOk = false;
        try {
            boolean isUpdate = photoId != -1;
            File srcFile = new File(path);
            String fileName = srcFile.getName();
            long time = System.currentTimeMillis();
            String picPath = srcFile.getAbsolutePath();
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.ImageColumns.DATA, picPath);
            values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName);
            values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpg");
            //將圖片的拍攝時間設置爲當前的時間
            values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, srcFile.lastModified());
            values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, time);
            values.put(MediaStore.Images.Media.SIZE, srcFile.length());
            ContentResolver resolver = context.getContentResolver();
            if (isUpdate) {
                values.put(MediaStore.Images.Media._ID, photoId);
                String whereSQL = " " + MediaStore.Images.Media._ID + "=? ";
                int id = resolver.update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, whereSQL, new String[]{String.valueOf(photoId)});
                if (id >= 0) {
                    isOk = true;
                }
            } else {
                Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
                if (uri != null) {
                    context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(new File(picPath))));
                    isOk = true;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
        return isOk;
    }

    private static boolean saveToMediaStore(Context context, String path) {

        File srcFile = new File(path);
        if (!srcFile.exists()) {
            return false;
        }
        ContentResolver resolver = context.getContentResolver();
        String[] fields = new String[]{
                MediaStore.Images.Media._ID
        };
        String whereSQL = " " + MediaStore.Images.Media.DATA + "=? ";
        String[] whereValues = {path};
        Cursor cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, fields, whereSQL, whereValues, null);
        long id = getPhotoId(cursor);
        return doSavePhotoToMediaStore(context, id, path);
    }

    public static boolean savePhoto(Context context, String path) {
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.FROYO) {
            return false;
        }
        File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), PIC_DIR_NAME);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        File file = new File(dir, "tiankong_cloud.jpg");
        if (!file.exists()) {
            try {
                RandomAccessFile inputRaf = new RandomAccessFile(path, "rw");
                RandomAccessFile outputRaf = new RandomAccessFile(file.getAbsolutePath(), "rw");
                FileChannel inputRafChannel = inputRaf.getChannel();
                FileChannel outputRafChannel = outputRaf.getChannel();
                inputRafChannel.transferTo(0, inputRafChannel.size(), outputRafChannel);
                inputRafChannel.close();
                outputRafChannel.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return saveToMediaStore(context, file.getAbsolutePath());
    }


    public static boolean savePhoto(Context context, int resId) {
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.FROYO) {
            return false;
        }
        File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), PIC_DIR_NAME);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        File file = new File(dir, "tiankong_cloud.jpg");
        if (!file.exists()) {
            InputStream is = null;
            try {
                is = context.getResources().openRawResource(resId);
                RandomAccessFile outputRaf = new RandomAccessFile(file.getAbsolutePath(), "rw");
                FileChannel outputRafChannel = outputRaf.getChannel();
                MappedByteBuffer map = outputRafChannel.map(FileChannel.MapMode.READ_WRITE, 0, is.available());
                int len = -1;
                byte[] buf = new  byte[1024];
                while ((len=is.read(buf,0,buf.length))!=-1){
                    map.put(buf,0,len);
                }
                outputRafChannel.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if(is!=null){
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return saveToMediaStore(context, file.getAbsolutePath());
    }


    private static long getPhotoId(Cursor cursor) {

        if (cursor != null && cursor.moveToFirst()) {
            do {
                long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID));
                return id;
            } while (cursor.moveToNext());
        }
        if(cursor!=null){
            cursor.close();
        }
        return -1;
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章