1. 程式人生 > >Android--圖片載入處理(記憶體溢位和三級快取)

Android--圖片載入處理(記憶體溢位和三級快取)

最簡單的解決辦法,用現成的框架,推薦glide和picasso

用法:在build.gradle中加入:

repositories {
  mavenCentral()
  maven { url 'https://maven.google.com' }
}

dependencies {
  compile 'com.github.bumptech.glide:glide:4.1.1'
  annotationProcessor 'com.github.bumptech.glide:compiler:4.1.1'
}

使用:
ImageView imageView = (ImageView) findViewById(R.id.my_image_view);

  Glide.with(this).load("http://goo.gl/gEgYUd").into(imageView);

this:上下文  load裡面要載入的圖片網址

用法:在build.gradle中加入:

compile 'com.squareup.picasso:picasso:2.5.2'

使用:
Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").resize(50, 50).into(imageView);

三、記憶體溢位解決原理:(轉自:http://www.cnblogs.com/Free-Thinker/p/6078765.html)

方案一、讀取圖片時注意方法的呼叫,適當壓縮  儘量不要使用setImageBitmapsetImageResourceBitmapFactory.decodeResource

來設定一張大圖,因為這些函式在完成decode後,最終都是通過java層的createBitmap來完成的,需要消耗更多記憶體。 因此,改用先通過BitmapFactory.decodeStream方法,創建出一個bitmap,再將其設為ImageView的  source,decodeStream最大的祕密在於其直接呼叫JNI>>nativeDecodeAsset()來完成decode,無需再使用java層的createBitmap,從而節省了java層的空間。

         InputStream is = this.getResources().openRawResource(R.drawable.pic1);

         BitmapFactory.Options options = new  BitmapFactory.Options();

         options.inJustDecodeBounds =  false;

         options.inSampleSize =  10;   // widthhight設為原來的十分一

         Bitmap btp =  BitmapFactory.decodeStream(is, null,  options);


如果在讀取時加上圖片的Config引數,可以跟有效減少載入的記憶體,從而跟有效阻止拋out of Memory異常。

   /**

     *  以最省記憶體的方式讀取本地資源的圖片

      */

    public  static  Bitmap readBitMap(Context  context, int resId){ 

         BitmapFactory.Options opt = new  BitmapFactory.Options();

         opt.inPreferredConfig =  Bitmap.Config.RGB_565;

         opt.inPurgeable = true;

         opt.inInputShareable = true;

         //  獲取資源圖片

        InputStream is =  context.getResources().openRawResource(resId);

         return  BitmapFactory.decodeStream(is, null, opt);

         }


另外,decodeStream直接拿圖片來讀取位元組碼,  不會根據機器的各種解析度來自動適應,使用了decodeStream之後,需要在hdpi和mdpi,ldpi中配置相應的圖片資源,  否則在不同解析度機器上都是同樣大小(畫素點數量),顯示出來的大小就不對了。
方案二、在適當的時候及時回收圖片佔用的記憶體  通常Activity或者Fragment在onStop/onDestroy時候就可以釋放圖片資源:  

 if(imageView !=  null &&  imageView.getDrawable() != null){  

      Bitmap oldBitmap =  ((BitmapDrawable) imageView.getDrawable()).getBitmap(); 

       imageView.setImageDrawable(null); 

      if(oldBitmap !=  null){ 

            oldBitmap.recycle();  

            oldBitmap =  null;

      } 

 }

 //  Other code.

 System.gc();


在釋放資源時,需要注意釋放的Bitmap或者相關的Drawable是否有被其它類引用。如果正常的呼叫,可以通過Bitmap.isRecycled()方法來判斷是否有被標記回收;而如果是被UI執行緒的介面相關程式碼使用,就需要特別小心避免回收有可能被使用的資源,不然有可能丟擲系統異常: E/AndroidRuntime: java.lang.IllegalArgumentException: Cannot draw recycled  bitmaps 並且該異常無法有效捕捉並處理。
方案三、不必要的時候避免圖片的完整載入 只需要知道圖片大小的情形下,可以不完整載入圖片到記憶體。 在使用BitmapFactory壓縮圖片的時候,BitmapFactory.Options設定inJustDecodeBounds為true後,再使用decodeFile()等方法,可以在不分配空間狀態下計算出圖片的大小。示例:   

 BitmapFactory.Options opts =  new  BitmapFactory.Options();  

 //  設定inJustDecodeBounds為false

 opts.inJustDecodeBounds = false

 //  使用decodeFile方法得到圖片的寬和高 

 BitmapFactory.decodeFile(path,  opts); 

 //  打印出圖片的寬和高

 Log.d("example", opts.outWidth + "," + opts.outHeight);

(ps:原理其實就是通過圖片的頭部資訊讀取圖片的基本資訊)
方案四、優化Dalvik虛擬機器的堆記憶體分配  堆(HEAP)是VM中佔用記憶體最多的部分,通常是動態分配的。堆的大小不是一成不變的,通常有一個分配機制來控制它的大小。比如初始的HEAP是4M大,當4M的空間被佔用超過75%的時候,重新分配堆為8M大;當8M被佔用超過75%,分配堆為16M大。倒過來,當16M的堆利用不足30%的時候,縮減它的大小為8M大。重新設定堆的大小,尤其是壓縮,一般會涉及到記憶體的拷貝,所以變更堆的大小對效率有不良影響。 Heap  Utilization是堆的利用率。當實際的利用率偏離這個百分比的時候,虛擬機器會在GC的時候調整堆記憶體大小,讓實際佔用率向個百分比靠攏。使用  dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強程式堆記憶體的處理效率。  

 private final static float  TARGET_HEAP_UTILIZATION = 0.75f; 

 //  在程式onCreate時就可以呼叫

 VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);


方案五、自定義堆(Heap)記憶體大小  對於一些Android專案,影響效能瓶頸的主要是Android自己記憶體管理機制問題,目前手機廠商對RAM都比較吝嗇,對於軟體的流暢性來說RAM對效能的影響十分敏感,除了優化Dalvik虛擬機器的堆記憶體分配外,我們還可以強制定義自己軟體的對記憶體大小,我們使用Dalvik提供的  dalvik.system.VMRuntime類來設定最小堆記憶體為例:  

 private final static int  CWJ_HEAP_SIZE = 6 * 1024 * 1024  ;

 VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);  //  設定最小heap記憶體為6MB大小。


但是上面方法還是存在問題,函式setMinimumHeapSize其實只是改變了堆的下限值,它可以防止過於頻繁的堆記憶體分配,當設定最小堆記憶體大小超過上限值(Max Heap  Size)時仍然採用堆的上限值,對於記憶體不足沒什麼作用。  
在預設情況下android程序的記憶體佔用量為16M,因為Bitmap他除了java中持有資料外,底層C++的  skia圖形庫還會持有一個SKBitmap物件,因此一般圖片佔用記憶體推薦大小應該不超過8M。這個可以調整,編譯原始碼時可以設定引數。

參考資料:http://www.tuicool.com/articles/yemM7zf

方案六:在Manifest.xml檔案裡面的<application  裡面新增Android:largeHeap="true"

簡單粗暴。這種方法允許應用需要耗費手機很多的記憶體空間,但卻是最快捷的解決辦法

四、三級快取實現原理:(轉自:http://blog.csdn.net/lovoo/article/details/51456515)

實現圖片快取也不難,需要有相應的cache策略。這裡我採用 記憶體-檔案-網路 三層cache機制,其中記憶體快取包括強引用快取和軟引用快取(SoftReference),其實網路不算cache,這裡姑且也把它劃到快取的層次結構中。當根據url向網路拉取圖片的時候,先從記憶體中找,如果記憶體中沒有,再從快取檔案中查詢,如果快取檔案中也沒有,再從網路上通過http請求拉取圖片。在鍵值對(key-value)中,這個圖片快取的key是圖片url的hash值,value就是bitmap。所以,按照這個邏輯,只要一個url被下載過,其圖片就被快取起來了。

但這裡不使用SoftReference,而使用LruCache進行圖片的快取 
為什麼使用LruCache: 
這個類非常適合用來快取圖片,它的主要演算法原理是把最近使用的物件用強引用儲存在 LinkedHashMap 中,並且把最近最少使用的物件在快取值達到預設定值之前從記憶體中移除。

在過去,我們經常會使用一種非常流行的記憶體快取技術的實現,即軟引用或弱引用 (SoftReference or WeakReference)。但是現在已經不再推薦使用這種方式了,因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的物件,這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的資料會儲存在本地的記憶體當中,因而無法用一種可預見的方式將其釋放,這就有潛在的風險造成應用程式的記憶體溢位並崩潰。

具體實現:

1)在構造方法裡初始化LruCache mCache

if (mCache == null) {
            // 最大使用的記憶體空間
            int maxSize = (int) (Runtime.getRuntime().freeMemory() / 4);
            mCache = new LruCache<String, Bitmap>(maxSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2)去記憶體中取

Bitmap bitmap = mCache.get(url);
        if (bitmap != null) {
            // 直接顯示
            iv.setImageBitmap(bitmap);
            return;
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3)去硬碟上取

bitmap = loadBitmapFromLocal(url);
        if (bitmap != null) {
            // 直接顯示
            iv.setImageBitmap(bitmap);
            return;
            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

4)從網路載入

loadBitmapFromNet(iv, url);
詳細程式碼:
public class ImageHelper {
    // 記憶體快取池
    // private Map<String, SoftReference<Bitmap>> mCache = new
    // LinkedHashMap<String, SoftReference<Bitmap>>();

    // LRUCahce 池子
    private static LruCache<String, Bitmap> mCache;
    private static Handler mHandler;
    private static ExecutorService mThreadPool;
    private static Map<ImageView, Future<?>> mTaskTags = new LinkedHashMap<ImageView, Future<?>>();
    private Context mContext;

    public ImageHelper(Context context) {
        this.mContext = context;
        if (mCache == null) {
            // 最大使用的記憶體空間
            int maxSize = (int) (Runtime.getRuntime().freeMemory() / 4);
            mCache = new LruCache<String, Bitmap>(maxSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }

        if (mHandler == null) {
            mHandler = new Handler();
        }

        if (mThreadPool == null) {
            // 最多同時允許的執行緒數為3個
            mThreadPool = Executors.newFixedThreadPool(3);
        }
    }

    public void display(ImageView iv, String url) {
        // 1.去記憶體中取
        Bitmap bitmap = mCache.get(url);
        if (bitmap != null) {
            // 直接顯示
            iv.setImageBitmap(bitmap);
            return;
        }

        // 2.去硬碟上取
        bitmap = loadBitmapFromLocal(url);
        if (bitmap != null) {
            // 直接顯示
            iv.setImageBitmap(bitmap);
            return;
        }

        // 3. 去網路獲取圖片
        loadBitmapFromNet(iv, url);
    }

    private void loadBitmapFromNet(ImageView iv, String url) {
        // 開執行緒去網路獲取
        // 使用執行緒池管理
        // new Thread(new ImageLoadTask(iv, url)).start();

        // 判斷是否有執行緒在為 imageView載入資料
        Future<?> futrue = mTaskTags.get(iv);
        if (futrue != null && !futrue.isCancelled() && !futrue.isDone()) {
            System.out.println("取消 任務");
            // 執行緒正在執行
            futrue.cancel(true);
            futrue = null;
        }

        // mThreadPool.execute(new ImageLoadTask(iv, url));
        futrue = mThreadPool.submit(new ImageLoadTask(iv, url));
        // Future 和 callback/Runable
        // 返回值,持有正在執行的執行緒
        // 儲存
        mTaskTags.put(iv, futrue);
        System.out.println("標記 任務");
    }

    class ImageLoadTask implements Runnable {

        private String mUrl;
        private ImageView iv;

        public ImageLoadTask(ImageView iv, String url) {
            this.mUrl = url;
            this.iv = iv;
        }

        @Override
        public void run() {
            // HttpUrlconnection
            try {
                // 獲取連線
                HttpURLConnection conn = (HttpURLConnection) new URL(mUrl).openConnection();

                conn.setConnectTimeout(30 * 1000);// 設定連線伺服器超時時間
                conn.setReadTimeout(30 * 1000);// 設定讀取響應超時時間

                // 連線網路
                conn.connect();

                // 獲取響應碼
                int code = conn.getResponseCode();

                if (200 == code) {
                    InputStream is = conn.getInputStream();

                    // 將流轉換為bitmap
                    Bitmap bitmap = BitmapFactory.decodeStream(is);

                    // 儲存到本地
                    write2Local(mUrl, bitmap);

                    // 儲存到記憶體
                    mCache.put(mUrl, bitmap);

                    // 圖片顯示:不可取
                    // iv.setImageBitmap(bitmap);
                    mHandler.post(new Runnable() {

                        @Override
                        public void run() {
                            // iv.setImageBitmap(bitmap);

                            display(iv, mUrl);
                        }
                    });
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    /**
     * 本地種去去圖片
     * 
     * @param url
     */
    private Bitmap loadBitmapFromLocal(String url) {
        // 去找檔案,將檔案轉換為bitmap
        String name;
        try {
            name = MD5Encoder.encode(url);

            File file = new File(getCacheDir(), name);
            if (file.exists()) {

                Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());

                // 儲存到記憶體
                mCache.put(url, bitmap);
                return bitmap;
            }

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return null;
    }

    private void write2Local(String url, Bitmap bitmap) {
        String name;
        FileOutputStream fos = null;
        try {
            name = MD5Encoder.encode(url);
            File file = new File(getCacheDir(), name);
            fos = new FileOutputStream(file);

            // 將影象寫到流中
            bitmap.compress(CompressFormat.JPEG, 100, fos);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                    fos = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private String getCacheDir() {
        String state = Environment.getExternalStorageState();
        File dir = null;
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            // 有sd卡
            dir = new File(Environment.getExternalStorageDirectory(), "/Android/data/" + mContext.getPackageName()
                    + "/icon");
        } else {
            // 沒有sd卡
            dir = new File(mContext.getCacheDir(), "/icon");

        }

        if (!dir.exists()) {
            dir.mkdirs();
        }

        return dir.getAbsolutePath();
    }
}

使用方法:
ImageView iv = (contentView)findViewById(R.id.iv);
String url = "http://localhost:8080/web/1.jpg";
new IamgeHelper(this).display(iv,url);

大神部落格詳細介紹:https://blog.csdn.net/guolin_blog/article/details/53759439