一、快速查询手机中的图片和视频
本方案适合通过媒体库实现快速查询视频和图片,对于SD卡扫描,也可以参考。
我们知道,媒体库属于数据库,CURD数据库属于IO操作,但是数据的IO相对特殊,很难使用一次拷贝,共享内存方式去优化,因此往往都是通过多线程操作去处理。一次查询所有图片和视频,极端情况下可能出现ANR或者查询慢的问题,因为媒体库接口时通过ContentProvider暴露的,虽然可以Hack ActivityThread使得ContentProviderClient不去发送ANR Error,但这种方式毕竟不太好。
二、方案设计
可以采用分段查询方式,创建初始任务时 图片和视频比与线程最大核心线程数相关,如果最大核心线程数为N(N必须大于4),那么图片线程为(N-2) : 视频线程为2,因为绝大多数情况下,手机中的视频数量比图片少很多倍,线程数量按分段方式查询,如果查询数量等于分页数目,那么继续创建线程查询下一页,最终所有线程收敛
1、创建配置类
public class MediaQueryConfig {
public static final int QUERY_ONLY_IMAGES = 1;
public static final int QUERY_ONLY_VIDEOS = 1<<1;
public static final int QUERY_IMAGES_AND_VIDEOS = QUERY_ONLY_IMAGES | QUERY_ONLY_VIDEOS;
private final int PAGE_SIZE = 1000;
private String orderBy = null;
private int queryType = QUERY_IMAGES_AND_VIDEOS;
private int pageSize = PAGE_SIZE;
public void setOrderBy(String orderBy,boolean isAsc) {
if(!TextUtils.isEmpty(orderBy)){
orderBy += " "+((isAsc)?" ASC ":" DESC ");
}
this.orderBy = orderBy;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public void setQueryType(int queryType) {
this.queryType = queryType;
}
public int getPageSize() {
return pageSize;
}
public int getQueryType() {
return queryType;
}
public String getOrderBy() {
return orderBy;
}
}
2、创建线程管理类
public class MediaQueryTaskManager implements Runnable {
private MediaQueryConfig mQueryConfig;
private Context context;
private static volatile ThreadPoolExecutor sThreadPoolExecutor = null;
private List<QueryTask> futureTasks;
private Thread monitorThread;
private Handler monitorHandler;
private int imagePageIndex = 0;
private int videoPageIndex = 0;
private AtomicInteger taskCounter;
private QueryResultListener listener;
private final int coreThreadNum = 4;
public MediaQueryTaskManager(Context context,MediaQueryConfig mediaQueryConfig) {
this.context = context.getApplicationContext();
if(mediaQueryConfig==null){
mediaQueryConfig = new MediaQueryConfig();
}
this.mQueryConfig = mediaQueryConfig;
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 long startQueryTime = System.currentTimeMillis();
final List<QueryResult.MediaInfo> videoList = new ArrayList<>();
final List<QueryResult.MediaInfo> imageList = new ArrayList<>();
final int PAGE_SIZE = mQueryConfig.getPageSize();
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) {
int size = videos.size();
if (size>0) {
videoList.addAll(videos);
}
Log.d("QueryResult", "查询视频: " + size +", costTime = "+queryResult.costTime);
if (size >=PAGE_SIZE) {
startNextPageVideos(monitorHandler);
}
}
} else {
List<QueryResult.MediaInfo> images = queryResult.getImages();
int size = images.size();
if (size>0) {
imageList.addAll(images);
}
Log.d("QueryResult", "查询图片: " + size+", costTime = "+queryResult.costTime);
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);
}
private void startNextPageVideos(Handler handler) {
int flag = mQueryConfig.getQueryType() & MediaQueryConfig.QUERY_ONLY_VIDEOS;
if(flag==0){
return;
}
QueryTask queryTask = new QueryTask(context, true, videoPageIndex, mQueryConfig, handler);
startQueryTask(queryTask);
videoPageIndex++;
}
private void startNextPageImages(Handler handler) {
int flag = mQueryConfig.getQueryType() & MediaQueryConfig.QUERY_ONLY_IMAGES;
if(flag==0){
return;
}
QueryTask queryTask = new QueryTask(context, false, imagePageIndex, mQueryConfig,handler);
startQueryTask(queryTask);
imagePageIndex++;
}
private void startQueryTaskOnReady(Handler handler) {
startNextPageImages(handler);
startNextPageImages(handler);
startNextPageImages(handler);
startNextPageImages(handler);
startNextPageVideos(handler);
}
private void startQueryTask(QueryTask task) {
this.taskCounter.incrementAndGet();
sThreadPoolExecutor.execute(task);
}
static class QueryTask implements Runnable {
private MediaQueryConfig mediaQueryConfig;
private Handler handler;
private boolean requestVideo;
private int pageIndex;
private Context context;
private static final int MSG_FINISH = 2;
private static final int MSG_ALL_FINISH = 3;
private static final int MSG_START = 1;
private QueryResult queryResult;
public QueryResult getQueryResult() {
return queryResult;
}
public QueryTask(Context context, boolean requestVideo, int pageIndex,MediaQueryConfig mediaQueryConfig, Handler handler) {
this.pageIndex = pageIndex;
this.context = context;
this.handler = handler;
this.requestVideo = requestVideo;
this.mediaQueryConfig = mediaQueryConfig;
}
@Override
public void run() {
try {
onStartTask(this);
long startQueryTime = System.currentTimeMillis();
MediaQuery fastMediaQuery = new MediaQuery(context, requestVideo, pageIndex,mediaQueryConfig);
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);
if(sThreadPoolExecutor ==null || sThreadPoolExecutor.isShutdown()) {
int maxCores = coreThreadNum + 2;
sThreadPoolExecutor = new ThreadPoolExecutor(coreThreadNum, maxCores, 120, 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.sThreadPoolExecutor.allowCoreThreadTimeOut(true);
}
}
this.monitorThread.start();
}
public interface QueryResultListener {
public void onResult(QueryResult queryResult);
}
}
3、创建查询任务
public class MediaQuery implements Callable<List<QueryResult.MediaInfo>> {
private final int pageIndex;
private final boolean requestVideo;
private MediaQueryConfig config;
private Context context;
public MediaQuery(Context context, boolean requestVideo, int pageIndex,MediaQueryConfig mediaQueryConfig) {
if(mediaQueryConfig==null){
mediaQueryConfig = new MediaQueryConfig();
}
this.context = context;
this.pageIndex = pageIndex;
this.requestVideo = requestVideo;
this.config = mediaQueryConfig;
}
public static String[] getQueryFields(boolean isVideo) {
if (isVideo) {
return new String[]{
MediaStore.Images.Media._ID,
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._ID
,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 static List<QueryResult.MediaInfo> readMediaInfo(Cursor cursor, boolean isVideo) {
List<QueryResult.MediaInfo> mediaInfos = null;
if (cursor != null && cursor.moveToFirst()) {
int index_path = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
int index_displayName = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME);
int index_mimeType = cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE);
int index_dataTaken = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN);
int index_dateModify = cursor.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED);
int index_size = cursor.getColumnIndex(MediaStore.Images.Media.SIZE);
int index_id = cursor.getColumnIndex(MediaStore.Images.Media._ID);
int index_duration = -1;
if(isVideo) {
index_duration = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
}
QueryResult.MediaInfo media = null;
do {
String path = cursor.getString(index_path);
String displayName = cursor.getString(index_displayName);
String mimeType = cursor.getString(index_mimeType);
long createTime = cursor.getLong(index_dataTaken);
long modifiedTime = cursor.getLong(index_dateModify);
long size = cursor.getLong(index_size);
long id = cursor.getLong(index_id);
if (TextUtils.isEmpty(displayName)) {
displayName = "";
}
media = new QueryResult.MediaInfo();
if (isVideo) {
long duration = cursor.getLong(index_duration);
media.setDuration(duration);
}
boolean isExist = isExistFile(path);
media.setExist(isExist);
media.setId(id);
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 List<QueryResult.MediaInfo> queryMediaStoreFiles(int pageIndex, boolean isVideo, String orderBy ) {
List<QueryResult.MediaInfo> mediaInfos = null;
Uri MediaStoreUri = isVideo ? MediaStore.Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = null;
int PAGE_SIZE = config.getPageSize();
// 获得图片
try {
String[] QUERY_FIELDS = getQueryFields(isVideo); //修改时间
String[] selectionArgs = null;
String whereStatement = null;
StringBuilder sqlLimit = new StringBuilder();
if (!TextUtils.isEmpty(orderBy)) {
sqlLimit.append(" ").append(orderBy).append(" ");
}
sqlLimit.append(" LIMIT ").append(pageIndex * PAGE_SIZE).append(", ").append(PAGE_SIZE).append(" ");
String sqlLimitStatement = sqlLimit.toString();
ContentResolver contentResolver = context.getContentResolver();
cursor = contentResolver.query(MediaStoreUri,
QUERY_FIELDS,
whereStatement,
selectionArgs,
sqlLimitStatement);
mediaInfos = readMediaInfo(cursor, isVideo);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return mediaInfos;
}
public static 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 orderType = config.getOrderBy();
List<QueryResult.MediaInfo> mediaInfos = queryMediaStoreFiles(this.pageIndex, requestVideo, orderType);
if (mediaInfos == null) {
mediaInfos = new ArrayList<>();
}
return mediaInfos;
}
public static List<QueryResult.MediaInfo> queryMediaStoreFiles(boolean isVideo, List<String> paths) {
if(paths==null||paths.isEmpty()) {
return null;
}
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); //修改时间
StringBuilder sb = new StringBuilder(isVideo?MediaStore.Video.Media.DATA:MediaStore.Images.Media.DATA);
sb.append(" IN ").append(" (");
int size = paths.size();
for (int i = 0; i < size; i++) {
sb.append("'").append(paths.get(i)).append("'");
if(i<size-1){
sb.append(",");
}
}
sb.append(") ");
cursor = HostHelper.getAppContext().getContentResolver().query(MediaStoreUri, QUERY_FIELDS,
sb.toString(), null, null);
mediaInfos = readMediaInfo(cursor, isVideo);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return mediaInfos;
}
}
4、使用方法
MediaQueryConfig mediaQueryConfig = new MediaQueryConfig();
mediaQueryConfig.setOrderBy( MediaStore.Images.ImageColumns.DATE_TAKEN,false);
mediaQueryTaskManager = new MediaQueryTaskManager(mContext,mediaQueryConfig);
mediaQueryTaskManager.getTask().doThen(new MediaQueryTaskManager.QueryResultListener(){
public void onResult(QueryResult queryResult) {
try {
long startTime = System.currentTimeMillis();
List<QueryResult.MediaInfo> images = queryResult.getImages();
List<QueryResult.MediaInfo> videos = queryResult.getVideos();
List<QueryResult.MediaInfo> mediaInfos = new ArrayList<>();
if (images != null) {
mediaInfos.addAll(images);
}
if (videos != null) {
mediaInfos.addAll(videos);
}
} finally {
}
}
});
同步调用可以使用CountDownLatch进行同步等待。
三、图片更新优化
虽然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;
}
}