移动App开发中,常常有本地图片浏览和选择的需求。如果浏览选择使用系统自带的浏览选择组件,那是比较方便的,下面的方法就可以了:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, RequestCodes.PICK_PHOTO);
但这样常常不合产品需求,比如需要自定义UI 样式的时候,就不得不自己实现了。
在实现图片浏览和选择时候,相信很多人都会遇到比如加载慢、OOM、滑动卡顿 等问题。对比微信的图片浏览,他们的体验做得很流畅,加载图片出非常快。本文记录分享在开发中碰到上述问题后一次次改进方案,实现图片流畅加载,减少OOM 的一些经验。
1 在宫格显示时候,不使用原图而使用缩略图去显示
要把图片的路径整理出来,比较大众的做法是去读取手机媒体的存储(MediaStore.Images.Media.DATA)数据。该存储记录了图片的大量信息,比如图片的长、宽、路径、格式等等,数据非常丰富。
在宫格显示图片时,如果直接把加载出来的图片用于显示,在显示控件没有自带优化的情况下,很容易就出现OOM了。因为一张图片很可能就几个M,如果直接把几百上千张图片显示出来,不OOM 才怪。理想的做法就是使用缩略图显示,还好,Android 系统已经为我们准备好了,图片的缩略图(这个是手机自己生成的)也存储在一个数据库(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI),它和原图片是通过id 来关联着的,把它加载出来用于缩略图显示。这样使用缩略图显示而不是原图显示,更能减少OOM问题,同时滑动时候,由于使用了小图显示,滑动时候也会流畅很多。
图片和缩略图的关系,如下图:
加载代码大概如下:
private LongSparseArray loadThumbnails() {
LongSparseArray imageThumbnails = new LongSparseArray<>();
Uri imageUri = MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI;
ContentResolver contentResolver = getContentResolver();
Cursor cursor = null;
String projection[] = new String[] {
MediaStore.Images.Thumbnails._ID,
MediaStore.Images.Thumbnails.IMAGE_ID,
MediaStore.Images.Thumbnails.DATA
};
cursor = contentResolver.query(imageUri, projection, null, null, null/*Media.DATE_MODIFIED*/);
int indexImageId = cursor.getColumnIndex(MediaStore.Images.Thumbnails.IMAGE_ID);
int indexData = cursor.getColumnIndex(MediaStore.Images.Thumbnails.DATA);
while (cursor.moveToNext()) {
long imageId = cursor.getLong(indexImageId);
String thumbnail = cursor.getString(indexData);
imageThumbnails.put(imageId, thumbnail);
}
cursor.close();
return imageThumbnails;
}
private List loadImageItems() {
LongSparseArray thumbnails = loadThumbnails();
List imageItems = new ArrayList<>();
// 通过 MIME_TYPE 来搜索更好
String selection = MediaStore.Images.Media.DATA +
" like ? or _data like ? or _data like ? or _data like ? or _data like ?";
String selectionArgs[] = {
"%.jpg",
"%.jpe",
"%.jpeg",
"%.png",
"%.bmp"
};
Uri imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver contentResolver = getContentResolver();
Cursor cursor = null;
String projection[] = new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.SIZE};
cursor = contentResolver.query(imageUri, projection, selection, selectionArgs, MediaStore.Images.Media.DATE_MODIFIED + " DESC ");
int indexId = cursor.getColumnIndex(MediaStore.Images.Media._ID);
int indexData = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
int indexSize = cursor.getColumnIndex(MediaStore.Images.Media.SIZE);
while (cursor.moveToNext()) {
String path = cursor.getString(indexData);
long id = cursor.getLong(indexId);
long size = cursor.getLong(indexSize);
String thumbnail = thumbnails.get(id);
if (thumbnail == null) {
// thumbnail 很可能为null,因为不是每张图片系统都一定就生成了缩略图了的
if (size < 200 * 1024) {
// 太小的图片,直接用做缩略图即可
thumbnail = path;
} else {
// 需要额外生成下? 代码略。
}
}
imageItems.add(new ImageItem(path, thumbnail));
}
cursor.close();
return imageItems;
}
private static class ImageItem {
public String mImagePath;
public String mThumbnail;
ImageItem(String imagePath, String thumbnail) {
mImagePath = imagePath;
mThumbnail= thumbnail;
}
}
注意的是,不是每个图片系统都会生成缩略图存储。如果通过id 找不到小图(mThumbnail)时候,就需要特殊处理下了。如果图片本身很小(这个可以通过SIZE 来判断),可以直接使用作为缩略图了;如果是图片较大,就创建缩略图。写个管理器来管理存储自己创建的缩略图就好,下次浏览图片时候也可以用得上。
2 图片分批加载
在 1 中,实现上是手机上有多少图片,就一次性把全部路径加载出来了。在图片足够多的时候(比如几万张,对于自拍爱好者或者摄影师这个数量不奇怪),这个加载过程就比较慢了。用户浏览图片时候,就算放了个loading 页面等待,这个等待若太久也很可能忍受不了。
细细想想,用户浏览图片不可能一下子就能够浏览到几百张图片以上的图片的,所以没有必要一下子把所有图片加载出来,先加载一部分图片出来,最好按最新保存的优先,因为很可能最新的就是用户最想选择的,再去分批加载出来,这样用户就不会感觉到慢了。
看下图
加载图片路径时候的工作,一般都是在线程(或者异步任务)做的,上图用虚线表示异步,加载好了再通过消息刷新到UI去显示。先加载100张图片信息,通过消息把这部分数据扔给UI 去刷新显示;如果还有更多,接着继续加载100 * 5 张图片,再通过消息把这部分数据扔给UI显示;如果还有更多,接着再加载100 * 5 * 5 张……一直都没有更多为止。这样用户就不用怎么等待,感觉图片就一下子加载出来了。
3 浏览选择图片独立到多进程实现(这段和快速浏览没有很大关系,这段的做法是为了减少OOM)
Android 有个机制是在资源紧张情况下会回收内存资源高的后台进程。对此,我们可以增加一个tools 进程,用它来辅助做一些不常用而又比较耗内存的功能,比如图片浏览选择、Webview 网页查看(这些都是较耗内存的)等。图片浏览选择一般都是比较耗内存的,特别是在有大量图片而且又没有使用缩略图的情况下,动不动就OOM了;WebView网页浏览也常常被调侃内存泄漏严重。
把这些耗内存的而又相对独立的功能挪到tools 进程去,主进程就显得很清爽感觉了,需要时候就把tools 进程调出来,用完后它回到后台,系统爱回收就回收,不影响主进程。因为耗内存的功能挪到辅助进程去后,主进程内存上会减少很多,这样也更能有效的减少回到后台后被回收。
把选择图片的Activity 独立到tools 进程去的方法也简单,就是在AndroidManifest.xml的该Activity 内指定process 即可(使用SelectImageActivity的用法差不多),如下:
<activity
android:name=".activities.SelectImageActivity"
android:process=":tools"
/>
使用多进程也有多进程的注意点,这个就不在本文讨论范围了。
总结:在实现自定义UI选择图片功能时候,上面提到的优化方案有“使用系统缩略图”(如果没有可以自己生成做管理)、“图片分批加载”、“把功能独立到辅助进程去”。如果应用了这些方案,加载图片几乎就是秒开了的,滑动浏览也是比较流畅。
在知识星球App 开发时候使用了这些方法,达到了加载像微信一样秒开的效果,有效解决了原来用户抱怨图片加载慢和OOM问题。
此外,控件显示图片上,知识星球内使用了 facebook 的SimpleDraweeView。在图片显示上使用什么控件、Adapter的使用技巧、使用Holder 技术等,这里就不简述了,这些都是比较流行的做法了,没有什么好说的。
来一张知识星球的选择图片页面的图(本想做一张gif 图片的,录好的图片有点大就算了),加载超快,进度条都没有看到就已经把图片显示出来了。