Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> [Android] 緩存機制

[Android] 緩存機制

編輯:關於Android編程

移動開發本質上就是手機和服務器之間進行通信,需要從服務端獲取數據。反復通過網絡獲取數據是比較耗時的,特別是訪問比較多的時候,會極大影響了性能,Android中可通過緩存機制來減少頻繁的網絡操作,減少流量、提升性能。

實現原理

把不需要實時更新的數據緩存下來,通過時間或者其他因素 來判別是讀緩存還是網絡請求,這樣可以緩解服務器壓力,一定程度上提高應用響應速度,並且支持離線閱讀。
  

Bitmap的緩存

在許多的情況下(像 ListView, GridView 或 ViewPager 之類的組件 )我們需要一次性加載大量的圖片,在屏幕上顯示的圖片和所有待顯示的圖片有可能需要馬上就在屏幕上無限制的進行滾動、切換。

像ListView, GridView 這類組件,它們的子項當不可見時,所占用的內存會被回收以供正在前台顯示子項使用。垃圾回收器也會釋放你已經加載了的圖片占用的內存。如果你想讓你的UI運行流暢的話,就不應該每次顯示時都去重新加載圖片。保持一些內存和文件緩存就變得很有必要了。

使用內存緩存

通過預先消耗應用的一點內存來存儲數據,便可快速的為應用中的組件提供數據,是一種典型的以空間換時間的策略。
LruCache 類(Android v4 Support Library 類庫中開始提供)非常適合來做圖片緩存任務 ,它可以使用一個LinkedHashMap 的強引用來保存最近使用的對象,並且當它保存的對象占用的內存總和超出了為它設計的最大內存時會把不經常使用的對象成員踢出以供垃圾回收器回收。

給LruCache 設置一個合適的內存大小,需考慮如下因素:

還剩余多少內存給你的activity或應用使用 屏幕上需要一次性顯示多少張圖片和多少圖片在等待顯示 手機的大小和密度是多少(密度越高的設備需要越大的 緩存) 圖片的尺寸(決定了所占用的內存大小) 圖片的訪問頻率(頻率高的在內存中一直保存) 保存圖片的質量(不同像素的在不同情況下顯示)

具體的要根據應用圖片使用的具體情況來找到一個合適的解決辦法,一個設置 LruCache 例子:

private LruCache mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // 獲得虛擬機能提供的最大內存,超過這個大小會拋出OutOfMemory的異常
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // 用1/8的內存大小作為內存緩存
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // 這裡返回的不是item的個數,是cache的size(單位1024個字節)
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

當為ImageView加載一張圖片時,會先在LruCache 中看看有沒有緩存這張圖片,如果有的話直接更新到ImageView中,如果沒有的話,一個後台線程會被觸發來加載這張圖片。

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    // 查看下內存緩存中是否緩存了這張圖片
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

在圖片加載的Task中,需要把加載好的圖片加入到內存緩存中。

class BitmapWorkerTask extends AsyncTask {
    ...
    // 在後台完成
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
    addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

使用磁盤緩存

內存緩存能夠快速的獲取到最近顯示的圖片,但不一定就能夠獲取到。當數據集過大時很容易把內存緩存填滿(如GridView )。你的應用也有可能被其它的任務(比如來電)中斷進入到後台,後台應用有可能會被殺死,那麼相應的內存緩存對象也會被銷毀。 當你的應用重新回到前台顯示時,你的應用又需要一張一張的去加載圖片了。

磁盤文件緩存能夠用來處理這些情況,保存處理好的圖片,當內存緩存不可用的時候,直接讀取在硬盤中保存好的圖片,這樣可以有效的減少圖片加載的次數。讀取磁盤文件要比直接從內存緩存中讀取要慢一些,而且需要在一個UI主線程外的線程中進行,因為磁盤的讀取速度是不能夠保證的,磁盤文件緩存顯然也是一種以空間換時間的策略。

如果圖片使用非常頻繁的話,一個 ContentProvider 可能更適合代替去存儲緩存圖片,比如圖片gallery 應用。

下面是一個DiskLruCache的部分代碼:

private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // 初始化內存緩存
    ...
    // 在後台線程中初始化磁盤緩存
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDir);
    ...
}

class InitDiskCacheTask extends AsyncTask {
    @Override
    protected Void doInBackground(File... params) {
        synchronized (mDiskCacheLock) {
            File cacheDir = params[0];
  mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
  mDiskCacheStarting = false; // 結束初始化
  mDiskCacheLock.notifyAll(); // 喚醒等待線程
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask {
    ...
    // 在後台解析圖片
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // 在後台線程中檢測磁盤緩存
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // 沒有在磁盤緩存中找到圖片
 final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // 把這個final類型的bitmap加到緩存中
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // 先加到內存緩存
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    //再加到磁盤緩存
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
        // 等待磁盤緩存從後台線程打開
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.get(key);
        }
    }
    return null;
}

public static File getDiskCacheDir(Context context, String uniqueName) {
    // 優先使用外緩存路徑,如果沒有掛載外存儲,就使用內緩存路徑
final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ?getExternalCacheDir(context).getPath():context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

不能在UI主線程中進行這項操作,因為初始化磁盤緩存也需要對磁盤進行操作。上面的程序片段中,一個鎖對象確保了磁盤緩存沒有初始化完成之前不能夠對磁盤緩存進行訪問。

內存緩存在UI線程中進行檢測,磁盤緩存在UI主線程外的線程中進行檢測,當圖片處理完成之後,分別存儲到內存緩存和磁盤緩存中。

設備配置參數改變時加載問題

由於應用在運行的時候設備配置參數可能會發生改變,比如設備朝向改變,會導致Android銷毀你的Activity然後按照新的配置重啟,這種情況下,我們要避免重新去加載處理所有的圖片,讓用戶能有一個流暢的體驗。

使用Fragment 能夠把內存緩存對象傳遞到新的activity實例中,調用setRetainInstance(true)) 方法來保留Fragment實例。當activity重新創建好後, 被保留的Fragment依附於activity而存在,通過Fragment就可以獲取到已經存在的內存緩存對象了,這樣就可以快速的獲取到圖片,並設置到ImageView上,給用戶一個流暢的體驗。

下面是一個示例程序片段:

private LruCache mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
RetainFragment mRetainFragment =            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = RetainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache(cacheSize) {
            ... //像上面例子中那樣初始化緩存
        }
        mRetainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache mRetainedCache;

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 使得Fragment在Activity銷毀後還能夠保留下來
        setRetainInstance(true);
    }
}

  可以在不適用Fragment(沒有界面的服務類Fragment)的情況下旋轉設備屏幕。在保留緩存的情況下,你應該能發現填充圖片到Activity中幾乎是瞬間從內存中取出而沒有任何延遲的感覺。任何圖片優先從內存緩存獲取,沒有的話再到硬盤緩存中找,如果都沒有,那就以普通方式加載圖片。
 

使用SQLite進行緩存

網絡請求數據完成後,把文件的相關信息(如url(一般作為唯一標示),下載時間,過期時間)等存放到數據庫。下次加載的時候根據url先從數據庫中查詢,如果查詢到並且時間未過期,就根據路徑讀取本地文件,從而實現緩存的效果。

注意:緩存的數據庫是存放在/data/data//databases/目錄下,是占用內存空間的,如果緩存累計,容易浪費內存,需要及時清理緩存。

文件緩存

思路和一般緩存一樣,把需要的數據存儲在文件中,下次加載時判斷文件是否存在和過期(使用File.lastModified()方法得到文件的最後修改時間,與當前時間判斷),存在並未過期就加載文件中的數據,否則請求服務器重新下載。

注意,無網絡環境下就默認讀取文件緩存中的。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved