Android GifImageView loading Gif pictures and its principle

background

I saw an interesting moving picture a few days ago. I originally wanted to download it and send it to my friends, but when I sent it on wechat, I prompted that the file was too large. I saw that it was 41M, so I was thinking about how to load such a large gif. So I made a demo to try.

Glide

As we all know, Glide supports loading gif images, so use Glide first. Put the graph into raw and load it with Glide.

Glide.with(this).load(R.raw.aa).into(gifImageView);

After waiting for a long time without any response, I saw that the log was printing all the time:

Background young concurrent copying GC freed 3021(205KB) AllocSpace objects, 47(22MB) LOS objects, 29% free, 51MB/73MB, paused 72us total 120.652ms

Run with Profile and the effect is as follows:

Good guy, it seems that the GC is triggered all the time during the loading process, which makes it impossible to load. So try to apply for a larger memory, android:largeHeap = "true".

It can be loaded this time, but it took about ten seconds. The speed is really slow. Moreover, the memory before loading is 59M. After loading, the memory directly soared to 273M,


This certainly won't work. The speed is slow, and it also eats memory, and the added areas are heap areas, which shows that glide is a decoding work done at the java level. So I searched github for gif and found one android-gif-drawable Library, 8.6K star. Then I used it. It's much easier to use, and it also supports the pause, playback, reset and other functions of gif. 40M gif can be turned on in seconds. Let's see how to use it.

android-gif-drawable

Import:

implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.19'

It is the same as normal ImageView:

<pl.droidsonroids.gif.GifImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/src_anim"
    android:background="@drawable/bg_anim"
    />

It can automatically identify whether the setting is a gif image. If it is an ordinary image, the effect is the same as setting ImageView or ImageButton. You can also set it directly in java:

gifImageView.setImageResource(int resId);
gifImageView.setBackgroundResource(int resId);
//Set GifDrawable
gifImageView.setImageDrawable(GifDrawable gifDrawable);

GifDrawable can be built directly from a variety of sources:

//asset file
GifDrawable gifFromAssets = new GifDrawable( getAssets(), "anim.gif" );
		
//resource (drawable or raw)
GifDrawable gifFromResource = new GifDrawable( getResources(), R.drawable.anim );
		
//Uri
ContentResolver contentResolver = ... //can be null for file:// Uris
GifDrawable gifFromUri = new GifDrawable( contentResolver, gifUri );

//byte array
byte[] rawGifBytes = ...
GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );
		
//FileDescriptor
FileDescriptor fd = new RandomAccessFile( "/path/anim.gif", "r" ).getFD();
GifDrawable gifFromFd = new GifDrawable( fd );
		
//file path
GifDrawable gifFromPath = new GifDrawable( "/path/anim.gif" );
		
//file
File gifFile = new File(getFilesDir(),"anim.gif");
GifDrawable gifFromFile = new GifDrawable(gifFile);
		
//AssetFileDescriptor
AssetFileDescriptor afd = getAssets().openFd( "anim.gif" );
GifDrawable gifFromAfd = new GifDrawable( afd );
				
//InputStream (it must support marking)
InputStream sourceIs = ...
BufferedInputStream bis = new BufferedInputStream( sourceIs, GIF_LENGTH );
GifDrawable gifFromStream = new GifDrawable( bis );
		
//direct ByteBuffer
ByteBuffer rawGifBytes = ...
GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );

InputStreams will automatically close when GifDrawable is no longer used. Through GifDrawable, you can pause and reset gif:

gifDrawable.start(); //Start playing
gifDrawable.stop(); //stop playing
gifDrawable.reset(); //Reset and restart playback
gifDrawable.isRunning(); //Is it playing
gifDrawable.setSpeed(float factor) ;//Set the playback speed, for example, 2.0f plays at twice the speed
gifDrawable.seekTo(int position); //Skip to the specified playback position
gifDrawable.getCurrentPosition() ; //Gets the time elapsed between now and the start of playback
gifDrawable.getDuration() ; //Get the time required to play once
gifDrawable.recycle();//Free memory*/

Simply demonstrate with code:

public class GifActivity extends AppCompatActivity {

    @BindView(R.id.iv_gif)
    ImageView imageView;
    @BindView(R.id.pl_gif)
    GifImageView gifImageView;
    GifDrawable gifDrawable = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_gif);
        ButterKnife.bind(this);
    }
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.load:
                startLoadGif();
                break;
            case R.id.pause:
                pauseGif();
                break;
            case R.id.play:
                playGif();
                break;
            case R.id.reset:
                resetGif();
                break;
        }
    }
	//Reset
    private void resetGif() {
        gifDrawable.reset();
    }
	//play
    private void playGif() {
        gifDrawable.start();
    }
	//suspend
    private void pauseGif() {
        gifDrawable.pause();
    }
	//load
    private void startLoadGif() {
        //Glide.with(this).load(R.raw.aa).into(gifImageView);
        try {
            gifDrawable = new GifDrawable(getResources(),R.raw.aa);
        } catch (IOException e) {
            e.printStackTrace();
        }
        gifImageView.setImageDrawable(gifDrawable);
    }
}


The layout will not be posted. Take a look at the memory after using Android GIF drawable:

The memory has only increased a little, and the memory in the decoding process has not soared to a high level. I think this frame is really powerful.

Let's take a look at how it is implemented.
First look at GifDrawable:

new GifDrawable(getResources(),R.raw.aa);
	/**
	 * Creates drawable from resource.
	 *
	 * @param res Resources to read from
	 * @param id  resource id (raw or drawable)
	 * @throws NotFoundException    if the given ID does not exist.
	 * @throws IOException          when opening failed
	 * @throws NullPointerException if res is null
	 */
	public GifDrawable(@NonNull Resources res, @RawRes @DrawableRes int id) throws NotFoundException, IOException {
		this(res.openRawResourceFd(id));
		final float densityScale = GifViewUtils.getDensityScale(res, id);
		mScaledHeight = (int) (mNativeInfoHandle.getHeight() * densityScale);
		mScaledWidth = (int) (mNativeInfoHandle.getWidth() * densityScale);
	}

You can see that there is a set width and height. Look at its overload method:

public GifDrawable(@NonNull AssetFileDescriptor afd) throws IOException {
	this(new GifInfoHandle(afd), null, null, true);
}

The passed in is AssetFileDescriptor, which is used to read resources under raw. Then new a GifInfoHandle.

GifDrawable(GifInfoHandle gifInfoHandle, final GifDrawable oldDrawable, ScheduledThreadPoolExecutor executor, boolean isRenderingTriggeredOnDraw) {
		mIsRenderingTriggeredOnDraw = isRenderingTriggeredOnDraw;
		mExecutor = executor != null ? executor : GifRenderingExecutor.getInstance();
		//Mnateinfohandle is the GifInfoHandle of new just now
		mNativeInfoHandle = gifInfoHandle;
		Bitmap oldBitmap = null;
		if (oldDrawable != null) {
			synchronized (oldDrawable.mNativeInfoHandle) {
				if (!oldDrawable.mNativeInfoHandle.isRecycled()
						&& oldDrawable.mNativeInfoHandle.getHeight() >= mNativeInfoHandle.getHeight()
						&& oldDrawable.mNativeInfoHandle.getWidth() >= mNativeInfoHandle.getWidth()) {
					oldDrawable.shutdown();
					oldBitmap = oldDrawable.mBuffer;
					oldBitmap.eraseColor(Color.TRANSPARENT);
				}
			}
		}
		//Initialize bitmap
		if (oldBitmap == null) {
			mBuffer = Bitmap.createBitmap(mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight(), Bitmap.Config.ARGB_8888);
		} else {
			mBuffer = oldBitmap;
		}
		mBuffer.setHasAlpha(!gifInfoHandle.isOpaque());
		mSrcRect = new Rect(0, 0, mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight());
		mInvalidationHandler = new InvalidationHandler(this);
		//
		mRenderTask.doWork();
		//Set width and height
		mScaledWidth = mNativeInfoHandle.getWidth();
		mScaledHeight = mNativeInfoHandle.getHeight();
	}

mBuffer is a Bitmap:

	/**
	 * Frame buffer, holds current frame.
	 */
	final Bitmap mBuffer;

RenderTask is a Runnable. Let's see what doWork does first:

class RenderTask extends SafeRunnable {

	RenderTask(GifDrawable gifDrawable) {
		super(gifDrawable);
	}

	@Override
	public void doWork() {
		//critical code
		final long invalidationDelay = mGifDrawable.mNativeInfoHandle.renderFrame(mGifDrawable.mBuffer);
		if (invalidationDelay >= 0) {
			mGifDrawable.mNextFrameRenderTime = SystemClock.uptimeMillis() + invalidationDelay;
			if (mGifDrawable.isVisible() && mGifDrawable.mIsRunning && !mGifDrawable.mIsRenderingTriggeredOnDraw) {
				mGifDrawable.mExecutor.remove(this);
				mGifDrawable.mRenderTaskSchedule = mGifDrawable.mExecutor.schedule(this, invalidationDelay, TimeUnit.MILLISECONDS);
			}
			if (!mGifDrawable.mListeners.isEmpty() && mGifDrawable.getCurrentFrameIndex() == mGifDrawable.mNativeInfoHandle.getNumberOfFrames() - 1) {
				mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(mGifDrawable.getCurrentLoop(), mGifDrawable.mNextFrameRenderTime);
			}
		} else {
			mGifDrawable.mNextFrameRenderTime = Long.MIN_VALUE;
			mGifDrawable.mIsRunning = false;
		}
		if (mGifDrawable.isVisible() && !mGifDrawable.mInvalidationHandler.hasMessages(MSG_TYPE_INVALIDATION)) {
			mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0);
		}
	}
}

You can see in doWork by calling gifdrawable The renderFrame method of mnateinfohandle, and a bitmap is passed in. The name should mean decoding a frame. Next, follow the renderFrame.

	synchronized long renderFrame(Bitmap frameBuffer) {
		return renderFrame(gifInfoPtr, frameBuffer);
	}
	//Enter jni method
	private static native long renderFrame(long gifFileInPtr, Bitmap frameBuffer);

The implementation of this method is in its bitmap In C, the gifFileInPtr passed in by renderFrame should be the address of the GifInfo generated when the gif resource is opened,

First, lock the current bitmap by calling lock pixels, which is a two-dimensional array, and then start drawing. This method has a return value. The return value of long type represents the time of the next frame.

There is an androidbitmap in lockPixels_ lockPixels method, mainly through AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr) decodes the picture and obtains the address pointer addrPtr of the decoded pixel stored in memory. By modifying the pixel of the memory space pointed to by addrPtr, it is equivalent to directly modifying the bitmap loaded into memory and calling androidbitmap_ Unlock pixels releases the locked bitmap data modified in memory and can be used to display it to the foreground.

Keep looking at getBitmap. Enter drawing In C:

Finally, the blitNormal method will be called, depending on how the incoming bm is used:

argb is a structure, and the GifColorType in it is a structure. You should understand when you see the GifColorType declaration. RGB is in it:

typedef struct {
	GifColorType rgb;
	uint8_t alpha;
} argb;

typedef struct GifColorType {
	uint8_t Red, Green, Blue;
} GifColorType;

blitNormal is the process of parsing gif and drawing bitmap. Go back to doWork() in RenderTask. At this time, the bitmap has been drawn, and then call:

mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0);
class InvalidationHandler extends Handler {

	static final int MSG_TYPE_INVALIDATION = -1;

	private final WeakReference<GifDrawable> mDrawableRef;

	InvalidationHandler(final GifDrawable gifDrawable) {
		super(Looper.getMainLooper());
		mDrawableRef = new WeakReference<>(gifDrawable);
	}

	@Override
	public void handleMessage(@NonNull final Message msg) {
		final GifDrawable gifDrawable = mDrawableRef.get();
		if (gifDrawable == null) {
			return;
		}
		if (msg.what == MSG_TYPE_INVALIDATION) {
			//critical code
			gifDrawable.invalidateSelf();
		} else {
			for (AnimationListener listener : gifDrawable.mListeners) {
				listener.onAnimationCompleted(msg.what);
			}
		}
	}
}

Finally, call the invalideself method of GifDrawable to draw:

	@Override
	public void invalidateSelf() {
		super.invalidateSelf();
		scheduleNextRender();
	}

The next frame drawing is also realized through RenderTask. Throw RenderTask into the thread pool. When the time of the next frame comes, execute the run method of the parent class SafeRunnable of RenderTask. In the run method, call the doWork() method to form a loop to achieve the purpose of continuous playback.

summary

The android gif drawable source code is not particularly complex, and the main process is easy to sort out. I didn't look at the specific details carefully. In fact, there is also a library for parsing gif in the android source code. The path is as follows:

You can also use the library in the source code for gif loading, but the parsing process still needs to be implemented by yourself. However, you should have a certain understanding of gif coding. If you have time, I will try to implement a gif loading framework by myself. Let's stop here first today. Please point out the shortcomings.

Tags: C Android JNI gif

Posted by r3dn3ck on Fri, 29 Apr 2022 12:46:32 +0300