QUẢN LÝ BỘ NHỚ KHI SỬ DỤNG BITMAP TRONG ANDROID

Tiếp theo đề tài sử dụng Bitmap một cách hiệu quả, trong bài này chúng ta tìm hiểu về cách quản lý bộ nhớ khi sử dụng Bitmap để hiển thị hình ảnh!

Ngoài các bước trong bài trước để lưu trữ lại Bitmap (Caching) , trong một số trường hợp cụ thể chúng ta có thể tạo điều kiện thuận lợi giúp hệ thống

thu gom rác (garbage collection) và tái sử dụng Bitmap khi cần thiết.

Bạn cần quan tâm tới mục tiêu phiên bản Android nhắm tới (Android version targeting), phụ thuộc vào phiên bản Android nhắm tới chúng ta sẽ sử dụng các API cho phù hợp.

Để chuẩn bị cho bài học này, đây là cách quản lý bộ nhớ Bitmap của Android đã phát triển:

  • Trên Android 2.2 (API level8) hoặc thấp hơn, khi thu gom rác thải xảy ra, luồng chính (main thread) trong ứng dụng của bạn có thể bị dừng lại, điều này gây ra sự chậm trễ làm giảm hiệu suất của ứng dụng. **Trên Android 2.3 thêm vào hệ thống thu góm rác (garbage collection) , điều đó có nghĩa là bộ nhớ sẽ được giải phóng ngay sau khi một bitmap không còn tham chiếu. **

  • Trên Android 2.3.3 (API level 10) hoặc thấp hơn, các dữ liệu pixel của Bitmap được sao lưu và lưu trữ trong bộ trong (native memory). Nó tách biệt với Bitmap của chính nó, nó được lưu trữ trong Dalvik heap. Các dữ liệu được sao lưu trong bộ nhớ là không được đưa ra (release) nên không thể dự đoán được, do vậy có khả năng gây ra ứng dụng vượt quá giới hạn bộ nhớ của nó và crash. Tính đến Android 3.0 (API cấp 11), các dữ liệu điểm ảnh được lưu trữ trên Dalvik heap nhưng đi cùng với bitmap liên quan.

Các phần sau đây mô tả làm thế nào để tối ưu hóa quản lý bộ nhớ bitmap cho các phiên bản Android khác nhau.

Quản lý bộ nhớ trên Android 2.3.3 (API level 10) và thấp hơn

Trên Android 2.3.3 (API level 10) và thấp hơn, sử dụng phương thức recycle() được khuyên dùng. Nếu bạn đang hiển thị một lượng lớn dữ liệu bitmap trong ứng dụng của bạn, bạn có khả năng sinh ra các lỗi OutOfMemoryError.Phương thức recycle() cho phép một ứng dụng phục hồi bộ nhớ sớm nhất có thể.

Chú ý: Bạn nên sử dụng recycle() chỉ khi bạn chắc chắn rằng các bitmap không còn được sử dụng. Nếu bạn gọi recycle() và sau đó cố gắng để vẽ bitmap (Draw bitmap), bạn sẽ nhận được thông báo lỗi: “Canvas: trying to use a recycled bitmap

Đoạn mã dưới đây cho một ví dụ về cách gọi recycle(). Nó sử dụng các cờ (trong các biến mDisplayRefCount và mCacheRefCount) để kiểm tra xem một bitmap hiện đang được hiển thị hay đang trong bộ nhớ cache. Gọi recycle() khi các điều kiện phù hợp được xem xét:

  • 2 biến đếm mDisplayRefCount và mCacheRefCount phải là 0

  • Bitmap không được null và chưa từng được giải phóng (recycle)

private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
    synchronized (this) {
        if (isDisplayed) {
            mDisplayRefCount++;
            mHasBeenDisplayed = true;
        } else {
            mDisplayRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
public void setIsCached(boolean isCached) {
    synchronized (this) {
        if (isCached) {
            mCacheRefCount++;
        } else {
            mCacheRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

private synchronized void checkState() {
    // If the drawable cache and display ref counts = 0, and this drawable
    // has been displayed, then recycle.
    if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
            && hasValidBitmap()) {
        getBitmap().recycle();
    }
}

private synchronized boolean hasValidBitmap() {
    Bitmap bitmap = getBitmap();
    return bitmap != null && !bitmap.isRecycled();
}

Quản lý bộ nhớ trên Android 3.0 và cao hơn

Trên Android 3.0 (API 11) giới thiệu biến BitmapFactory.Options.inBitmap . Nếu tùy chọn này được thiết lập, các phương thức giải mã (Decode bitmap) có những tùy chọn (Options bitmap) sẽ cố gắng để tái sử dụng một bitmap khi tải nội dung. Điều này có nghĩa là bộ nhớ của bitmap được tái sử dụng, giúp nâng cao hiệu suất, và loại bỏ cả việc cấp phát bộ nhớ và huỷ bỏ cấp pháp bộ nhớ.

Tuy nhiên, có những hạn chế nhất định với cách inBitmap có thể được sử dụng. Đặc biệt, trước khi Android 4.4 (API level 19), chỉ có các bitmap có kích thước bằng nhau mới được hỗ trợ. Để biết chi tiết, xem tài liệu inBitmap tại đây.

Lưu lại Bitmap để tái sử dụng

Đoạn mã sau minh họa cách một bitmap được lưu trữ để sử dụng sau. Khi một ứng dụng đang chạy trên Android 3.0 hoặc cao hơn và khi một bitmap được đẩy ra khỏi LruCache, một tham chiếu mềm (soft references) đến bitmap được đặt trong một HashSet, có thể tái sử dụng về sau với inBitmap:

private Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;

// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
    mReusableBitmaps =
            Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {

    // Notify the removed entry that is no longer being cached.
    @Override
    protected void entryRemoved(boolean evicted, String key,
            BitmapDrawable oldValue, BitmapDrawable newValue) {
        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
            // The removed entry is a recycling drawable, so notify it
            // that it has been removed from the memory cache.
            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {
            // The removed entry is a standard BitmapDrawable.
            if (Utils.hasHoneycomb()) {
                // We're running on Honeycomb or later, so add the bitmap
                // to a SoftReference set for possible use with inBitmap later.
                mReusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}

Sử dụng Bitmap đã tồn tại

Trong các ứng dụng đang chạy, các phương thức giải mã (decode) kiểm tra để xem nếu bitmap đã tồn tại và hiện có thể sử dụng. Ví dụ:

public static Bitmap decodeSampledBitmapFromFile(String filename,
      int reqWidth, int reqHeight, ImageCache cache) {

    final BitmapFactory.Options options = new BitmapFactory.Options();
    ...
    BitmapFactory.decodeFile(filename, options);
    ...

    // If we're running on Honeycomb or newer, try to use inBitmap.
    if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options, cache);
    }
    ...
    return BitmapFactory.decodeFile(filename, options);
}

Đoạn tiếp theo cho thấy phương thức addInBitmapOptions () được gọi trong đoạn mã trên. Nhìn nó như là lấy một Bitmap đã tồn tại để gán cho inBitmap. Lưu ý rằng phương thức này chỉ giả định đặt một giá trị cho inBitmap nếu nó tìm thấy Bitmap phù hợp (code của bạn không nên giả định rằng một Bitmap sẽ được tìm thấy):

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // If a suitable bitmap has been found, set it as the value of
            // inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        Bitmap bitmap = null;

    if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
        synchronized (mReusableBitmaps) {
            final Iterator<SoftReference<Bitmap>> iterator
                    = mReusableBitmaps.iterator();
            Bitmap item;

            while (iterator.hasNext()) {
                item = iterator.next().get();

                if (null != item && item.isMutable()) {
                    // Check to see it the item can be used for inBitmap.
                    if (canUseForInBitmap(item, options)) {
                        bitmap = item;

                        // Remove from reusable set so it can't be used again.
                        iterator.remove();
                        break;
                    }
                } else {
                    // Remove from the set if the reference has been cleared.
                    iterator.remove();
                }
            }
        }
    }
    return bitmap;
}

Cuối cùng, phương thức này sẽ xác định liệu một bitmap có đáp ứng được các tiêu chí kích thước được sử dụng cho inBitmap:


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use if the byte size of
        // the new bitmap is smaller than the reusable bitmap candidate
        // allocation byte count.
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}

    // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
}

/**
 * A helper function to return the byte usage per pixel of a bitmap based on its configuration.
 */
static int getBytesPerPixel(Config config) {
if (config == Config.ARGB_8888) {
    return 4;
} else if (config == Config.RGB_565) {
    return 2;
} else if (config == Config.ARGB_4444) {
    return 2;
} else if (config == Config.ALPHA_8) {
    return 1;
}
    return 1;
}

Hiển thị Bitmap trong ứng dụng của bạn

Bài học này tập hợp tất cả mọi thứ từ những bài học trước đó, cho thấy bạn làm thế nào để tải nhiều ảnh bitmap vào ViewPager và các thành phần GridView sử dụng background thread và bộ nhớ cache bitmap.

Tải Bimap khi triển khai trong 1 ViewPager

Các mô hình kéo vuốt (swipe) như là một cách tuyệt vời để điều hướng các đến chi tiết của hình ảnh trong một thư viện hình ảnh (Gallery). Bạn có thể triển khai mô hình này với ViewPager cùng với vợi PagerAdapter.Tuy nhiên, một adapter phù hợp hơn trong trường hợp này là FragmentStatePagerAdapter vì nó có thể tự động phá hủy và lưu trạng thái của các Fragment trong ViewPager khi nó ẩn khỏi màn hình, làm giảm sử dụng bộ nhớ xuống.

Lưu ý: Nếu bạn có một số lượng nhỏ của hình ảnh và tin tưởng tất cả chúng đều nằm trong các giới hạn bộ nhớ ứng dụng, sau đó nên sử dụng một PagerAdapter hoặc FragmentPagerAdapter có thể thích hợp hơn.

Dưới đây thực hiện một ViewPager với các item là ImageView. Lớp Main activity giữ ViewPager và các Adapter:

public class ImageDetailActivity extends FragmentActivity {
public static final String EXTRA_IMAGE ="extra_image";
private ImagePagerAdapter mAdapter;
private ViewPager mPager;

// A static dataset to back the ViewPager adapter
public final static Integer[] imageResIds =newInteger[]{
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};@Overridepublicvoid onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);
        setContentView(R.layout.image_detail_pager);// Contains just a ViewPager

        mAdapter =newImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
        mPager =(ViewPager) findViewById(R.id.pager);
        mPager.setAdapter(mAdapter)
    ;}

public static classImagePagerAdapter extends FragmentStatePagerAdapter {
private final int mSize;
public ImagePagerAdapter(FragmentManager fm,int size)
{
    super(fm);
    mSize = size;
}

@Override
public int getCount(){
    return mSize;
}

@Override
public Fragment getItem(int position)
{
    return ImageDetailFragment.newInstance(position);
}
}
}

Dưới đây là thực hiện triển khai mà hình để hiển thị ImageView. Điều này có vẻ giống như một cách tiếp cận hoàn toàn hợp lý, nhưng bạn có thể thấy những hạn chế của việc thực hiện điều này? Làm thế nào nó có thể được cải thiện?

public class ImageDetailFragment extends Fragment {
    private static final String IMAGE_DATA_EXTRA = "resId";
    private int mImageNum;
    private ImageView mImageView;

    static ImageDetailFragment newInstance(int imageNum) {
        final ImageDetailFragment f = new ImageDetailFragment();
        final Bundle args = new Bundle();
        args.putInt(IMAGE_DATA_EXTRA, imageNum);
        f.setArguments(args);
        return f;
    }

    // Empty constructor, required as per Fragment docs
    public ImageDetailFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // image_detail_fragment.xml contains just an ImageView
        final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
        mImageView = (ImageView) v.findViewById(R.id.imageView);
        return v;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final int resId = ImageDetailActivity.imageResIds[mImageNum];
        mImageView.setImageResource(resId); // Load image into ImageView
    }
}

Hy vọng rằng bạn nhận thấy vấn đề: những hình ảnh này đang được đọc từ các nguồn tài nguyên trên các thread UI (main’s Thread), mà có thể dẫn đến treo ứng dụng và force close.

Sử dụng một AsyncTask như trong bài trước “Hiển Thị Bitmap một cách hiệu quả” , nó đơn giản để tải và xử lý hình ảnh trong một luồng ngầm (background thread).

public class ImageDetailActivity extends FragmentActivity {
    ...

    public void loadBitmap(int resId, ImageView imageView) {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }

    ... // include BitmapWorkerTask class
}

public class ImageDetailFragment extends Fragment {
    ...

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (ImageDetailActivity.class.isInstance(getActivity())) {
            final int resId = ImageDetailActivity.imageResIds[mImageNum];
            // Call out to ImageDetailActivity to load the bitmap in a background thread
            ((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
        }
    }
}

Bất kỳ xử lý nào khác (như thay đổi kích thước hoặc hình ảnh lấy từ mạng) đều có thể diễn ra trong BitmapWorkerTask mà không ảnh hưởng đến tương tác của giao diện người dùng trong luồng chính (main UI thread). Dưới đây bổ sung thêm cache lại Bitmap để tăng tốc độ hiển thị hình ảnh như được mô tả trong bài “Caching Bitmap trong Android“.

public class ImageDetailActivity extends FragmentActivity {
    ...
    private LruCache<String, Bitmap> mMemoryCache;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        // initialize LruCache as per Use a Memory Cache section
    }

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

        final Bitmap bitmap = mMemoryCache.get(imageKey);
        if (bitmap != null) {
            mImageView.setImageBitmap(bitmap);
        } else {
            mImageView.setImageResource(R.drawable.image_placeholder);
            BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
            task.execute(resId);
        }
    }

    ... // include updated BitmapWorkerTask from Use a Memory Cache section
}

Tải Bitmap khi triển khai trong thành phần GridView

Xây dựng danh sách lưới (grid) rất hữu ích cho hiển thị các tập dữ liệu hình ảnh và có thể được thực hiện bằng cách sử dụng một thành phần GridView trong đó có nhiều hình ảnh có thể được trên màn hình cùng một lúc và nhiều nhu cầu hơn là sẵn sàng để xuất hiện nếu người dùng cuộn lên hoặc xuống.

Khi thực hiện các loại điều khiển này, bạn phải đảm bảo các giao diện người dùng phải xuyên suốt, sử dụng bộ nhớ vẫn còn dưới sự kiểm soát và đồng thời được xử lý một cách chính xác (do cách GridView tái chế (recycle) các con của nó).

Để bắt đầu, đây là một thực hiện GridView tiêu chuẩn với các mục là ImageView đặt bên trong một Fragment. Một lần nữa, điều này có vẻ như một cách tiếp cận hoàn toàn hợp lý, nhưng làm cách nào giúp cho chúng tốt hơn?

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
private ImageAdapter mAdapter;
// A static dataset to back the GridView adapter
public final static Integer[] imageResIds = new Integer[] {
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};

    // Empty constructor as per Fragment docs
public ImageGridFragment() {}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
        mAdapter = new ImageAdapter(getActivity());
}

@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
        mGridView.setAdapter(mAdapter);
        mGridView.setOnItemClickListener(this);
return v;
}

@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
        i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
        startActivity(i);
}

private class ImageAdapter extends BaseAdapter {
private final Context mContext;

public ImageAdapter(Context context) {
super();
            mContext = context;
}

@Override
public int getCount() {
return imageResIds.length;
}

@Override
public Object getItem(int position) {
return imageResIds[position];
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup container) {
ImageView imageView;
if (convertView == null) { // if it's not recycled, initialize some attributes
                imageView = new ImageView(mContext);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
                imageView.setLayoutParams(new GridView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
} else {
                imageView = (ImageView) convertView;
}
imageView.setImageResource(imageResIds[position]); // Load image into ImageView
return imageView;
}
}
}

Một lần nữa, vấn đề với việc thực hiện này là hình ảnh đang được đặt trong thread UI. Trong khi điều này có thể làm việc với các hình ảnh nhỏ hơn (thumb – do tải tài nguyên hệ thống và bộ nhớ đệm). Việc xử lý và bộ nhớ đệm không đồng bộ (asynchronize) tương tự từ các phần trước có thể được thực hiện ở đây. Tuy nhiên, bạn cũng cần phải cảnh giác với vấn đề đồng thời là GridView tái chế các con của nó.

Dưới đây triển khai bổ sung điều này:

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    ...

    private class ImageAdapter extends BaseAdapter {
        ...

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ...
            loadBitmap(imageResIds[position], imageView)
            return imageView;
        }
    }

    public void loadBitmap(int resId, ImageView imageView) {
        if (cancelPotentialWork(resId, imageView)) {
            final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
            final AsyncDrawable asyncDrawable =
                    new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
            imageView.setImageDrawable(asyncDrawable);
            task.execute(resId);
        }
    }

    static class AsyncDrawable extends BitmapDrawable {
        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

        public AsyncDrawable(Resources res, Bitmap bitmap,
                BitmapWorkerTask bitmapWorkerTask) {
            super(res, bitmap);
            bitmapWorkerTaskReference =
                new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
        }

        public BitmapWorkerTask getBitmapWorkerTask() {
            return bitmapWorkerTaskReference.get();
        }
    }

    public static boolean cancelPotentialWork(int data, ImageView imageView) {
        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

        if (bitmapWorkerTask != null) {
            final int bitmapData = bitmapWorkerTask.data;
            if (bitmapData != data) {
                // Cancel previous task
                bitmapWorkerTask.cancel(true);
            } else {
                // The same work is already in progress
                return false;
            }
        }
        // No task associated with the ImageView, or an existing task was cancelled
        return true;
    }

    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
       if (imageView != null) {
           final Drawable drawable = imageView.getDrawable();
           if (drawable instanceof AsyncDrawable) {
               final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
               return asyncDrawable.getBitmapWorkerTask();
           }
        }
        return null;
    }

    ... // include updated BitmapWorkerTask class

Chú ý: Cùng mã này có thể dễ dàng được điều chỉnh để làm việc với ListView.

Việc thực hiện này cho phép sự linh hoạt trong cách thức những hình ảnh được xử lý và nạp mà không cản trở sự mượt mà của giao diện người dùng. Trong các tác vụ chạy nền (background task), bạn có thể tải hình ảnh từ mạng hoặc thay đổi kích thước hình ảnh từ máy ảnh kỹ thuật số lớn .

Hãy xem đầy đủ mã nguồn Tại Đây.


All Rights Reserved