Android幀動畫的持續優化

Android 系統提供了兩種幀動畫實現方式

1.xml 文件定義 animation-list
2.java 文件設置 AnimationDrawable

# [缺點] 
- 系統會把每一幀圖片讀取到內存中
- 當圖片不少且每張都很大的狀況下,容易出現卡頓,甚至 OOM
複製代碼

解決問題的關鍵在於避免一次性讀取全部圖片java

[方案] 在每一幀繪製以前,才加載圖片到內存中,而且釋放前一幀圖片的資源
複製代碼

優化點

  • 因爲圖片存儲在 res / assets 資源目錄或者 sd 卡中,須要經過子線程讀取圖片避免ANR
  • BitmapFactory 加載圖片經過 Options 配置參數優化
    • inPreferredConfig 設置顏色模式,不帶透明度的 RGB_565 內存只有默認的 ARGB_8888 的一半
    • inSampleSize 根據顯示控件的大小對圖像採樣,返回較小的圖像以節省內存

經過以上處理,能夠實現幀動畫的流暢播放android

內存問題解決了?

經過 Android Profiler,看到頻繁的 IO 操做(每讀取一張圖片的同時釋放一張圖片)致使內存劇烈抖動。內存頻繁的分配和回收容易產生內存碎片,存在 OOM 的風險,頻繁的 GC 也容易致使UI卡頓。緩存

  • 經過 Options 參數繼續優化
    • inMutable 設置解碼獲得的 bitmap 可變
    • inBitmap 複用前一幀圖片,避免內存抖動(效果以下圖)

暫時處理了內存問題後繼續思考,頻繁的 IO 也會致使 CPU 的使用率高bash

  1. 當前 App 對幀動畫的使用場景多,使用頻率高
  2. 存儲在 sd 卡的圖片作了加密,解碼每一幀圖片前須要解密,對 CPU 的考驗又加重了
  • 對於單次播放的幀動畫,每一幀圖片使用以後及時複用或者回收是合理的
  • 對於不限次數循環播放的幀動畫,假如 1 秒播放 25 幀,那麼每 40 毫秒須要解碼 1 幀,觸發 1 次 IO,若是同一頁面有多個幀動畫同時播放,那麼狀況更加糟糕

在某華爲榮耀9手機上,測試簡單頁面播放 sd 卡里某一幀動畫循環播放的 CPU 狀況ide

很容易想到緩存 —— 這時候又回到了「之內存空間換取cpu時間」的思路

App 中循環或屢次播放的幀動畫大部分狀況是局部的小圖(什麼地方須要無限播放全屏的幀動畫呢?),對這類小圖添加緩存就挺合適的。優化效果以下圖: 測試

另外: 什麼業務場景須要幀動畫無限循環播放呢?用戶會盯着手機上的某個動畫多長時間?是否能夠針對大部分狀況,設置一個上限,播放 n 次以後就中止動畫,只保留最後一幀的畫面優化

緩存的實現

Android 提供了 LruCache,根據最近最少使用優先清理的原則緩存數據。動畫

public class FrameAnimationCache extends LruCache<String, Bitmap> {
    private static int mCacheSize = (int) (Runtime.getRuntime().maxMemory() / 8);
    
    public FrameAnimationCache() {
	    super(mCacheSize);
    }

    @Override
    protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
        return value.getByteCount();
    }
}
複製代碼

對於內存使用不太緊張的 App, 這樣一個緩存就夠用了,圖片緩存最多隻會佔用 mCacheSize 大小的內存。ui

進一步優化

當緩存裏的幀動畫圖片長時間沒有使用,如何釋放?this

SoftReference(軟引用)若是內存空間足夠,垃圾回收器就不會回收它,若是內存空間不足,就會回收這些對象的內存(系統自動幫你回收,不用操心多好)

public class FrameAnimationCache extends LruCache<String, SoftReference<Bitmap>> {
    private static int mCacheSize = (int) (Runtime.getRuntime().maxMemory() / 8);
    
    public FrameAnimationCache() {
	    super(mCacheSize);
    }

    @Override
    protected int sizeOf(@NonNull String key, @NonNull SoftReference<Bitmap> value) {
        if (value.get() != null) {
            return value.get().getByteCount();
        } else {
            return 0;
        }
    }
}
複製代碼

大功告成??

當 GC 自動回收 SoftReference,會致使緩存的 sizeOf 計算出錯,日誌裏可能看到這樣的警告

W/System.err: java.lang.IllegalStateException: xxx.xxxAnimationCache.sizeOf() is reporting inconsistent results!
W/System.err: at android.support.v4.util.LruCache.trimToSize(LruCache.java:167)
W/System.err: at android.support.v4.util.LruCache.put(LruCache.java:150)
複製代碼

假如咱們經過 get(K key) 獲取的以前已緩存過的 Bitmap 軟引用,而剛好它已被 GC 回收,那麼返回 null,須要從新解碼圖片,調用 put(K key, V value) 緩存起來。

public final V put(@NonNull K key, @NonNull V value) {
    if (key != null && value != null) {
        Object previous;
        synchronized(this) {
            ++this.putCount;
            this.size += this.safeSizeOf(key, value);
            previous = this.map.put(key, value);
            if (previous != null) {
                this.size -= this.safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            this.entryRemoved(false, key, previous, value);
        }

        this.trimToSize(this.maxSize);
        return previous;
    } else {
        throw new NullPointerException("key == null || value == null");
    }
}
複製代碼
  • 查看 LruCache 的源碼可知,每一個 put 操做,對於 key 相同的狀況(咱們恰好如此),會對 size 減去 previous(前一個緩存數據)的大小, 而由於數據被回收,致使 previous 爲 null,此時 size 大於實際緩存數據的大小
  • 若相似狀況長期發生下去,最終可能出現 size 達到 maxSize,而實際上全部緩存數據都被回收
public void trimToSize(int maxSize) {
    while(true) {
        Object key;
        Object value;
        synchronized(this) {
            if (this.size < 0 || this.map.isEmpty() && this.size != 0) {
                throw new IllegalStateException(this.getClass().getName() + ".sizeOf() is reporting inconsistent results!");
            }

            if (this.size <= maxSize || this.map.isEmpty()) {
                return;
            }

            Entry<K, V> toEvict = (Entry)this.map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            this.map.remove(key);
            this.size -= this.safeSizeOf(key, value);
            ++this.evictionCount;
        }

        this.entryRemoved(true, key, value, (Object)null);
    }
}
複製代碼
  • 並且每一個 put 會執行 trimToSize
  • 當 size > maxSize 的狀況, 會將緩存隊列的數據逐個remove,而後修改 size
  • 惋惜數據被回收 safeSizeOf 獲得的大小爲 0,至關於沒有修改 size
  • 一直 remove,直到隊列爲空
  • 符合 map.isEmpty() && size != 0,拋出日誌打印的警告

如何處理

問題已知 —— 數據回收致使大小計算出錯,那麼解決這個問題就能夠了。

ReferenceQueue

  • 當 GC 回收了 SoftReference,會通知與其綁定的 ReferenceQueue 隊列,可經過這個方式檢測到內存回收,主動正確修改緩存數據 size

而我用了以下方式

public class FrameAnimationCache extends LruCache<String, SizeSoftReferenceBitmap> {
    private static int mCacheSize = (int) (Runtime.getRuntime().maxMemory() / 8);

    public FrameAnimationCache() {
    	super(mCacheSize);
    }

    @Override
    protected int sizeOf(@NonNull String key, @NonNull SizeSoftReferenceBitmap value) {
    	return value.getSize();
    }
}

private class SizeSoftReferenceBitmap {
    private SoftReference<Bitmap> mBitmap;
    private int mSize;
    
    private SizeSoftReferenceBitmap(SoftReference<Bitmap> bitmap, int size) {
    	mBitmap = bitmap;
    	mSize = size;
    }
    
    private int getSize() {
    	return mSize;
    }
    
    private SoftReference<Bitmap> getBitmap() {
    	return mBitmap;
    }
}

public Bitmap getBitmapFromCache(String key) {
    SizeSoftReferenceBitmap value = mFrameAnimationCache.get(key);
    return value != null && value.getBitmap() != null ? value.getBitmap().get() : null;
}

public void addBitmapToCache(String key, Bitmap value) {
    mFrameAnimationCache.put(key, new SizeSoftReferenceBitmap(new SoftReference<>(value), value.getByteCount()));
}
複製代碼

用一個 SizeSoftReferenceBitmap 類,作了簡單的對象組合,在建立緩存的時候提早存下 size。

更多

  • 目前只對循環播放的幀動畫小圖作自動緩存,小圖的判斷依據?
  • 對屢次播放的幀動畫緩存依賴於業務判斷,自行調用緩存,是否可添加計數器,自動判斷播放必定次數後添加緩存,計數器多佔了內存?
相關文章
相關標籤/搜索