The designer will supply a sequence of PNG images, and the developer will display these images frame by frame to create an animated effect.
Why not use GIFs directly? There are excellent open-source libraries on GitHub for playing GIFs. The primary reason for not using them is that the project requirements necessitate this choice.
1. A seemingly straightforward approach is to place the PNG sequence provided by the designer directly into an animation list like this:
Then, simply set it as an ImageView.
So, is that all there is to it? The answer is, that it can work, but it might not, making it not the most reliable solution (yes, it's a bit confusing).
The designer provided a sequence of over 90 PNGs, which resulted in an out-of-memory (OOM) issue. Unfortunately, such an easy solution ended up being dismissed so dramatically.
2. Utilize one thread to read the PNG sequence and another thread to play the read PNG sequence. Then, we need to address some challenges:
a. With one thread reading and one thread writing, the reading thread writes the PNG sequence, and the playing thread reads it. This is a classic "Producer-Consumer Model." The question then becomes, what should be used to store the read bitmaps? The answer is to use a BlockingQueue. If you're not familiar with BlockingQueue, you can learn more about it and explore other options in Java. util.concurrent package.
b. How do we prevent out-of-memory (OOM) issues? This solution can indeed mitigate the OOM problem for the following reasons:
First, we can obtain the current maximum memory using Runtime.getRuntime().maxMemory() and the current available memory using Runtime.getRuntime().freeMemory(). By combining these values with BitmapFactory.Options and the inJustDecodeBounds attribute, we can accurately determine whether there is enough memory to load more bitmaps.
Second, maintain a currentSize variable to track the memory occupied by the bitmaps parsed into memory. For each bitmap read, add the memory occupied by the read bitmap to currentSize. Since currentSize is constantly changing, make sure to subtract the memory of the just-played bitmap from currentSize.
This diagram illustrates the entire process:
You might think we've reached the peak of optimization, but there's always room for improvement!
It's pretty clear that the consumer's consumption power is super strong, and the process for reading PNGs just isn't fast enough. This causes playback to constantly wait for a new image to be loaded and displayed. So, what's the solution?
Let's use multiple threads for reading! Sounds like a great idea, doesn't it?
Here, there may be multiple threads for reading PNGs. Once multi-threading is introduced, you'll realize that the problem becomes much more complex!
Here, you need to control the current reading progress. Since it's multi-threaded, that simple "int currentLoad" you had before can't be used anymore. Otherwise, three threads reading the same PNG might accidentally collide. So, what should you do? Use AtomicInteger. OK, it seems like you've solved this problem, and now you've ensured that all PNGs are loaded without duplication!
However, an even more troublesome issue still needs to be addressed. Keep in mind that GIFs have a playback order. But you've turned the BlockingQueue into this kind of sequence:
Is this acceptable? Obviously not. So, how can we ensure that the bitmaps inserted into the BlockingQueue are in the order of the PNG sequence?
To achieve this, it's clear that the PNG sequence number needs to be brought into the reading thread. After the reading thread is done, it asks a manager, "Hey, are there any reading threads with smaller sequence numbers that haven't submitted their bitmaps yet?" If the manager tells you there are, then you'll have to wait patiently (using the "wait" keyword), right? If the manager tells you there aren't any, then you're the one with the smallest sequence number, so go ahead and hand your bitmap over to BlockingQueue, and your mission is complete.
But the problem is, if you're waiting, who's going to wake you up? The manager says he'll notify you. When the manager receives the bitmap with the smallest sequence number (I misspoke earlier; the smallest sequence number needs to submit the bitmap to the manager), he hands the bitmap over to BlockingQueue. Then, the manager notifies all the reading thread underlings that it's time to hand in their work. At this moment, your left hand, which has been single for 10 years, finally grabs the "lock," and you submit your homework bitmap to the manager.
Is this OK now?
If you think so, then you're still TOO NAIVE!
You never would have thought that when a single thread was reading before, loading a single PNG only took about 220ms (tested using an emulator), and it was slightly faster on a real device like the Huawei Mate 8.
However, when using multi-threading to read, loading a single PNG surprisingly takes around 1100ms with four reading threads... It's really astonishing.
Too many threads? What about trying with just two? It takes around 400ms!!!
Looking back, the bottleneck is indeed in reading, but the bottleneck in reading lies in the phone's storage card... There might be other factors as well.
3. Not giving up, let's continue to think: when a single thread reads PNGs, is it possible to improve the reading efficiency?
Let's put the problem aside for a moment. If we really can't find a good solution, we should at least ensure smoothness in terms of memory usage. Let's take a look at the memory graph. It doesn't matter if you don't look, but once you do, it's astonishing:
Observant students should see the sawtooth pattern. This GC (garbage collection) is quite intense. After analyzing, we found that before finishing playing a frame, we recycled the bitmap. This results in the appearance of this kind of graph, but we can't avoid recycling, as the memory usage of the bitmap keeps increasing, and OOM (Out of Memory) is inevitable.
So, since neither releasing nor not releasing is the solution, can we reuse the bitmap that needs to be released?
What does that mean?
If the memory of the bitmap to be released can be directly used to load a new PNG, that would be great. So, is it possible? The options have a parameter that allows reusing a bitmap's memory to store another new bitmap, but with certain requirements:
For Android 4.4 and above, the old bitmap's byte size just needs to be larger than the byte size required for the new bitmap. However, for versions below 4.4, the old bitmap must have the same width and height as the new bitmap (more stringent).
Our PNG sequence has each image of the same size, so it obviously meets all these characteristics (consistent width and height).
Thus, we have a collection to store the bitmaps that are about to be released for reuse.
The saw tooth disappears decisively, and it seems that it has got an extra reward!
The loading speed has improved.
Analysis: It may be because of the reuse of bitmap memory, which saves some time when loading a new bitmap without reallocating memory.