Cách tối ưu của Glide và Fresco khi load image

Trong android, khi làm việc với Images (bitmaps), chắc hẳn khái niệm Out of memory (OOM) trở nên quen thuộc và ám ảnh với mỗi developer. Những vấn đề mà chúng ta phải đối mặt khi load một bức ảnh vào ImageView

  • Lỗi Out of memory
  • Lỗi load ảnh chậm
  • Lỗi App not responding (ARN), scroll không mượt

May mắn thay, những thư viện mã nguồn mở GlideFresco ra đời đã giúp chúng ta giải quyết phần lớn vấn đề mắc phải nêu trên, đồng thời nâng cao hiệu suất khi làm việc với bitmap. Hãy cùng xem cách mà những thư viện này giúp chúng ta giải quyết từng vấn đề.

Out of memory error

Đây thực sự là ác mộng với mỗi lập trình viên android. Có thể bạn biết lý do là do load quá nhiều hình ảnh gây nên lỗi không đủ bộ nhớ, nhưng để fix nó một cách triệt để thì thực sự không hề đơn giản.

Glide giúp chúng ta bằng cách Downsamling, tức là giảm kích thước hình ảnh thực sự xuống nhỏ hơn (hoặc bằng) kích thước của view chứa nó. Giả sử chúng ta có 1 bức ảnh với kích thước 2000x2000, nhưng kích thước của view chứa nó chỉ có 400x400, việc của Glide là giúp chúng ta giảm kích thước bức ảnh xuống còn 400x400 và hiển thị nó trong view.

Glide.with(context).load(url).into(imageView)

Glide biết được kích thước của imageView vì nó dùng imageView như là một parameter. Glide giảm kích thước của ảnh mà không phải tải toàn bộ hình ảnh vào bộ nhớ, bằng cách này bitmap sử dụng ít bộ nhớ hơn và vấn đề OOM được giải quyết.

Slow loading

Load chậm là một vấn đề khác khi load hình ảnh vào View. Một trong những nguyên nhân chính của việc load hình chậm là do chúng ta ko thể hủy bỏ tác vụ tải xuống, decode những bitmap không cần thiết khi mà View chứa chúng không nằm trong khoảng nhìn thấy của màn hình, vì thế có rất nhiều tác vụ được thực hiện mặc dù chúng ta không cần, do đó thời gian để load những image thực sự cần thiết (trong vùng nhìn thấy) sẽ tốn nhiều hơn.

Glide sẽ lo việc này, nó sẽ hủy tất cả các task không cần thiết và chỉ tải những hình ảnh cần thiết hiển thị cho người dùng.

Glide nhận thức được vòng đời của activity, fragment. Điều này nó sẽ biết được những task download nào cần được loại bỏ.

Một cách khác là tạo bộ nhớ cache để chứa những thông tin của hình ảnh, chúng ta không cần phải decode cùng một hình ảnh nhiều lần vì đây là việc mất thời gian. Glide sẽ tạo một vùng cache với kích thước được định nghĩa để chứa bitmaps.

Có 2 loại cache:

1. Memory cache

2. Disk cache

Khi cung cấp một url của hình ảnh cần tải, Glide sẽ thực hiện các bước sau:

  1. Đầu tiên, kiểm tra hình ảnh với khóa là url đã tồn tại trong memory cache hay chưa?
  2. Nếu đã tồn tại thì chỉ việc lấy hình ảnh đã lưu trong memory cachevà hiển thị nó trên view.
  3. Nếu nó không tồn tại trong memory cache thì tiếp tục kiểm tra trong disk cache.
  4. Nếu tồn tại trong disk cache, nó sẽ load bitmap từ disk cache, đồng thời thêm nó vào memory cache, và hiển thị nó trong view.
  5. Nếu không tồn tại trong disk cache, nó sẽ tải hình ảnh từ network, thêm nó vào disk cachememory cache, đồng thời hiển thị nó trên view.

Rõ ràng việc hiển thị trực tiếp từ vùng nhớ cache sẽ nhanh hơn rất nhiều.

Unresponsive UI

Có nhiều nguyên nhân gây nên việc Unresponsive UI như thực hiện nhiều animation, dữ liệu load vào view khá lớn... Nhưng tất cả những nguyên nhân trên chỉ là bề nổi. Nguyên nhân sâu xa, quan trọng nhất của Unresponsive UI là do ứng dụng thực hiện quá nhiều tác vụ trên UI thread. Như chúng ta đã biết, các tác vụ liên quan đến việc render UI đều phải thực hiện trên UI thread. Và Android sẽ update UI sau mỗi 16ms, nếu bạn thực hiện tác vụ lâu hơn 16ms, android sẽ bỏ qua việc update và vì thế bỏ qua frame (khung hình), và tất nhiên giảm frame per second (FPS) đồng nghĩa với việc ứng dụng bị giật lag.

Nếu FPS thấp, người dùng sẽ cảm nhận được hiện tượng giật lag. Trong quá trình load bitmap, thậm chí khi chúng ta thực hiện chúng trong background thì UI vẫn giật lag, tại sao?

Nguyên nhân là khi bitmap lớn, sẽ làm cho Garbage collector (GC) chạy thường xuyên hơn. Một vài dòng logcat thường thấy trong khi ứng dụng của bạn đang chạy:

I/Choreographer: Skipped 405 frames!  The application may be doing too much work on its main thread.

hay là:

07-01 16:00:44.690: I/art(801): Explicit concurrent mark sweep GC freed 65595(3MB) AllocSpace objects, 9(4MB) LOS objects, 34% free, 38MB/58MB, paused 1.195ms total 87.219ms

(Bạn có thể tìm hiểu thêm ý nghĩa của những dòng log này tại đây)

Một nguyên tắc cơ bản :

Khi GC chạy thì ứng dụng của bạn sẽ không chạy.

GC càng tốn thời gian chạy thì nó sẽ khiến hệ thống skip càng nhiều frames. Vậy nên GC thực sự là thủ phạm chính gây giật lag.

Vậy Glide đã làm thế nào để giải quyết vấn đề này?

Câu trả lời đó là việc sử dung Bitmap Pool để làm giảm việc gọi GC nhiều nhất có thể.

Bằng việc sử dụng Bitmap Pool, nó sẽ tránh việc cấp phát (allocation) và thu hồi (deallocation) vùng nhớ của ứng dụng, làm giảm việc quá tải GC, và kết quả cuối cùng là ứng dụng chạy trơn tru, mượt mà hơn.

Vậy làm thế nào để tránh việc cấp phát và thu hồi liên tục bộ nhớ của ứng dụng?

Bằng việc sử dụng inBitmap property của bitmap (tái sử dụng bộ nhớ của bitmap)

Giả sử chúng ta load một vài hình ảnh trong ứng dụng android của mình. Chúng ta tải 2 bitmap (bitmap1, bitmap2) tuần tự. Khi load bitmap1, nó sẽ được cấp 1 vùng nhớ. Sau đó nếu chúng ta không còn dùng bitmap1, không hủy (recycle) bitmap1 (vì việc hủy liên quan đến GC được gọi). Thay vào đó, Glide sẽ sử dụng bitmap1 như là inBitmap của bitmap2. Điều này có nghĩa là vùng nhớ được cấp phát cho bitmap1 sẽ được tái sử dụng cho bitmap2.

Bitmap bitmapOne = BitmapFactory.decodeFile(filePathOne);
imageView.setImageBitmap(bitmapOne);
// we do not need image bitmapOne now and we have to set another bitmap in imageView
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePathTwo, options);
options.inMutable = true;
options.inBitmap = bitmapOne; // the key is here.
options.inJustDecodeBounds = false;
Bitmap bitmapTwo = BitmapFactory.decodeFile(filePathTwo, options);
imageView.setImageBitmap(bitmapTwo);

Chúng ta tái sử dụng vùng nhớ của bitmap1 khi tiến hành decode bitmap2. Bằng cách này, chúng ta không cho phép GC gọi liên tục vì không bỏ rơi vùng nhớ của bitmap1 mà dùng nó để load bitmap2. Một điều quan trọng đó là kích thước của bitmap1 nên bằng hoặc lớn hơn kích thước của bitmap2 để vùng nhớ của bitmap1 có thể tái sử dụng.

Bạn có thể xem bitmap pool như là một list các bitmap mà không còn cần thiết nhưng vẫn sẵn sàng được tái sử dụng để load bitmap mới có cùng kích thước.

Khi bất kì bitmap nào có thể hủy, Glide sẽ đẩy nó vào bitmap pool. Khi Glide cần để load một bitmap mới, nó chỉ lấy một bitmap mà có thể tái sử dụng để load bitmap mới này vào cùng vùng nhớ từ bitmap pool. Vậy nên không có cái nào hủy và GC sẽ không gọi.

Fresco cũng hoàn toàn tương tự, có một vài thứ khác nhưng ý tưởng thì giống nhau.

Happy Coding !

Reference links: