像微信一样流畅地浏览选择图片

像微信一样流畅地浏览选择图片

移动App开发中,常常有本地图片浏览和选择的需求。如果浏览选择使用系统自带的浏览选择组件,那是比较方便的,下面的方法就可以了:
(在某Activity内)

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<String> loadThumbnails() {
    LongSparseArray<String> 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<ImageItem> loadImageItems() {
    LongSparseArray<String> thumbnails = loadThumbnails();

    List<ImageItem> 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 图片的,录好的图片有点大就算了),加载超快,进度条都没有看到就已经把图片显示出来了。

(全文完)

(欢迎转载本站文章,但请注明作者和出处 云域 – Yuccn

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注