1. 我们在打开的Gallery时候,就会把相关的相册信息给我们看,其中的信息有:
相册封面缩略图,相册的文件夹名称,相册包含的相片数量。如图:
Gallery是采用MVC框架,界面的显示和数据管理逻辑是分开的,那么这些数据是怎么获取和怎么管理的,本文为你揭晓相册的数据加载和管理。
2. 上文我们提到,界面包含有数据加载一般有AlbumSetPage 和AlbumPage这两个界面,所以会对这两个界面进行分析。
分析:
1. AlbumSetPage界面数据加载分析
查看AlbumSetPage.java类文件,通常界面的显示一般会有一个适配器的,这里是AlbumSetDataLoader(适配器), 而且适配器一般是数据源关联的,这里的数据源是
MediaSet(数据的集合),好了有了数据之后,就要显示真正的界面,界面就是由AlbumSetSlotRenderer这类对界面进行沟通的桥梁。
活动图如下所示:
Gallery有各种各样的数据源:
例如:本地的(local)、gmail同步的(picasa)、连接电脑的媒体传输的(mtp)、混合各种类型的(combo)等。
这些数据源是由DataManager来管理的。DataManager中初始化所有的数据源(LocalSource,PicasaSource, MtpSource, ComboSource, ClusterSource, FilterSource, UriSource,SnailSource), 将数据源放到一个Hash表中,提供存取操作,MediaSource负责管理数据集,以LoacalSource为例,从他的createMediaObject函数可以看出,根据路径他可以创建出LocalMediaSet,LocalMedia, LocalImage, LocalVideo等。
数据加载流程如图:
2.1 AlbumSetPage数据流程
1. 我们进入Gallery,首先会进入AlbumSetPage类,先从AlbumSetPage.java的onCreate方法入手。
public void onCreate(Bundle data, Bundle restoreState) {
initializeViews();// 初始化view,AlbumSetSlotRenderer和SlotView初始化配置 initializeData(data); ………
} |
private void initializeData(Bundle data) {
String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH);// 得到媒体路径,DataManager会根据路径生成相应数据源
; mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath, mMtkInclusion); // 新建数据源
mSelectionManager.setSourceMediaSet(mMediaSet); mAlbumSetDataAdapter = new AlbumSetDataLoader( mActivity, mMediaSet, DATA_CACHE_SIZE);// 新建适配器 mAlbumSetView.setModel(mAlbumSetDataAdapter); // 适配器和数据源绑定 } |
上面提到数据源由很多种,如果是本地数据源他会新建一个LocalAlbumSet.
那么他是如何生成的。
这句mActivity.getDataManager()他会返回一个DataManager,我们进入这个方法看看:
这里的mActivity为GalleryAppImpl类。
public synchronized DataManager getDataManager() { if (mDataManager == null) { mDataManager = new DataManager(this); mDataManager.initializeSourceMap(); //初始化DataManager } return mDataManager; } |
public synchronized void initializeSourceMap() { if (!mSourceMap.isEmpty()) return;
// the order matters, the UriSource must come last addSource(new LocalSource(mApplication)); //本地数据源
addSource(new PicasaSource(mApplication)); // gmail同步数据源 if (ApiHelper.HAS_MTP) { addSource(new MtpSource(mApplication)); // mtp传输数据源 } addSource(new ComboSource(mApplication)); // 组合数据源 addSource(new ClusterSource(mApplication)); // 时间,地点排序后的数据源 addSource(new FilterSource(mApplication)); addSource(new SecureSource(mApplication)); addSource(new UriSource(mApplication)); addSource(new SnailSource(mApplication)); } 上面把各种数据源存到一个Hash表,提供存取。 |
mMediaSet =mActivity.getDataManager().getMediaSet(mediaPath,
mMtkInclusion);
// 进入DataManager类的getMediaSet类中。
LocalSource.java public MediaObject createMediaObject(Path path) { GalleryApp app = mApplication; switch (mMatcher.match(path)) { case LOCAL_ALL_ALBUMSET: case LOCAL_IMAGE_ALBUMSET: case LOCAL_VIDEO_ALBUMSET: return new LocalAlbumSet(path, mApplication); // 这里返回LocalAlbumSet的数据源,mMediaSet = LocalAlbumSet } |
public MediaObject getMediaObject(Path path) { synchronized (LOCK) {
……… MediaSource source = mSourceMap.get(path.getPrefix()); // 获取Hash表的数据源,这里架设获取的是LocalSource try { MediaObject object = source.createMediaObject(path); // 进入方法中 。。。。。。。。。。。 } |
AlbumSetPage.onCreate() 之后进入onResume()方法中,mAlbumSetDataAdapter.resume();
调用AlbumSetDataLoader的onResume函数中,调用mReloadTask.start();
mReloadTask是一个线程,会启动线程到run方法中。
public void run() { updateLoading(true); // 发送消息更新开始 ……… long version = mSource.reload(); // 调用各种数据源的 reload,这里架设是LocalAlbumSet ……….. if (info.version != version) { // 相册是否有更新 info.version = version; info.size = mSource.getSubMediaSetCount();
// If the size becomes smaller after reload(), we may // receive from GetUpdateInfo an index which is too // big. Because the main thread is not aware of the size // change until we call UpdateContent. 。。。。。。。。。。。 if (info.index != INDEX_NONE) { info.item = mSource.getSubMediaSet(info.index);// 得到某个相册标识 if (info.item == null) continue; info.cover = info.item.getCoverMediaItem(); // item是一个相册,得到相册集合中的第一张照片作为封面 info.totalCount = info.item.getTotalMediaItemCount(); // 相册个数 } executeAndWait(new UpdateContent(info)); // 发送加载成功 } |
1. mSource.reload() , localAlbumSet.java
public synchronized long reload() { // 新建一个AlbumsLoader的任务,并添加线程池 mLoadTask = mApplication.getThreadPool().submit(new AlbumsLoader(), this); } if (mLoadBuffer != null) { // mLoadBuffer就是相册集合,在onFutureDone方法里线程结束时返回 mAlbums = mLoadBuffer; mLoadBuffer = null; for (MediaSet album : mAlbums) { album.reload();// 每个的相册都加载相片 } mDataVersion = nextVersionNumber(); // 相册版本 } return mDataVersion; } |
AlbumsLoader相册加载任务,返回是一组相册
public ArrayList<MediaSet> run(JobContext jc) { BucketEntry[] entries = BucketHelper.loadBucketEntries( jc, mApplication.getContentResolver(), mType, mPath); // 真正数据查询在这里,会从数据库加载数据,根据mtype 和mPath来划分image 和video是否同一个相册,BUCKET_ID字段为目录路径(path)的HASH值,不同BUCKET_ID代表一个相册
int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID); 。。。。。 index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID); if (index != -1) { circularShiftRight(entries, offset++, index); } // 查询的DICM/Camera路径的mediasource
ArrayList<MediaSet> albums = new ArrayList<MediaSet>(); DataManager dataManager = mApplication.getDataManager(); for (BucketEntry entry : entries) { //相册的一组数据 MediaSet album = getLocalAlbum(dataManager, mType, mPath, entry.bucketId, entry.bucketName); if(album.getMediaItemCount() > 0) { albums.add(album); // 添加到相册集合中 } } return albums; } } |
private MediaSet getLocalAlbum( DataManager manager, int type, Path parent, int id, String name) ; 。。。。。。。。。。。。 if (object != null) return (MediaSet) object; switch (type) { // 包含图像和视频,标识一个相册 case MEDIA_TYPE_IMAGE: return new LocalAlbum(path, mApplication, id, true, name); case MEDIA_TYPE_VIDEO: return new LocalAlbum(path, mApplication, id, false, name); case MEDIA_TYPE_ALL: Comparator<MediaItem>comp=DataManager.sDateTakenComparator; // 两者组合 ……… } } |
现在回到AlbumSetDataLoader.run方法中。
UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
,,,,, // info.item = mSource.getSubMediaSet(info.index); if (info.item == null) continue; info.cover = info.item.getCoverMediaItem(); info.totalCount = info.item.getTotalMediaItemCount(); |
info.item 就是一个相册,info.cover为封面的照片,info.item.getCoverMediaItem();
直接从数据库拉取图形或视频作为封面。
1. info.totalCount =info.item.getTotalMediaItemCount(); 返回图形和视频总数
到这里相册已经完成了,具体的时序图:
2.2 AlbumPage数据加载
1. Albumpage 和 AlbumSetPage的数据加载类似。
不同的地方就是在AlbumDataLoader.run的方法那里,AlbumSetPage是取相片中的第一张照片作为封面,而AlbumPage是把所有的照片生成缩略图的。
private class ReloadTask extends Thread { …… @Override public void run() { …… if (info.version != version) { info.size = mSource.getMediaItemCount();//得到相册中相片个数 info.version = version; } if (info.reloadCount > 0) { //得到reloadStart开始的reloadCount个媒体对象。 info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount); // 一般start从0开始,显示多少个图片缩略图 } …… } |
public UpdateInfo call() throws Exception { ……… for (int i = mContentStart, n = mContentEnd; i < n; ++i) { int index = i % DATA_CACHE_SIZE; if (setVersion[index] != version) { info.reloadStart = i; info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i); // MAX_LOAD_COUNT = 64,一次最多加载64张照片 return info; } } |
2.3图片缩略图的生成
1. 数据加载成功之后,就要在界面上显示了,那么数据如何在界面灵活的显示出来的。Gallery的图片显示不是一个控件的VIEW来显示的,因为为了提高浏览图片的效率,他是通过OpenGl ES 把这些照片给画出来的,所以下面来分析我们点进相册之后会生成多张图片的缩略图,起到一个预浏览的效果。我们点击某张图片,他就会鲜活得出现在我们前面,他是怎么样来显示这些照片的,他对图片的解码又是在哪个地方的,下面具体分析。
2. AlbumSlotRender负责数据的缩略图的加载工作,为了提高性能,数据加载使用了【线程池】,AlbumSlotRender从AlbumDataLoader获取要加载的数据MediaItem, 根据每一个MediaItem的状态确定是是否Bitmap缩略图的是需要加载、回收、还是等待等。对于需要加载的缩略图,提交到线程池中,所以首先第一个是怎么获得数据源。
3. 数据源的导入和对AlbumSlotRender配置
initializeViews
private void initializeViews() { ……. Config.AlbumPage config = Config.AlbumPage.get(mActivity); // 相册页面的配 置项,config文件定义了各个页面的配置
mSlotView = new SlotView(mActivity, config.slotViewSpec); // 加载配置项 mAlbumView = new AlbumSlotRenderer(mActivity, mSlotView, mSelectionManager, config.placeholderColor); // 对AlbumSlotRenderer进行配置,用来显示相关的操作 mSlotView.setSlotRenderer(mAlbumView);// 这里的mAlbumView相关于 GLSurfaceview mRootPane.addComponent(mSlotView); // 增加到画布上 。。。。。。。。。。。。。。。。。。。 } |
上面只是简单得对AlbumSlotRenderer进行了配置,要知道显示必须加载数据源,那么数据源加载在哪里?
在initializeData()方法中导入了数据源。mAlbumView.setModel(mAlbumDataAdapter)
public void setModel(AlbumDataLoader model) {
if (model != null) {AlbumSlidingWindow = mDataWindow mDataWindow = new AlbumSlidingWindow(mActivity, model, CACHE_SIZE); // 缩略图的窗口 mDataWindow.setListener(new MyDataModelListener()); // 监听 } |
上面窗口AlbumSlidingWindow类的监听器是MyDataModelListener类,所以我们点某个相册,窗口就会创建和状态发生改变,这里分析MyDataModelListener. onVisibleRangeChanged方法
AlbumSlotRenderer.java public void onVisibleRangeChanged(int visibleStart, int visibleEnd) { // 两次进入这个方法,第一次是创建大窗口 if (mDataWindow != null) { mDataWindow.setActiveWindow(visibleStart, visibleEnd); // 设置获得窗口,mDataWindow是窗口相关显示类 /// M: Video thumbnail play @{ mVideoThumbnailDirector.updateStage(); /// @} } } |
public void setActiveWindow(int start, int end) { if (!(start <= end && end - start <= mData.length && end <= mSize)) { Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize); } // 这里的start 一般是0, end这里有个规则,如果相片个数< 16,则end= 相片数, 因为在竖屏模式下,最多屏幕能显示16组照片,横屏是12张 。。。。。。。 int contentEnd = Math.min(contentStart + data.length, mSize);// 相片的总数,如果有29张,则=29 setContentWindow(contentStart, contentEnd); // 设置窗口,每个照片对应一个窗口 updateTextureUploadQueue(); if (mIsActive) updateAllImageRequests();// 会更新所有缩略图的请求 } |
setContentWindow会设置包含窗口,加载每个媒体数据
private void setContentWindow(int contentStart, int contentEnd) { …………. for (int i = contentStart; i < contentEnd; ++i) { prepareSlotContent(i); // 准备每个窗口的数据 }
|
private void prepareSlotContent(int slotIndex) { AlbumEntry entry = new AlbumEntry(); MediaItem item = mSource.get(slotIndex); //得到图片标识 entry.item = item; entry.contentLoader = new ThumbnailLoader(slotIndex, entry.item); // ……. 图片的缩略图生成就在这个线程完成 } |
开始生成缩略图线程。
回到setActiveWindow的方法中,if(mIsActive) updateAllImageRequests()会更新缩略图的请求,并为每个mediaitem 调用startLoad()方法,接着mTask =submitBitmapTask(this);开始一个任务来生成缩略图。
AlbumSlidingWindow.java
private class ThumbnailLoader extends BitmapLoader { ……... @Override protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { return mThreadPool.submit( mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this); // 向线程池请求,生成缩略图 } } |
这里向每个数据源提到submit线程生成缩略图要求,这里我们假设是localImage本地的数据处理。
public Job<Bitmap> requestImage(int type) { // 请求图片 return new LocalImageRequest(mApplication, mPath, type, filePath, dateModifiedInSec); } |
LocalImageRequest继承ImageCacheRequest类,第一次生成缩略图会有缓冲,下次就无需再次生成 public static class LocalImageRequest extends ImageCacheRequest { // private String mLocalFilePath; LocalImageRequest(GalleryApp application, Path path, int type, String localFilePath, long dateModifiedInSec) { // 生成缩略图 …………………………………. } ……………………… public Bitmap onDecodeOriginal(JobContext jc, final int type) { // 没有生成缩略图会走这里 。。。。。。。。。。 if (type == MediaItem.TYPE_MICROTHUMBNAIL) { // 小的省略图 ExifInterface exif = null; byte [] thumbData = null; try { exif = new ExifInterface(mLocalFilePath); if (exif != null) { thumbData = exif.getThumbnail(); 直接从jpeg的exif库函数里面 //得到缩略图,注意至于jpeg而且是exif的头的才可以进行此操作 // 不是很清楚那个类 } } catch (Throwable t) { Log.w(TAG, "fail to get exif thumb", t); } if (thumbData != null) { Bitmap bitmap = DecodeUtils.decodeIfBigEnough( jc, thumbData, options, targetSize); // 生成bitmap if (bitmap != null) return bitmap; } }
Bitmap bitmap = decodeOriginEx(jc, mApplication, mLocalFilePath, type, options, targetSize); //解码在此进行 …………………………. } |
ImageCacheRequest 继承自Job<Bitmap>类,我们来解析这个类
public Bitmap run(JobContext jc) { ImageCacheService cacheService = mApplication.getImageCacheService(); // 得到 cache服务 BytesBuffer buffer = MediaItem.getBytesBufferPool().get(); try { //boolean found = cacheService.getImageData(mPath, mType, buffer); boolean found = cacheService.getImageData(mPath, mType, buffer, mDateModifiedInSec); // 查询有没有缩略图,之前已经缓存过的放在buffer中 // if support picture quality tuning, we decode bitmap from origin image // in order to apply picture quality every time if (MtkLog.SUPPORT_PQ) found = false; if (jc.isCancelled()) return null; if (found) { // 有,则构建bitmap BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ARGB_8888; Bitmap bitmap; if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { // 构建小缩略图 bitmap = DecodeUtils.decode(jc, buffer.data, buffer.offset, buffer.length, options, MediaItem.getMicroThumbPool()); } else { bitmap = DecodeUtils.decode(jc, buffer.data, buffer.offset, buffer.length, options, MediaItem.getThumbPool()); } if (bitmap == null && !jc.isCancelled()) { Log.w(TAG, "decode cached failed " + debugTag()); } return bitmap; } } finally { MediaItem.getBytesBufferPool().recycle(buffer); } Bitmap bitmap = onDecodeOriginal(jc, mType);// 没有cache则跳到上面的onDecodeOriginal方法,进行解码 。。。。。。。
if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { //根据不同类型的缩略图转换解码后的bitmap(不是很理解,这里不是小缩略图)
bitmap = BitmapUtils.resizeAndCropCenter(bitmap, mTargetSize, true); } else { bitmap = BitmapUtils.resizeDownBySideLength(bitmap, mTargetSize, true); } 。。。。。。。。。 return bitmap; } |
上面的ImageCacheRequest的run方法中,一种是已经缓冲了缩略图,则直接拿出来,不需要解码,如果没有调用localImage. onDecodeOriginal来解码。来看看这个函数decodeOriginEx
private static Bitmap decodeOriginEx(JobContext jc, GalleryApp application, String filePath, int type, BitmapFactory.Options options, int targetSize) { ……… DataBundle dataBundle = RequestHelper.requestDataBundle(jc, params, (Context)application, filePath, false); // 上面调用解码请求 } |
public static DataBundle requestDataBundle(JobContext jc, Params params, Context context, String filePath, boolean allowDefault) {
IMediaRequest mediaRequest = null; //check if this file is drm and can get decrypted buffer byte[] buffer = DrmHelper.forceDecryptFile(filePath, false); // 解码drm if (null != buffer) { //for drm, we have to retrieve its mime type first. mimeType = DrmHelper.getOriginalMimeType(context, filePath); Log.i(TAG, "requestOriginalBitmap:mimeType="+mimeType); mediaRequest = RequestManager.getMediaRequest(mimeType, true); // 根据数据源的mimeType来请求媒体,这里是imageReqult } else { mimeType = MediaFile.getMimeTypeForFile(filePath); mediaRequest = RequestManager.getMediaRequest(mimeType, allowDefault); }
。。。。。。。。。。。。。
if (null == buffer) {//除了drm部分处理,调用imageRequest return mediaRequest.request(jc, params, filePath); } else { // drm 处理 return mediaRequest.request(jc, params, buffer, 0, buffer.length); } } |
ImageRequest.java public DataBundle request(JobContext jc, Params params, String filePath) { // 对图片进行解码 originThumb = DecodeUtils.decodeThumbnail( jc, filePath, options, params.inOriginalTargetSize, params.inType); } }
我们再来看DecodeUtils.decodeThumbnail |
public static Bitmap decodeThumbnail( JobContext jc, FileDescriptor fd, Options options, int targetSize, int type) { if (options == null) options = new Options(); jc.setCancelListener(new DecodeCanceller(options));
options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(fd, null, options); // 最后使用BitmapFactory类对图片进行解码 if (jc.isCancelled()) return null;
int w = options.outWidth; int h = options.outHeight;
if (type == MediaItem.TYPE_MICROTHUMBNAIL) { // We center-crop the original image as it's micro thumbnail. In this case, // we want to make sure the shorter side >= "targetSize". float scale = (float) targetSize / Math.min(w, h); options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
// For an extremely wide image, e.g. 300x30000, we may got OOM when decoding // it for TYPE_MICROTHUMBNAIL. So we add a max number of pixels limit here. final int MAX_PIXEL_COUNT = 640000; // 400 x 1600 if ((w / options.inSampleSize) * (h / options.inSampleSize) > MAX_PIXEL_COUNT) { options.inSampleSize = BitmapUtils.computeSampleSize( FloatMath.sqrt((float) MAX_PIXEL_COUNT / (w * h))); } } else { // For screen nail, we only want to keep the longer side >= targetSize. float scale = (float) targetSize / Math.max(w, h); options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); }
options.inJustDecodeBounds = false; setOptionsMutable(options);
Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options); if (result == null) return null;
// We need to resize down if the decoder does not support inSampleSize // (For example, GIF images) float scale = (float) targetSize / (type == MediaItem.TYPE_MICROTHUMBNAIL ? Math.min(result.getWidth(), result.getHeight()) : Math.max(result.getWidth(), result.getHeight()));
if (scale <= 0.5) result = BitmapUtils.resizeBitmapByScale(result, scale, true); return ensureGLCompatibleBitmap(result); } |
解码后就可以生成缩略图了。
其中类的关系流程图如下: