背景
前幾天看到個有趣的動圖,本來下載下來想發給朋友看看的,但是用微信發送時候提示檔案過大,一看大小竟然是41M,好吧我說這個動圖怎么長,于是就在想這么大的gif怎么加載的,所以就搞了個demo去試試,
Glide
眾所周知Glide支持加載gif圖片,所以一開始先使用Glide,將動圖放到raw中,然后用Glide加載,
Glide.with(this).load(R.raw.aa).into(gifImageView);
然后等了半天一點反應也沒有,就看見log一直在列印:
Background young concurrent copying GC freed 3021(205KB) AllocSpace objects, 47(22MB) LOS objects, 29% free, 51MB/73MB, paused 72us total 120.652ms
用Profile跑了一下,效果如下:

好家伙,看來是在加載程序中一直觸發GC,導致無法加載成功,于是申請大一點記憶體試試,android:largeHeap=“true”,
這次倒是能加載出來了,但是大概用了十幾秒,速度是真的慢,而且加載之前記憶體是59M,加載完成后記憶體直接飆升到了273M,

這肯定不行啊,速度慢不說而且還吃記憶體,而且增加的區域都是在堆區,說明Glide是java層面做的解碼作業,后來大概看了一下,Glide決議gif是在GifDecoder中實作的,感興趣的童鞋可以看一下setPixels方法,
于是在github上搜gif相關的東西,發現了一個android-gif-drawable庫,8.6K的star,然后使用了一下確實好用的多,而且還支持gif的暫停、播放、重置等功能,40M的gif基本上可以做到秒開,下面就一起看看怎么使用的,
android-gif-drawable
匯入:
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.19'
使用就和普通的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"
/>
它可以自動識別設定的是否是gif圖片,如果是普通圖片那效果就和設定ImageView或者ImageButton一樣,也可以在java中直接設定:
gifImageView.setImageResource(int resId);
gifImageView.setBackgroundResource(int resId);
//設定GifDrawable
gifImageView.setImageDrawable(GifDrawable gifDrawable);
GifDrawable 可以直接從各種來源構建,大家應該一看就懂了,不再翻譯了直接貼過來:
//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 會自動close當GifDrawable 不再使用時,所以不需要手動去關閉輸入流了,
通過GifDrawable 可以對gif進行暫停、重置、播放等操作,非常的方便:
gifDrawable.start(); //開始播放
gifDrawable.stop(); //停止播放
gifDrawable.reset(); //復位,重新開始播放
gifDrawable.isRunning(); //是否正在播放
gifDrawable.setSpeed(float factor) ;//設定播放速度,比如2.0f以兩倍速度播放
gifDrawable.seekTo(int position); //跳到指定播放位置
gifDrawable.getCurrentPosition() ; //獲取現在到從開始播放所經歷的時間
gifDrawable.getDuration() ; //獲取播放一次所需要的時間
gifDrawable.recycle();//釋放記憶體*/
簡單用代碼演示一下吧:
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;
}
}
//重置
private void resetGif() {
gifDrawable.reset();
}
//播放
private void playGif() {
gifDrawable.start();
}
//暫停
private void pauseGif() {
gifDrawable.pause();
}
//加載
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);
}
}

布局就不貼出來了,看一下使用android-gif-drawable后記憶體情況:

記憶體僅僅增加了一點,而且解碼程序記憶體也沒有飆升到很高,感覺這框架真挺厲害,
下面就大概看看它是怎么實作的,
首先就從new 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);
}
可以看到這里面有設定寬高,再看它的多載構造方法:
public GifDrawable(@NonNull AssetFileDescriptor afd) throws IOException {
this(new GifInfoHandle(afd), null, null, true);
}
傳入的是AssetFileDescriptor,讀取raw下面資源用的,然后new了一個GifInfoHandle,
GifDrawable(GifInfoHandle gifInfoHandle, final GifDrawable oldDrawable, ScheduledThreadPoolExecutor executor, boolean isRenderingTriggeredOnDraw) {
mIsRenderingTriggeredOnDraw = isRenderingTriggeredOnDraw;
mExecutor = executor != null ? executor : GifRenderingExecutor.getInstance();
//mNativeInfoHandle就是剛才new的GifInfoHandle
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);
}
}
}
//初始化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();
//設定寬高
mScaledWidth = mNativeInfoHandle.getWidth();
mScaledHeight = mNativeInfoHandle.getHeight();
}
mBuffer 就是一個Bitmap:
/**
* Frame buffer, holds current frame.
*/
final Bitmap mBuffer;
RenderTask 是一個Runnable,它的父類SafeRunnable 繼承自Runnable,先看下doWork干了什么:
class RenderTask extends SafeRunnable {
RenderTask(GifDrawable gifDrawable) {
super(gifDrawable);
}
@Override
public void doWork() {
//關鍵代碼
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);
}
}
}
可以看到doWork中通過呼叫GifDrawable.mNativeInfoHandle的renderFrame方法,而且傳入了一個bitmap,看名字應該是解碼一幀的意思,接下來就跟蹤renderFrame,
synchronized long renderFrame(Bitmap frameBuffer) {
return renderFrame(gifInfoPtr, frameBuffer);
}
//進入jni方法中
private static native long renderFrame(long gifFileInPtr, Bitmap frameBuffer);
該方法的實作是在它的bitmap.c中,renderFrame傳入的gifFileInPtr應該是打開gif資源時生成的GifInfo的地址,

首先通過呼叫lockPixels鎖住當前的bitmap,pixels是一個二維陣列,然后開始繪制,這個方法是有回傳值的,long型別的回傳值,代表下一幀的時間,

lockPixels中有個AndroidBitmap_lockPixels方法,主要通過AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr)對圖片進行解碼并獲取解碼后像素保存在記憶體中的地址指標addrPtr,通過對addrPtr指向的記憶體空間進行像素修改,就相當于直接修改了被加載到記憶體中的位圖,調用AndroidBitmap_unlockPixels釋放鎖定,在記憶體中被修改的位圖資料就可以用于顯示到前臺,
繼續看getBitmap,就進入到drawing.c中:

最侄訓呼叫到blitNormal方法,就看傳入的bm怎么用的:

argb是一個結構體,它里面的GifColorType 又是個結構體,看到GifColorType 宣告就應該明白了,它里面就是RGB,這里實際上就是設定每個像素的顏色,當回圈跑完,一幀bitmap就繪制完成了:
typedef struct {
GifColorType rgb;
uint8_t alpha;
} argb;
typedef struct GifColorType {
uint8_t Red, Green, Blue;
} GifColorType;
blitNormal就是決議gif并且繪制bitmap的程序,再回到RenderTask的doWork()中來,此時bitmap已經繪制完成,然后呼叫:
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) {
//關鍵代碼
gifDrawable.invalidateSelf();
} else {
for (AnimationListener listener : gifDrawable.mListeners) {
listener.onAnimationCompleted(msg.what);
}
}
}
}
最終呼叫到GifDrawable的invalidateSelf方法,進行繪制:
@Override
public void invalidateSelf() {
super.invalidateSelf();
scheduleNextRender();
}
下一幀繪制也是通過RenderTask來實作,將RenderTask丟到執行緒池中,當下一幀時間到了便執行RenderTask父類SafeRunnable的run方法,run方法中又去呼叫doWork()方法,便形成了一個回圈,達到連續播放的目的,
總結
android-gif-drawable原始碼不算特別復雜,主線流程也很容易理清,具體的細節就沒有仔細去看了,其實android原始碼中也有決議gif的庫,路徑如下:

也可以使用原始碼中的庫進行gif加載,不過決議程序還是需要自己去實作,但是要對gif編碼有一定的了解,有時間的話我會嘗試自己實作一個gif加載框架,今天就先到這吧,不足之處還請各位大佬指出,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/243339.html
標籤:其他
