Android system provides two kinds of frame animation implementation

XML file defines animation-list. Java file sets AnimationDrawable# [disadvantage]- The system will read every frame into memory - when there are many images and each image is very large, it is easy to get stuck or even OOMCopy the code

The key is to avoid reading all the images at once

[Solution] Before each frame is drawn, the image is loaded into memory and the resources of the previous frame are freedCopy the code

Optimal point

  • Since the images are stored in the RES/Assets directory or sd card, the images need to be read by child threads to avoid ANR
  • BitmapFactory loads images optimized with Options configuration parameters
    • inPreferredConfigWith color mode set, RGB_565 without transparency has half the memory of the default ARGB_8888
    • inSampleSizeSample the image according to the size of the display control, returning a smaller image to save memory

Through the above processing, you can achieve smooth playback of the frame animation

Memory problem solved?

Through the Android Profiler, you can see that the frequent I/O operations (freeing an image for every image read) cause the memory to shake violently. Frequent memory allocation and reclamation may lead to memory fragmentation and OOM risk, and frequent GC may cause UI lag.

  • Continue optimization with the Options parameter
    • inMutableSet the bitmap obtained by decoding to be variable
    • inBitmapReuse the previous frame to avoid memory jitter (the effect is shown below)

After dealing with memory problems for a while, frequent I/OS can also lead to high CPU usage

  1. Currently, App uses many scenes for frame animation with high frequency
  2. The images stored on the SD card are encrypted, and each frame needs to be decrypted before it can be decoded, adding to the CPU’s challenge
  • For single-play frame animation, it is reasonable to reuse or recycle each frame after use
  • For an unlimited loop of 25 frames per second, a frame needs to be decoded every 40 milliseconds to trigger an IO, and this is even worse if multiple frames are played on the same page

On a Huawei Honor 9 mobile phone, the CPU performance of a simple page playing a frame of animation on the SD card was tested

It’s easy to think of caches — which brings us back to the idea of “trading memory space for CPU time.

Frame animations that loop or play multiple times in an App are mostly small local images (where do you need to play full screen frame animations indefinitely?). Adding a cache to these small graphs is appropriate. The optimization effect is shown as follows:

Also: what business scenario requires an infinite loop of frame animation? How long does a user stare at an animation on their phone? Is it possible to set an upper limit for most cases and stop the animation after n plays, keeping only the last frame

Implementation of caching

Android provides LruCache, which caches data based on least recently used first cleanup.

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) {
        returnvalue.getByteCount(); }}Copy the code

For less memory intensive apps, a single cache is sufficient, and the image cache will only take up mCacheSize size of memory at most.

Further optimization

How to release a frame animation image that has not been used for a long time?

If there is enough memory, the garbage collector will not reclaim the object, and if there is not enough memory, the garbage collector will reclaim the object.

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 {
            return0; }}}Copy the code

Done??

If the GC automatically retrieves SoftReference, the sizeOf the cache is incorrect, and such a warning may be displayed in the log

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)
Copy the code

If we get a Bitmap soft reference that has been cached by get(K key) and it happens to have been collected by GC, then null is returned and the image needs to be decoded again. Put (K key, V value) is used for caching.

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"); }}Copy the code
  • Each PUT operation, in the case of the same key (which we did), subtracts the size of the previous cached data, which is null because the data is being retrieved. In this case, size is larger than the actual size of cached data
  • If a similar situation occurs for a long time, the size may eventually reach maxSize, and virtually all cached data will be reclaimed
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); }}Copy the code
  • And each put does trimToSize
  • If size > maxSize, remove the cache queue data one by one, and then change the size
  • Unfortunately, the size of safeSizeOf is 0, which means that the size is not changed
  • Remove until the queue is empty
  • Conforms to map.isempty () && size! = 0, throws a log print warning

How to deal with

The problem is known — data reclamation is causing the size calculation to go wrong, so you can fix the problem.

ReferenceQueue

  • When GC retrieves SoftReference, it notifies the ReferenceQueue queue bound to it. In this way, memory reclamation can be detected and the cached data size can be changed correctly

And I did it the following way

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);
    returnvalue ! = 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())); }Copy the code

Use a SizeSoftReferenceBitmap class to make simple object combinations and store the size in advance when creating the cache.

More and more

  • At present, only the loop plays the frame animation minigraph to do automatic cache, the judgment of the minigraph?
  • For the frame animation cache which is played for many times depends on the business judgment, it calls the cache by itself. Can we add the counter, and add the cache after automatically judging the play for a certain number of times, because the counter occupies too much memory?