原创文章,转载请注明出处。
作为一个刚接触android不久的小白,对于通过gridview来加载大量图片的性能问题的解决也是比较曲折的。之前写过一篇弱应用的使用场景,介绍了通过异步线程和缓存加载图片http://blog.csdn.net/guduyishuai/article/details/54616201。但是性能还是有些慢的,现在发现android缩略图的功能,结合之前异步线程和缓存来加载图片,这种方式的性能可以接受了。目前测试上千张图片,二十来个视频都是没问题的。
先看一下效果:

先梳理一下知识点。
1、android自带的缩略图功能概述
android自带了媒体扫描服务MEDIA_SCANNER,该服务通过扫描媒体文件,进行如下操作。
a、更新媒体表的数据,包括缩略图,图片,视频,音频
b、创建缩略图到/storage/emulated/0/DCIM/Camera/.thumbnails,该文件夹可能是隐藏文件夹
问题点:a、关于MEDIA_SCANNER的启动时机不太清楚,也没有查询到相关资料。不过可以手动启动
b、目前在华为上测试,MEDIA_SCANNER执行后确实更新了除缩略图表以外的数据库表,但是缩略图的数据库表未更新也未生成缩略图。
对于视频,在第一次播放后会自动更新缩略图的数据库表,生成缩略图。
对于图片,未找到更新的时机。
2、Thumbnails类
该类提供了获取缩略图的一系列方法,可以在该类中找到媒体数据库表的表名,字段名。然后可以通过对数据库的查询获得相应信息。
看一下的相关表名和字段吧
a、图片
表名:Thumbnails.EXTERNAL_CONTENT_URI
字段:Thumbnails._ID, Thumbnails.IMAGE_ID,Thumbnails.DATA
b、视频
表名:MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI
字段:MediaStore.Video.Thumbnails._ID, MediaStore.Video.Thumbnails.VIDEO_ID, MediaStore.Video.Thumbnails.DATA
3、Media类
同Thumbnails类,不同的是提供的是图片的相应信息
表名:Media.EXTERNAL_CONTENT_URI
字段:Media._ID, Media.BUCKET_ID,Media.PICASA_ID, Media.DATA, Media.DISPLAY_NAME, Media.TITLE,Media.SIZE, Media.BUCKET_DISPLAY_NAME
4、MediaStore类
同Media类,提供了更丰富的媒体信息,包括了视频,音频
视频表名:MediaStore.Video.Media.EXTERNAL_CONTENT_URI
字段:MediaStore.Video.Media._ID,MediaStore.Video.Media.BUCKET_ID,MediaStore.Video.Media.DATA, MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.TITLE,MediaStore.Video.Media.SIZE, MediaStore.Video.Media.BUCKET_DISPLAY_NAME,MediaStore.Video.Media.TITLE
5、ThumbnailUtils类
该类提供了手动创建缩略图的方法
图片:Bitmap ThumbnailUtils.extractThumbnail(Bitmap source,int width,int height)
Bitmap ThumbnailUtils.extractThumbnail(Bitmap source,int width,int height, int options)
视频:BitmapThumbnailUtils.createVideoThumbnail(String filePath, int kind)
6、ImageLoader工具
该工具为第三方开源图片加载工具,思路也是多线程异步加载,提供多种缓存机制,包括弱引用缓存,其他多种算法的缓存。高可配置化。在这里就不多做介绍。
再梳理一下思路
1、拍照和视频录制后
执行MEDIA_SCANNER服务。
注意:这个时候不能保证生成缩略图,因此我们需要自己建立一个缩略图
2、打开相册
通过查询数据库,获得图片,视频的缩略图路径,图片,视频的原始路径
通过多线程加载图片,由于生成缩略图的时机不可把握(也可能是华为系统的问题,在MEDIA_SCANNER服务后不生成缩略图)。另外,如果是视频缩略图,这里加了水印对图片缩略图进行区分。因此思路如下
a、如果是视频
a.1、如果存在系统缩略图,不存在本地缩略图(非本app录制的视频)
取出系统缩略图,加上水印,保存到自带缩略图
加载本地缩略图
a.2、否则
加载本地缩略图
b、如果是图片
a.1、存在本地缩略图
a.1.1、需要判断图片是否进行修改(因为相册图片修改后,保存时原图片名会在一定时机变为"原图名_#",#表示数字,也可能是华为系统的特例,尚未验证)
a.1.1.1、图片已经修改
a.1.1.1.1、存在原文件名的本地缩略图
更新为新文件名
加载本地缩略图
a.1.1.1.2、不存在原文件名的缩略图(非本app拍摄的图片)
创建本地缩略图
加载本地缩略图
a.1.1.2、图片未修改
a.1.1.2.1、存在系统缩略图
加载系统缩略图
a.1.1.2.2、不存在系统缩略图
创建本地缩略图
加载本地缩略图
a.2、存在本地缩略图
加载本地缩略图
还有一种简单的算法,如果不存在本地缩略图,直接创建本地缩略图。但是这样带来的问题是,如果存在大量的非本app拍摄的图片和视频,那么在第一次加载时,由于大量IO操作会对系统造成阻塞。使用了异步任务也是如此。所以才使用了以上算法。
3、修改图片后
更新本地缩略图,执行MEDIA_SCANNER服务(这里其实还存在一个问题,如果是其他应用修改了图片,本地缩略图不会及时更新。解决办法可能需要一个server进行扫描。这样的话,其实就是MEDIA_SCANNER服务该做的事情啊。问题是MEDIA_SCANNER服务玩忽职守,可能是华为系统的原因)
最后,看一下关键代码:
调用MEDIA_SCANNER服务
Intent scanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);scanIntent.setData(Uri.fromFile(new File(filePath)));context.sendBroadcast(scanIntent); 对图片生成本地缩略图public static void thumbnail(Activity context,String folder,String path,String fileName){ Bitmap source = BitmapUtil.getComPRessBitmap(context, path,1); Bitmap thumb=ThumbnailUtils.extractThumbnail(source, 96, 96, ThumbnailUtils.OPTIONS_RECYCLE_INPUT); File thumbnailFolder=new File(folder); if(!thumbnailFolder.exists()){ thumbnailFolder.mkdirs(); } path=folder+"/"+fileName; BitmapUtil.store(thumb, new File(path)); } 对视频生成本地缩略图public static void thumbnailVideo(Activity context,String folder,String path,String fileName){ Bitmap thumb=ThumbnailUtils.createVideoThumbnail(path, 96); Drawable drawable = context.getResources().getDrawable(R.drawable.video_recorder); Bitmap waterMark=drawableToBitmap(drawable); thumb=waterMark(thumb,waterMark); File thumbnailFolder=new File(folder); if(!thumbnailFolder.exists()){ thumbnailFolder.mkdirs(); } path=folder+"/"+fileName; BitmapUtil.store(thumb, new File(path));} 存储图片public static void store(Bitmap bitmap,File file){ OutputStream fos=null; try{ fos = new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); }catch(Exception e){ e.printStackTrace(); }finally{ try { fos.close(); } catch (IOException e) { e.printStackTrace(); } }} 相册类public class ImageBucket { public int count = 0; public String bucketName; public List<ImageItem> imageList;} 图片和视频信息类public class ImageItem implements Serializable,Comparable<ImageItem>{ private static final long serialVersionUID = -6304538407773729848L; public String imageId; public String imageName; public String thumbnailPath; public String imagePath; public boolean isVideo=false; public boolean isSelected = false; @Override public int compareTo(ImageItem imageItem) { return imageItem.imageId.compareTo(this.imageId); }} 获得图片和视频的相关信息public void init(Context context){ if(AlbumUtil.bucketList!=null || !AlbumUtil.bucketList.isEmpty()){ AlbumUtil.bucketList=null; System.gc(); AlbumUtil.bucketList=new HashMap<String,ImageBucket>(); } getThumbnail(context); getImage(); getVideo(); Set<String> key=AlbumUtil.bucketList.keySet(); for(String k : key){ ImageBucket ib=AlbumUtil.bucketList.get(k); Collections.sort(ib.imageList); }}private void getImage(){ // 构造相册索引 String columns[] = new String[] { Media._ID, Media.BUCKET_ID, Media.PICASA_ID, Media.DATA, Media.DISPLAY_NAME, Media.TITLE, Media.SIZE, Media.BUCKET_DISPLAY_NAME }; cr.delete(Media.EXTERNAL_CONTENT_URI, Media._ID+"=?", new String[]{"55701"}); // 得到一个游标 Cursor cur = cr.query(Media.EXTERNAL_CONTENT_URI, columns, null, null,null); if (cur.moveToFirst()) { // 获取指定列的索引 int photoIDIndex = cur.getColumnIndexOrThrow(Media._ID); int photoPathIndex = cur.getColumnIndexOrThrow(Media.DATA); int photoNameIndex = cur.getColumnIndexOrThrow(Media.DISPLAY_NAME); int photoTitleIndex = cur.getColumnIndexOrThrow(Media.TITLE); int photoSizeIndex = cur.getColumnIndexOrThrow(Media.SIZE); int bucketDisplayNameIndex = cur.getColumnIndexOrThrow(Media.BUCKET_DISPLAY_NAME); int bucketIdIndex = cur.getColumnIndexOrThrow(Media.BUCKET_ID); int picasaIdIndex = cur.getColumnIndexOrThrow(Media.PICASA_ID); // 获取图片总数 int totalNum = cur.getCount(); do { String _id = cur.getString(photoIDIndex); String name = cur.getString(photoNameIndex); String path = cur.getString(photoPathIndex); if(!path.contains(DEFAULT_PIC_DIR)){ continue; } String title = cur.getString(photoTitleIndex); String size = cur.getString(photoSizeIndex); String bucketName = cur.getString(bucketDisplayNameIndex); String bucketId = cur.getString(bucketIdIndex); String picasaId = cur.getString(picasaIdIndex); Log.d(TAG, name); if(name.contains("0207")){ System.out.println(); } ImageBucket bucket = bucketList.get(bucketId); if (bucket == null) { bucket = new ImageBucket(); bucketList.put(bucketId, bucket); bucket.imageList = new ArrayList<ImageItem>(); bucket.bucketName = bucketName; } bucket.count++; ImageItem imageItem = new ImageItem(); imageItem.imageId = _id; imageItem.imageName = name; imageItem.imagePath = path; imageItem.thumbnailPath = thumbnailList.get(_id); bucket.imageList.add(imageItem); } while (cur.moveToNext()); } Iterator<Entry<String, ImageBucket>> itr = bucketList.entrySet() .iterator(); while (itr.hasNext()) { Map.Entry<String, ImageBucket> entry = (Map.Entry<String, ImageBucket>) itr .next(); ImageBucket bucket = entry.getValue(); Log.d(TAG, entry.getKey() + ", " + bucket.bucketName + ", " + bucket.count + " ---------- "); for (int i = 0; i < bucket.imageList.size(); ++i) { ImageItem image = bucket.imageList.get(i); Log.d(TAG, "----- " + image.imageId + ", " + image.imagePath + ", " + image.thumbnailPath); } } //hasBuildImagesBucketList = true; } private void getVideo(){ String[] columns = new String[] {MediaStore.Video.Media._ID, MediaStore.Video.Media.BUCKET_ID, MediaStore.Video.Media.DATA, MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.TITLE, MediaStore.Video.Media.SIZE, MediaStore.Video.Media.BUCKET_DISPLAY_NAME, MediaStore.Video.Media.TITLE }; Cursor cur = cr.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, columns, null, null, MediaStore.Video.Media.DEFAULT_SORT_ORDER); if (cur.moveToFirst()) { // 获取指定列的索引 int photoIDIndex = cur.getColumnIndexOrThrow(MediaStore.Video.Media._ID); int photoPathIndex = cur.getColumnIndexOrThrow(MediaStore.Video.Media.DATA); int photoNameIndex = cur.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_DISPLAY_NAME); int photoTitleIndex = cur.getColumnIndexOrThrow(MediaStore.Video.Media.TITLE); int photoSizeIndex = cur.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE); int bucketDisplayNameIndex = cur.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_DISPLAY_NAME); int bucketIdIndex = cur.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_ID); // 获取视频总数 int totalNum = cur.getCount(); do { String _id = cur.getString(photoIDIndex); String name = cur.getString(photoNameIndex); String path = cur.getString(photoPathIndex); if(!path.contains(DEFAULT_PIC_DIR)){ continue; } String title = cur.getString(photoTitleIndex); String size = cur.getString(photoSizeIndex); String bucketName = cur.getString(bucketDisplayNameIndex); String bucketId = cur.getString(bucketIdIndex); ImageBucket bucket = bucketList.get(bucketId); if (bucket == null) { bucket = new ImageBucket(); bucketList.put(bucketId, bucket); bucket.imageList = new ArrayList<ImageItem>(); bucket.bucketName = bucketName; } bucket.count++; ImageItem imageItem = new ImageItem(); imageItem.imageId = _id; imageItem.imageName = title+".jpg"; imageItem.imagePath = path; imageItem.thumbnailPath = thumbnailList.get(_id); imageItem.isVideo=true; bucket.imageList.add(imageItem); } while (cur.moveToNext()); } Iterator<Entry<String, ImageBucket>> itr = bucketList.entrySet() .iterator(); while (itr.hasNext()) { Map.Entry<String, ImageBucket> entry = (Map.Entry<String, ImageBucket>) itr .next(); ImageBucket bucket = entry.getValue(); Log.d(TAG, entry.getKey() + ", " + bucket.bucketName + ", " + bucket.count + " ---------- "); for (int i = 0; i < bucket.imageList.size(); ++i) { ImageItem image = bucket.imageList.get(i); Log.d(TAG, "----- " + image.imageId + ", " + image.imagePath + ", " + image.thumbnailPath); } } } /** * 得到缩略图 */ private void getThumbnail(Context context) { String[] projection = { Thumbnails._ID, Thumbnails.IMAGE_ID,Thumbnails.DATA}; cr = context.getContentResolver(); Cursor cursor = cr.query(Thumbnails.EXTERNAL_CONTENT_URI, projection, null, null, null); getThumbnailColumnData(cursor,false); String[] VideoThumbColumns = new String[]{ MediaStore.Video.Thumbnails._ID, MediaStore.Video.Thumbnails.VIDEO_ID, MediaStore.Video.Thumbnails.DATA}; cursor = cr.query(MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI, VideoThumbColumns, null, null, null); getThumbnailColumnData(cursor,true); } /** * 从数据库中得到缩略图 * * @param cur */ private void getThumbnailColumnData(Cursor cur,boolean isVideo) { if (cur.moveToFirst()) { int _id; int image_id; String image_path; if(isVideo){ int _idColumn = cur.getColumnIndex(MediaStore.Video.Thumbnails._ID); int image_idColumn = cur.getColumnIndex( MediaStore.Video.Thumbnails.VIDEO_ID); int dataColumn = cur.getColumnIndex( MediaStore.Video.Thumbnails.DATA); do { _id = cur.getInt(_idColumn); image_id = cur.getInt(image_idColumn); image_path = cur.getString(dataColumn); thumbnailList.put("" + image_id, image_path); } while (cur.moveToNext()); } else{ int _idColumn = cur.getColumnIndex(Thumbnails._ID); int image_idColumn = cur.getColumnIndex(Thumbnails.IMAGE_ID); int dataColumn = cur.getColumnIndex(Thumbnails.DATA); do { _id = cur.getInt(_idColumn); image_id = cur.getInt(image_idColumn); image_path = cur.getString(dataColumn); thumbnailList.put("" + image_id, image_path); } while (cur.moveToNext()); } } } public static List<String> getPicsAndVideo(){ if(pics==null || pics.isEmpty() || isRefresh){ pics=new ArrayList<String>(); Set<String> key=AlbumUtil.bucketList.keySet(); for(String k : key){ ImageBucket ib=AlbumUtil.bucketList.get(k); List<ImageItem> its=ib.imageList; for(ImageItem it : its){ pics.add(it.imagePath); } } } return pics; } 多线程异步加载图片@Override public View getView(int position, View convertView, ViewGroup parent) { int windowWidth = ((Activity) context).getWindowManager().getDefaultDisplay().getWidth(); int pad = 4; ImageView imageView; if(convertView == null){ imageView = new ImageView(context); } else{ imageView = (ImageView)convertView; } //判断是否有线程在加载该图片,或者该imageView的图片已经改变 /* if (cancelPotentialLoad(fullPathImg.get(position), imageView)) { AsyncLoadImageTask task = new AsyncLoadImageTask(imageView); LoadedDrawable loadedDrawable = new LoadedDrawable(task); imageView.setLayoutParams(new GridView.LayoutParams((windowWidth - pad * 12) / 4, (windowWidth - pad * 12) / 4)); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setImageDrawable(loadedDrawable); task.execute(position); } */ File cacheDir = StorageUtils.getOwnCacheDirectory(context, "imageloader/Cache"); DisplayImageOptions options = new DisplayImageOptions.Builder() .cacheInMemory(true) .cacheOnDisc(true) .bitmapConfig(Bitmap.Config.RGB_565) .showStubImage(R.drawable.line) .showImageForEmptyUri(R.drawable.line) .showImageOnFail(R.drawable.line) .imageScaleType(ImageScaleType.EXACTLY) .build(); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context) .defaultDisplayImageOptions(options) .memoryCache(new LruMemoryCache(12 * 1024 * 1024)) .memoryCacheSize(12 * 1024 * 1024) .discCache(new UnlimitedDiscCache(cacheDir)) .discCacheSize(128 * 1024 * 1024) .threadPriority(Thread.MAX_PRIORITY - 2) .denyCacheImageMultipleSizesInMemory() .tasksProcessingOrder(QueueProcessingType.LIFO) .writeDebugLogs() .build(); imageView.setLayoutParams(new GridView.LayoutParams((windowWidth - pad * 12) / 4, (windowWidth - pad * 12) / 4)); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); Set<String> key=AlbumUtil.bucketList.keySet(); String url=""; for(String k : key){ List<ImageItem> items=AlbumUtil.bucketList.get(k).imageList; String path=items.get(position).imagePath; String thumbnail=items.get(position).thumbnailPath; boolean isVideo=items.get(position).isVideo; if(isVideo){ Log.d(TAG,"----------video---------"+items.get(position).imagePath); if(thumbnail!=null && !thumbnail.equals("")){ //使用其他工具录制的视频,需要在缩略图上增加水印 AddThumbnailWarterMarkTask addThumbnailWarterMarkTask=new AddThumbnailWarterMarkTask(); addThumbnailWarterMarkTask.doInBackground(new String[]{items.get(position).imageName,path}); url=Info.THUMBNAIL+"/"+items.get(position).imageName; } else{ //video使用加了水印的缩略图 url=Info.THUMBNAIL+"/"+items.get(position).imageName; } } else{ //由于系统更新缩略图的时机完全不可捕捉,也或者是华为为了省电在媒体扫描后不更新缩略图, //所以弃用系统缩略图,全部使用自己生成的缩略图 //但是一次性生成缩略图会使第一次打开相册较慢,所以逐步替换 /* if(thumbnail!=null && !thumbnail.equals("")){ Log.d(TAG,"缩略图"); //已经生成缩略图后,删除非系统生成的缩略图 DeleteThumbnailTask deleteThumbnailTask=new DeleteThumbnailTask(); deleteThumbnailTask.doInBackground(items.get(position).imageName); url=thumbnail; }else{ Log.d(TAG,"原图"); //不知道缩略图在什么时候生成,如果未生成,使用之前手动生成的缩略图 path=Info.THUMBNAIL+"/"+items.get(position).imageName; //相册文件修改后,在媒体扫描服务执行后,会对文件名进行修改,此时需要更新缩略图 File thumbnailFile=new File(path); if(!thumbnailFile.exists()){ String name=items.get(position).imageName; String orginalThumbnailName=name.substring(0,name.lastIndexOf("_"))+".jpg"; thumbnailFile=new File(Info.THUMBNAIL+"/"+orginalThumbnailName); thumbnailFile.renameTo(new File(path)); } url=path; } */ path=Info.THUMBNAIL+"/"+items.get(position).imageName; //相册文件修改后,在媒体扫描服务执行后,会对文件名进行修改,此时需要更新缩略图 File thumbnailFile=new File(path); //不存在自己建立的缩略图 if(!thumbnailFile.exists()){ String name=items.get(position).imageName; //由于相册图片修改造成的缩略图失效 if(name.split("_").length>3){ String orginalThumbnailName=name.substring(0,name.lastIndexOf("_"))+".jpg"; thumbnailFile=new File(Info.THUMBNAIL+"/"+orginalThumbnailName); if(thumbnailFile.exists()){ thumbnailFile.renameTo(new File(path)); }else{ //创建缩略图 CreateThumbnailTask createThumbnailTask=new CreateThumbnailTask(); createThumbnailTask.doInBackground(new String[]{items.get(position).imagePath,name}); } url=path; }else{ if(thumbnail!=null && !thumbnail.equals("")){ //系统缩略图 url=thumbnail; } else{ //创建缩略图 CreateThumbnailTask createThumbnailTask=new CreateThumbnailTask(); createThumbnailTask.doInBackground(new String[]{items.get(position).imagePath,name}); url=path; } } }else{ url=path; } } } /* String url=fullPathImg.get(position); */ String imageUrl = Scheme.FILE.wrap(url); ImageLoader.getInstance().displayImage(imageUrl, imageView, options); /* imageView.setLayoutParams(new GridView.LayoutParams((windowWidth - pad * 12) / 4, (windowWidth - pad * 12) / 4)); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); String url=fullPathImg.get(position); imageView.setImageBitmap(BitmapUtil.getCompressBitmap((Activity) context, url,100)); */ return imageView; }private class CreateThumbnailTask extends AsyncTask<String,Void,Boolean>{ @Override protected Boolean doInBackground(String... args) { BitmapUtil.thumbnail((Activity) context,Info.THUMBNAIL,args[0],args[1]); return true; } }private class AddThumbnailWarterMarkTask extends AsyncTask<String,Void,Boolean>{ @Override protected Boolean doInBackground(String... args) { File file=new File(Info.THUMBNAIL+"/"+args[0]); if(!file.exists()){ BitmapUtil.thumbnailVideo((Activity) context,Info.THUMBNAIL,args[1],args[0]); } return true; } }
新闻热点
疑难解答