请选择 进入手机版 | 继续访问电脑版

Android GifImageView加载Gif图片及原理

[复制链接]
谭先生 发表于 2021-1-3 11:54:46 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
配景

前几天看到个有趣的动图,原来下载下来想发给朋侪看看的,但是用微信发送时候提示文件过大,一看巨细竟然是41M,好吧我说这个动图怎么长,于是就在想这么大的gif怎么加载的。所以就搞了个demo去试试。
Glide

众所周知Glide支持加载gif图片,所以一开始先使用Glide。将动图放到raw中,然后用Glide加载。
  1. Glide.with(this).load(R.raw.aa).into(gifImageView);
复制代码
然后等了半天一点反应也没有,就望见log一直在打印:
  1. 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

导入:
  1. implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.19'
复制代码
使用就宁静常的ImageView一样:
  1. [/code] 它可以自动识别设置的是否是gif图片,如果是平常图片那效果就和设置ImageView大概ImageButton一样。也可以在java中直接设置:
  2. [code]gifImageView.setImageResource(int resId);gifImageView.setBackgroundResource(int resId);//设置GifDrawablegifImageView.setImageDrawable(GifDrawable gifDrawable);
复制代码
GifDrawable 可以直接从各种泉源构建,各人应该一看就懂了,不再翻译了直接贴过来:
  1. //asset fileGifDrawable gifFromAssets = new GifDrawable( getAssets(), "anim.gif" );                //resource (drawable or raw)GifDrawable gifFromResource = new GifDrawable( getResources(), R.drawable.anim );                //UriContentResolver contentResolver = ... //can be null for file:// UrisGifDrawable gifFromUri = new GifDrawable( contentResolver, gifUri );//byte arraybyte[] rawGifBytes = ...GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );                //FileDescriptorFileDescriptor fd = new RandomAccessFile( "/path/anim.gif", "r" ).getFD();GifDrawable gifFromFd = new GifDrawable( fd );                //file pathGifDrawable gifFromPath = new GifDrawable( "/path/anim.gif" );                //fileFile gifFile = new File(getFilesDir(),"anim.gif");GifDrawable gifFromFile = new GifDrawable(gifFile);                //AssetFileDescriptorAssetFileDescriptor 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 ByteBufferByteBuffer rawGifBytes = ...GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );
复制代码
InputStreams 会自动close当GifDrawable 不再使用时,所以不需要手动去关闭输入流了。
通过GifDrawable 可以对gif举行暂停、重置、播放等使用,非常的方便:
  1. gifDrawable.start(); //开始播放gifDrawable.stop(); //停止播放gifDrawable.reset(); //复位,重新开始播放gifDrawable.isRunning(); //是否正在播放gifDrawable.setSpeed(float factor) ;//设置播放速度,比如2.0f以两倍速度播放gifDrawable.seekTo(int position); //跳到指定播放位置gifDrawable.getCurrentPosition() ; //获取现在到从开始播放所履历的时间gifDrawable.getDuration() ; //获取播放一次所需要的时间gifDrawable.recycle();//释放内存*/
复制代码
简朴用代码演示一下吧:
  1. 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开始:
  1. new GifDrawable(getResources(),R.raw.aa);
复制代码
  1.         /**         * 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);        }
复制代码
可以看到这内里有设置宽高。再看它的重载构造方法:
  1. public GifDrawable(@NonNull AssetFileDescriptor afd) throws IOException {        this(new GifInfoHandle(afd), null, null, true);}
复制代码
传入的是AssetFileDescriptor,读取raw下面资源用的。然后new了一个GifInfoHandle。
  1. 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:
  1.         /**         * Frame buffer, holds current frame.         */        final Bitmap mBuffer;
复制代码
RenderTask 是一个Runnable,它的父类SafeRunnable 继承自Runnable,先看下doWork干了什么:
  1. 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。
  1.         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就绘制完成了:
  1. typedef struct {        GifColorType rgb;        uint8_t alpha;} argb;typedef struct GifColorType {        uint8_t Red, Green, Blue;} GifColorType;
复制代码
blitNormal就是剖析gif而且绘制bitmap的过程。再回到RenderTask的doWork()中来,此时bitmap已经绘制完成,然后调用:
  1. mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0);
复制代码
  1. class InvalidationHandler extends Handler {        static final int MSG_TYPE_INVALIDATION = -1;        private final WeakReference 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方法,举行绘制:
  1.         @Override        public void invalidateSelf() {                super.invalidateSelf();                scheduleNextRender();        }
复制代码
下一帧绘制也是通过RenderTask来实现,将RenderTask丢到线程池中,当下一帧时间到了便执行RenderTask父类SafeRunnable的run方法,run方法中又去调用doWork()方法,便形成了一个循环,到达一连播放的目标。
总结

android-gif-drawable源码不算特别复杂,主线流程也很容易理清,详细的细节就没有仔细去看了。实在android源码中也有剖析gif的库,路径如下:

也可以使用源码中的库举行gif加载,不外剖析过程照旧需要自己去实现,但是要对gif编码有一定的相识,有时间的话我会实验自己实现一个gif加载框架,本日就先到这吧。不敷之处还请各位大佬指出。

来源:https://blog.csdn.net/u012894808/article/details/112008603
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

发布主题

专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )