編輯:關於Android編程
現在網上對此Imageloader圖片加載的開源框架的解析有好多文章,有好多只是簡單分析它的實現,此篇文章是通過自己對其源碼的分析,對它的實現方式進行分析,針對它用到的重點知識點進行重點介紹,以及自己對於此框架的理解。下面的分析從以下兩個方面進行分析。
Imageloader的初始化
Imageloader加載圖片的實現方式分析
1.Imageloader的初始化
Imageloader是在Application的onCreate()方法中進行初始化的,在官方的demo 中的初始化方式如下:
ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context); config.threadPriority(Thread.NORM_PRIORITY - 2); config.denyCacheImageMultipleSizesInMemory(); config.diskCacheFileNameGenerator(new Md5FileNameGenerator()); config.diskCacheSize(50 * 1024 * 1024); // 50 MiB config.tasksProcessingOrder(QueueProcessingType.LIFO); config.writeDebugLogs(); // Remove for release app // Initialize ImageLoader with configuration. ImageLoader.getInstance().init(config.build());從上面我們能看出,官方的demo對指定的配置選項進行了配置,當然,沒有設置的選項,系統也會采用默認的方式進行實現。可以通過config.build()方法中實現的,會調用initEmptyFieldsWithDefaultValues()方法會將那些必要的並且沒有設置的配置參數進行初始化,比如downloader(執行下載的對象),decoder(執行解碼的對象)等等,如果想了解,可以自己查看更多默認參數,下面分析在加載的圖片的流程中,用到這些對象會對其進行詳細的分析。
2.Imageloader加載圖片的實現方式分析
本文主要對displayImage()方法進行分析,其實其他方式原理都一樣,就不過多描述了。實現看displayImage()參數列表如下:
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)其實其中,需要了解的也就是參數三的DisplayImageOptions了,這是對顯示的圖片的配置信息,我們來看官方demo的實現方式如下:
options = new DisplayImageOptions.Builder() .showImageOnLoading(R.drawable.ic_stub) .showImageForEmptyUri(R.drawable.ic_empty) .showImageOnFail(R.drawable.ic_error) .cacheInMemory(true) .cacheOnDisk(true) .considerExifParams(true) .displayer(new CircleBitmapDisplayer(Color.WHITE, 5)) .build();其中重點需要說一下的就是設置displayer(),這裡實現的是顯示圓形圖片,系統還提供了其他幾種Displayer,都是繼承Bitmap Displayer進行擴展實現的。內部的實現是通過Drawable實現,而不是對ImageView進行操作了,這樣性能會更好。實現原理就是將Bitmap畫到Drawable,然後直接將Drawable設置給ImageVIew即可。我記得好像之前看到鴻陽有一篇文件就是寫的這方面,想要了解的可以看一下。這裡代碼的具體實現:
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) { if (!(imageAware instanceof ImageViewAware)) { throw new IllegalArgumentException("ImageAware should wrap ImageView. ImageViewAware is expected."); } imageAware.setImageDrawable(new CircleDrawable(bitmap, strokeColor, strokeWidth)); }內部有一個CircleDrawable的靜態內部淚,它繼承自Drawable,具體實現,以及原理,大家可以自己查看,以及查閱相關資料,這個不是這裡的重點。
繼續上面displayImage()的分析,其他最後它真正調用的方法是:
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
下面分析其內部代碼的實現(只截取重要部分):
if (targetSize == null) { //根據 ImageView的寬高 和 設置的最大圖片寬高 計算出 目標圖片的尺寸(如果圖片寬(高)等於0,那麼會取configuration中的設置的最大圖片的尺寸(默認去屏幕尺寸)) targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize()); } //根據 uri 和 圖片尺寸計算出 緩存的key String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize); engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);//存儲起來(Map存儲,key:imageView的hashcode;value:計算出來的 cachekey) listener.onLoadingStarted(uri, imageAware.getWrappedView());//回調函數的回調 //第一層緩存(內存緩存) Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);//普通的基於lru算法的緩存(使用LinkedHashMap實現) if (bmp != null && !bmp.isRecycled()) {//緩存中有,就直接取出來,不去通過網絡獲取了 L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey); if (options.shouldPostProcess()) {//是否設置了BitmapProcessor(Bitmap處理器,對 Bitmap獲取到的Bitmap做相應的處理) ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, progressListener, engine.getLockForUri(uri)); ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo, defineHandler(options));//這是一個Runnable任務,run主體方法中,會調用Options中設置的BitmapProcessor處理圖片,然後加入到engine的線程池中執行對應顯示圖片的任務 if (options.isSyncLoading()) {//同步執行,直接調用 displayTask.run(); } else {//異步執行,添加到線程池中,等待異步執行 engine.submit(displayTask); } } else { options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);//根據設置Displayer直接為ImageView設置Drawable(根據要求,通過Bitmap創建對應的Drawable) listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);//回調方法的調用 } } else {// 內存緩存沒有,走 第二層磁盤緩存 if (options.shouldShowImageOnLoading()) {//設置正在加載的圖片 imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources)); } else if (options.isResetViewBeforeLoading()) {//如果設置加載之前,重置圖片,那麼就將ImageView的 圖像設置為null imageAware.setImageDrawable(null); } ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, progressListener, engine.getLockForUri(uri));//最後一個參數:創建url對應的線程鎖 LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo, defineHandler(options));//創建下載任務 if (options.isSyncLoading()) {//同步 displayTask.run();//直接調用 Runable的 run()方法 } else {//異步執行(默認)(運行在子線程中,將其添加到線程池中) engine.submit(displayTask); } }代碼都已經加上注釋,其實它也是采用的兩級級緩存,通過之前的配置選項也可以看出來,內存,本地磁盤這兩級緩存來優化體驗。每個代碼的內部具體實現,自己查閱內部代碼實現吧,我就不詳細分析其內部實現,下面重點分析LoadAndDisplayImageTask這個任務中的具體實現,其實它內部就是先從磁盤緩存中查找是否含有對應的緩存,有的話直接取出來,進行處理解碼設置給ImageView,否者調用對應的downloader去下載對應的資源。主要看它run()方法的實現:
loadFromUriLock.lock();//線程安全 Bitmap bmp; try { checkTaskNotActual();//判斷當前ImageView是否回首掉了,是否正在被其他線程使用 bmp = configuration.memoryCache.get(memoryCacheKey);//從緩存中獲取數據 if (bmp == null || bmp.isRecycled()) {//沒有從緩存中獲取數據,或者bitmap已經回收掉了 bmp = tryLoadBitmap();//先從緩存中獲取數據,如果沒有,再從網絡下載,然後存儲在到本地(如果設置緩存在本地) if (bmp == null) return; // listener callback already was fired checkTaskNotActual(); checkTaskInterrupted(); if (options.shouldPreProcess()) { L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey); bmp = options.getPreProcessor().process(bmp);//提前處理器,對Bitmap進行相應的處理(這個需要自己配置,並且需要自己實現對應的PreProcessor) if (bmp == null) { L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey); } } //如果設置緩存到內存中,將數據緩存到內存中.(存儲到內存中的是已經壓縮處理後的圖片) if (bmp != null && options.isCacheInMemory()) { L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey); configuration.memoryCache.put(memoryCacheKey, bmp); } } else { loadedFrom = LoadedFrom.MEMORY_CACHE; L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey); } if (bmp != null && options.shouldPostProcess()) { L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey); bmp = options.getPostProcessor().process(bmp);//如果配置,執行對Bitmap的處理操作 if (bmp == null) { L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey); } } checkTaskNotActual(); checkTaskInterrupted(); } catch (TaskCancelledException e) { fireCancelEvent(); return; } finally { loadFromUriLock.unlock(); } DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);//為Imageview設置圖片的task,內部是調用displayer實現的 runTask(displayBitmapTask, syncLoading, handler, engine);//如果是異步操作,並且handler不為null,用handler執行此task,也就是設置圖片的操作要運行在主線程中下面主要對tryLoadBitmap()方法內部進行分析,其內部會先從內存中獲取圖片,如果有處理之後返回,如果沒有,使用downloader去下載圖片。
File imageFile = configuration.diskCache.get(uri);//下載之前從緩存中獲取(可能已經存在,其他線程現在了相同的圖片) if (imageFile != null && imageFile.exists() && imageFile.length() > 0) { L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey); loadedFrom = LoadedFrom.DISC_CACHE; checkTaskNotActual(); bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));//decodeImage()操作:讀取數據流,然後調整圖片的應該縮小的大小(使用BitmapFactory.Options實現, // 裡面使用BufferInputStream來存儲數據流,重復使用) } if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey); loadedFrom = LoadedFrom.NETWORK; String imageUriForDecoding = uri; //第一個判斷:是否緩存到本地,只有第一個判斷為true,才會執行第二個判斷方法 if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {//tryCacheImageOnDisk()下載圖片,並緩存本地 imageFile = configuration.diskCache.get(uri); if (imageFile != null) { imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath()); } } checkTaskNotActual(); //如果沒有設置緩存本地,那麼執行下面的方法的時候,imageUriForDecoding 還是 http,還是會調用 Downloader的 getStream()方法, //此方法會判斷uri是什麼開頭的,可以處理http,file,content,assets等 bitmap = decodeImage(imageUriForDecoding); if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { fireFailEvent(FailType.DECODING_ERROR, null); } }首先對decodeImage()方法進行分析,此方法就是調用decoder的decode()方法進行實現,所以直接看官方demo使用的BaseImageDecoder的decode()方法的實現:
InputStream imageStream = getImageStream(decodingInfo);//如果沒有下載,下載圖片,否則獲取到緩存到本地的file的流 if (imageStream == null) { L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey()); return null; } try { imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);//創建一個file info(計算出圖片流的圖片的實際尺寸) //這裡很重要;因為上面已經decodeStream()已經將上面的流給讀取了,當前流指定的位置已經改變了,所以下面的流要重置一下(兩種方式,如果支持mark(),那麼就reset()之前的位置) //如果不支持,那麼就重新下載這個流 imageStream = resetStream(imageStream, decodingInfo);//重置圖片流,如果圖片流可以重置到之前讀取的位置,那麼就reset(),如果不能,就重新下載這個流 //創建縮放的比例,然後賦值創建Options返回 Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);//參數1:圖片流的大小, decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);//根據流創建對應的Bitmap(如果這裡的圖片源太大,此方法會報異常,親測結果) } finally { IoUtils.closeSilently(imageStream);//不要忘記:將數據流關閉 } if (decodedBitmap == null) { L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey()); } else { decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal); } return decodedBitmap;有兩個地方需要注意一下,第一個:getImageStream()獲取的輸入流是緩沖輸入流,支持markSupport()和reset()和mark()方法,如果一個流都到末尾之後,可以調用reset()將讀取位置重置到頭部,這個流就可以繼續讀取數據。在BitmapFactory.decodeStream()第一次獲取圖片的尺寸的時候,已經將流讀到末尾了;第二次BitmapFactory.decodeStream()的之前必須調用這個輸入流的reset()方法重置這個流,才能繼續使用這個流。第二個:BitmapFactory.decodeStream()方法報異常,由於圖片太大,沒找到解決方法(希望哪位大神了解的,告知小弟)。
下面分析,磁盤緩存沒有對應的緩存的時候,執行的方法:tryCacheImageOnDisk().
loaded = downloadImage();//下載圖片,並將圖片緩存到本地緩存中 if (loaded) { int width = configuration.maxImageWidthForDiskCache; int height = configuration.maxImageHeightForDiskCache; if (width > 0 || height > 0) { L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey); resizeAndSaveImage(width, height);// 調整圖片大小,並將調整後的圖片的Bitmap緩存在本地緩存中// TODO : process boolean result } }首先看downloadImage()方法的實現:
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader()); if (is == null) { L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey); return false; } else { try { return configuration.diskCache.save(uri, is, this);//下載完成之後,緩存在本地(沒有經過處理和壓縮的圖片流) } finally { IoUtils.closeSilently(is);//釋放數據流 } }緊接著看getDownloader.getStream()的實現,官方demo采用的是BaseImageDownloader 的 getStream()的實現:
switch (Scheme.ofUri(imageUri)) { case HTTP: case HTTPS://網絡獲取 return getStreamFromNetwork(imageUri, extra); case FILE://本地文件中獲取 return getStreamFromFile(imageUri, extra); case CONTENT://ContentProvider return getStreamFromContent(imageUri, extra); case ASSETS://assets文件夾中獲取 return getStreamFromAssets(imageUri, extra); case DRAWABLE://從資源文件中獲取 return getStreamFromDrawable(imageUri, extra); case UNKNOWN: default: return getStreamFromOtherSource(imageUri, extra); }主要看從網絡獲取圖片實現:
HttpURLConnection conn = createConnection(imageUri, extra); int redirectCount = 0; while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) { conn = createConnection(conn.getHeaderField("Location"), extra); redirectCount++; } InputStream imageStream; try { imageStream = conn.getInputStream(); } catch (IOException e) { // Read all data to allow reuse connection (http://bit.ly/1ad35PY) IoUtils.readAndCloseStream(conn.getErrorStream()); throw e; } if (!shouldBeProcessed(conn)) {//狀態碼!=200 IoUtils.closeSilently(imageStream); throw new IOException("Image request failed with response code " + conn.getResponseCode()); } //注意它將外層包裹了 BufferedInputStream(使用緩沖輸入流,它可以markSupport()返回true,支持mark()和reset()操作,可以重復讀取流數據) //解決問題:在BitmapFactory.decode()執行之後,讀取的位置到達了流的結尾,如果是這個對象,就可以調用reset()方法之後繼續使用這個流 return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());下面看resizeAndSaveImage()方法的實現,內部調整存儲在本地的圖片流的大小,生成對應的Bitmap存儲到磁盤緩存中。
File targetFile = configuration.diskCache.get(uri); if (targetFile != null && targetFile.exists()) { ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight); DisplayImageOptions specialOptions = new DisplayImageOptions.Builder().cloneFrom(options) .imageScaleType(ImageScaleType.IN_SAMPLE_INT).build(); ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, Scheme.FILE.wrap(targetFile.getAbsolutePath()), uri, targetImageSize, ViewScaleType.FIT_INSIDE, getDownloader(), specialOptions); Bitmap bmp = decoder.decode(decodingInfo); if (bmp != null && configuration.processorForDiskCache != null) { L.d(LOG_PROCESS_IMAGE_BEFORE_CACHE_ON_DISK, memoryCacheKey); bmp = configuration.processorForDiskCache.process(bmp); if (bmp == null) { L.e(ERROR_PROCESSOR_FOR_DISK_CACHE_NULL, memoryCacheKey); } } if (bmp != null) { saved = configuration.diskCache.save(uri, bmp);//將處理完的Bitmap存儲到本地緩存 bmp.recycle(); } }
1、概述 相信做Android開發的寫得最多的就是ListView,GridView的適配器吧,記得以前開發一同事開發項目,一個項目下來基本就一直在寫Lis
Android開放的平台,獲得高度自由度,用戶也要承受系統當中一些潛在的問題,比如後台流量的消耗。那麼怎樣才能有效控制Android的流量使用呢?下面這5個
你是否覺得手機QQ上的好友列表那個控件非常棒? 不是..... 那也沒關系,學多一點知識對自己也有益無害。那麼我們就開始吧。展開型列表控件, 原名ExpandableLi
??上一篇文章,我們主要分析了Activity的正常情況下生命周期及其方法,本篇主要涉及內容為Activity的異常情況下的生命周期。Activity異常生命周期??異常