CACHING BITMAPS TRONG ANDROID

Tải một Bitmap duy nhất tới ứng dụng của bạn là đơn giản. Nhưng vấn đề sẽ trở nên phức tạp hơn nếu bạn cần tải một tập hợp lớn các hình ảnh cùng một lúc. Trong nhiều trường hợp (chẳng hạn với các thành phần ListView, GridView, ViewPager) tổng sổ hình ảnh đang hiển thị trên màn hình kết hợp với những hình ảnh mà có thể nhanh chóng cuộn lên trên về căn bản là không giới hạn.

Sử dụng bộ nhớ được giữ lại với các thành phần như trên bằng cách tái sử dụng những thành phần con (views) khi chúng di chuyển ra ngoài vùng hiển thị. Thu gom rác (garbage collector) và giải phóng Bitmap đã được tải của bạn khi chúng không con được truy cập đến. Đến đây mọi chuyện đều tốt đẹp nhưng để giữ giao diện một cách xuyên suốt và tải hình ảnh nhanh chóng bạn không muốn tải lại những hình ảnh trước đó mỗi lần cuộn lên trên. Sử dụng bộ nhớ và bộ nhớ đĩa đệm có thể giúp đỡ việc này, cho phép cách thành phần UI nhanh chóng tải lại những hình ảnh đã được xử lý.

Bài học này sẽ giúp bạn học cách sử dụng bộ nhớ và bộ nhớ đệm để giúp cải thiện khả năng hồi đáp và tính lưu động cho các thành phần giao diện khi phải tải nhiều Bitmap cùng một lúc.

Sử dụng bộ nhớ đệm (Use a Memory Cache)

Bộ nhớ đệm cung cấp tốc độ truy cập rất nhanh tới các Bitmap. Lớp LruCache (Sẵn có trong thư viện hỗ trợ sử dụng API 4 trở lên) đặc biệt phù hợp với nhiệm vụ lưu giữ(caching) Bitmaps, và giữ các đối tượng gần đầy trong một liên kết mạnh (strong references) sử dụng LinkedHashMap và trục xuất các thành viên đã được sử dụng trước khi bộ nhớ đệm vượt quá kích thước được chỉ định.

Để chọn một kích cỡ phù hợp với LruCache, một số yếu tố cần được xem xét, ví dụ:

  • Có bao nhiêu hình ảnh sẽ được hiển thị trên màn hình cùng một lúc? Có bao nhiêu hình ảnh cần phải được hiển thị ngay.

  • Kích thước của màn hình và mật độ điểm ảnh của thiết bị là bao nhiêu? Một màn hình độ phân giải cao (xhdpi) như Galaxy Nexus sẽ cần một bộ nhớ cache lớn hơn để giữ cùng một số hình ảnh trong bộ nhớ so với một thiết bị như Nexus S (hdpi).

  • Kích thước và cấu hình Bitmap là bao nhiêu? Và sau đó sẽ tốn bao nhiêu bộ nhớ? Làm cách nào ảnh sẽ được truy cập. Hình ảnh nào sẽ được truy cập thường xuyên hơn những hình ảnh khác?

  • Bạn có thể cân bằng giữa chất lượng và số lượng? Đôi khi nó có thể hữu ích hơn để lưu trữ một số lượng lớn ảnh bitmap chất lượng thấp hơn, có khả năng tải một phiên bản chất lượng cao hơn trong một công việc nền (background tasks).

  • Không có kích thước cụ thể hoặc công thức phù hợp với tất cả các ứng dụng , do đó bạn phải phân tích ứng dụng của bạn và đưa ra một giải pháp phù hợp.

  • Một bộ nhớ đệm với kích thước quá nhỏ sẽ gây thêm tốn bộ nhớ và không có lợi ích, một bố nhớ đệm với kích thước rất lớn một lần nữa có thể gây ra ngoại lệ OutOfMemory và có quá ít bộ nhớ dành cho phần còn lại cho ứng dụng của bạn.

Dưới đây là một ví dụ về việc thiết lập một LruCache cho bitmap:

Override
protected void onCreate(Bundle savedInstanceState){
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory =(int)(Runtime.getRuntime().maxMemory()/1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory /8;

    mMemoryCache =newLruCache<String,Bitmap>(cacheSize){
        @Override
        protected int sizeOf(String key,Bitmap bitmap){
            // The cache size will be measured in kilobytes rather than
            // number of items.
            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);
}

Chú ý: Trong ví dụ này, 1/8 của bộ nhớ ứng dụng được cấp phát cho bộ nhớ cache của chúng ta. Trên một thiết bị với độ phân giải bình thường (hdpi) với cấu hình này sẽ chiếm bộ nhớ tối thiểu khoảng 4MB (32/8). Một GridView được lấp đầy những hình ảnh với một thiết bị với độ phân giải 800×480 sẽ sử dụng khoảng 1.5MB (800 * 480 * 4 bytes), vì vậy đây là bộ nhớ tối thiểu có thể hiển thị được (4/1.5 = ~2.5) trang hình ảnh trong bộ nhớ.

Khi tải một bitmap tới ImageView, LruCache được kiểm tra đầu tiên. Nếu có một vùng đệm được tìm thấy, nó được sử dụng ngay lập tức để cập nhật các ImageView, nếu không nhiệm vụ nền (background thread) được sinh ra để xử lý hình ảnh:

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 =newBitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

Các BitmapWorkerTask (trong bài trước) cũng cần phải được cập nhật để thêm các cài đặt vào bộ nhớ cache:

class BitmapWorkerTask extends AsyncTask<Integer,Void,Bitmap>{
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer...params){
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(),params[0],100,100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

Để cache lại Bitmap sử dụng:

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

Để lấy Bitmap từ bộ nhớ đệm:

    public Bitmap getBitmapFromMemCache(String key) {
           if(mMemoryCache != null) {
                 return mMemoryCache.get(key);
           }
     return null;
   }

Sử dụng đĩa đệm (Use a Disk Cache)

Một bộ nhớ cache (Memory Cache) trong phần trên là là hữu ích trong việc tăng tốc độ truy cập tới các Bitmap vừa được sử dụng, tuy nhiên bạn không thể dựa vào hình ảnh đang có sẵn trong bộ nhớ cache này. Các thành phần như GridView với bộ dữ liệu lớn hơn có thể dễ dàng làm đầy bộ nhớ này. Ứng dụng của bạn có thể bị gián đoạn bởi một nhiệm vụ như một cuộc gọi điện thoại, và trong khi ở chế độ nền (background) nó có thể bị giết chết (kill thread) và bộ nhớ cache bị phá hủy. Sau khi người dùng quay trở lại, ứng dụng của bạn có thể xử lý mỗi hình ảnh lại một lần nữa.

Một bộ nhớ đĩa đệm (Disk Cache) có thể được sử dụng trong những trường hợp này và giúp làm giảm thời gian tải hình ảnh mà không còn có sẵn trong bộ nhớ đệm (Memory Cache). Tất nhiên, lấy hình ảnh từ đĩa (disk) là chậm hơn so với tải từ bộ nhớ (memory) và nên được thực hiện trong một thread nền (background thread).

Chú ý: Một ContentProvider có thể là một nơi thích hợp hơn để lưu trữ hình ảnh được lưu trữ nếu chúng được truy cập thường xuyên hơn, ví dụ như trong một ứng dụng thư viện hình ảnh.

Đây là 1 ví dụ của class sử dụng DiskLruCache được triển khai trong bộ mã nguồn mở Android. Code dưới đây cập nhất thêm sử dụng đĩa đệm vào bộ nhớ đệm (Memory cache) sẵn có:

privateDiskLruCache mDiskLruCache;
privatefinalObject mDiskCacheLock =newObject();
privateboolean mDiskCacheStarting =true;
privatestaticfinalint DISK_CACHE_SIZE =1024*1024*10;// 10MB
privatestaticfinalString DISK_CACHE_SUBDIR ="thumbnails";

@Override
protectedvoid onCreate(Bundle savedInstanceState){
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    newInitDiskCacheTask().execute(cacheDir);
    ...
}

classInitDiskCacheTaskextendsAsyncTask<File,Void,Void>{
    @Override
    protectedVoid doInBackground(File...params){
        synchronized(mDiskCacheLock){
            File cacheDir =params[0];
            mDiskLruCache =DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting =false;// Finished initialization
            mDiskCacheLock.notifyAll();// Wake any waiting threads
        }
        returnnull;
    }
}

classBitmapWorkerTaskextendsAsyncTask<Integer,Void,Bitmap>{
    ...
    // Decode image in background.
    @Override
    protectedBitmap doInBackground(Integer...params){
        finalString imageKey =String.valueOf(params[0]);

        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if(bitmap ==null){// Not found in disk cache
            // Process as normal
            finalBitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(),params[0],100,100));
        }

        // Add final bitmap to caches
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

publicvoid addBitmapToCache(String key,Bitmap bitmap){
    // Add to memory cache as before
    if(getBitmapFromMemCache(key)==null){
        mMemoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized(mDiskCacheLock){
        if(mDiskLruCache !=null&& mDiskLruCache.get(key)==null){
            mDiskLruCache.put(key, bitmap);
        }
    }
}

publicBitmap getBitmapFromDiskCache(String key){
    synchronized(mDiskCacheLock){
        // Wait while disk cache is started from background thread
        while(mDiskCacheStarting){
            try{
                mDiskCacheLock.wait();
            }catch(InterruptedException e){}
        }
        if(mDiskLruCache !=null){
            return mDiskLruCache.get(key);
        }
    }
    returnnull;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
publicstaticFile getDiskCacheDir(Context context,String uniqueName){
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    finalString cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())||
                    !isExternalStorageRemovable()? getExternalCacheDir(context).getPath():
                            context.getCacheDir().getPath();

    returnnewFile(cachePath +File.separator + uniqueName);
}

Chú ý: Thậm chị khởi tạo bộ nhớ đĩa đệm đòi hỏi đĩa phải tính toán, vì vậy không nên làm việc này trong luồng chính (main UI thread). Tuy nhiên, điều này không đảm bảo là không có một cơ hội nào truy cập tới bộ nhớ cache trước khi được khởi tạo. Để giải quyết vấn đề này, trong việc thực hiện ở trên, một đối tượng khóa (lock) đảm bảo rằng các ứng dụng không đọc từ bộ nhớ cache đĩa cho đến khi bộ nhớ cache đã được khởi tạo.

Trong khi bộ nhớ đệm (memory cache) được kiểm tra trong luồng chính (Ui thread), bộ nhớ đĩa đệm (disk cache) được kiểm tra trong thread nền (background thread). Các tính toán trên đĩa nên không bao giờ diễn ra trên luồn chính. Khi xử lý hình ảnh hoàn tất, bitmap được thêm vào cả bộ nhớ và bộ nhớ đĩa đệm để sử dụng trong tương lai.

Demo: DisplayBitmap.zip


All Rights Reserved