背景

前几天看到个有趣的动图,本来下载下来想发给朋友看看的,但是用微信发送时候提示文件过大,一看大小竟然是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.GifImageViewandroid: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;@Overrideprotected 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的GifInfoHandlemNativeInfoHandle = 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);}}}//初始化bitmapif (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);}@Overridepublic 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);}@Overridepublic 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方法,进行绘制:

 @Overridepublic void invalidateSelf() {super.invalidateSelf();scheduleNextRender();}

下一帧绘制也是通过RenderTask来实现,将RenderTask丢到线程池中,当下一帧时间到了便执行RenderTask父类SafeRunnable的run方法,run方法中又去调用doWork()方法,便形成了一个循环,达到连续播放的目的。

总结

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


也可以使用源码中的库进行gif加载,不过解析过程还是需要自己去实现,但是要对gif编码有一定的了解,有时间的话我会尝试自己实现一个gif加载框架,今天就先到这吧。不足之处还请各位大佬指出。

Android GifImageView加载Gif图片及原理相关推荐

  1. Android Glide加载圆形图片、圆角图片,部分圆角图片的使用方法

    Android Glide加载圆形图片.圆角图片,部分圆角图片的使用方法 前言 Gilde圆形图片/头像 Gilde普通圆角图片即四个角都是圆角 Gilde对指定角设置圆角 前言 通过本文,您可以实现 ...

  2. android中加载Gif图片

           很多时候由于项目的需要,我们需要加载Gif的图片,实现动画效果,但是android本身并不支持直接加载Gif图片.因此网络上出现了很多关于android加载Gif图片的框架.今天在这里就 ...

  3. Android 高效加载大图片

    来源:http://www.open-open.com/lib/view/open1389755918242.html 我们在编写Android程序的时候经常要用到许多图片,不同图片总是会有不同的形状 ...

  4. Android Glide 加载圆形图片(绝对实用)

    1.导入依赖 implementation 'com.github.bumptech.glide:glide:4.6.1' 2.用Glide加载圆形图片 Glide.with(context).loa ...

  5. Android Glide加载圆形图片,设置圆形边框

    Glide加载图片,Glide可以很方便的实现圆形加载并且也还可以设置变色边框. 关于Glide的加载在这里不详细说了,主要说一下自定义BitmapTransformation来实现圆形图片加载 st ...

  6. android webview加载html图片自适应手机屏幕大小点击查看大图

    我们在开发中,显示信息详情时,一般后台会给出html文本,在android端一般采用webview控件来展示,但是后台给出的html文本一般是给电脑端用的,没有自适配手机,导致手机端图片显示过大,需要 ...

  7. Android实现加载(loading)图片旋转的三种方式

    我们在Android应用开发中可能经常用到类似如下效果的加载过程中的图片旋转效果: 上面的图片是一张gif格式的动态图片,我们知道,在Android中对gif动态图片的支持是不好的.可以通过第三方ja ...

  8. 解决Android 加载大图片OOM

    图片在Android 占用内存计算 假如一张图的像素为100×200,那么他在内存中占用的内存为: 100×200(像素点) × 4(每个像素点占用的内存,默认为4.) public Bitmap.C ...

  9. android 广告效果图,Android_Android实现加载广告图片和倒计时的开屏布局,这是一个android开屏布局的实例 - phpStudy...

    Android实现加载广告图片和倒计时的开屏布局 这是一个android开屏布局的实例,可以用于加载广告图片和倒计时的布局.程序中设置的LayoutParams,划分额外空间比例为6分之5,具体权重比 ...

  10. Android加载的图片在内存中的大小

    1.图片占内存的大小 计算公式:占内存大小 = 分辨率 * 像素点的大小. 其中每个像素点的大小如下: ALPHA_8 -- (1B) RGB_565 -- (2B) ARGB_4444 -- (2B ...

最新文章

  1. 学python爬虫需要什么基础-学爬虫,需要掌握哪些Python基础?
  2. c语言内存拷贝 memcpy()函数
  3. [WEB API] CLIENT 指定请求及回应格式(XML/JSON)
  4. MCSDK HUA Demonstration Guide
  5. IOCP配合AcceptEx的例子
  6. HP unix 常用管理命令
  7. mysql添加新用户
  8. java的debug模式_java第六章:debug模式介绍及大量实例练习
  9. 60-330-000-使用-窗口TopN分析与实现
  10. 通过配置文件连接mysql_利用配置文件连接数据库
  11. 只需简单的整理,让你的Mac 更安全、更智能
  12. 深度学习pytorch基础入门教程(1小时)-张量、操作、转换
  13. Supervisor的作用与配置
  14. linux 蓝牙管理软件,Blueman - Ubuntu的蓝牙管理器
  15. 达梦8数据库安装教程
  16. SRE(站点可靠性工程)介绍
  17. 大数据杀熟!我被美团会员割了韭菜
  18. linux命令行下的BT软件
  19. python 扩展c extention
  20. 月薪50K的测试工程师,要求原来是这样!

热门文章

  1. 如何在本地运行jar文件
  2. 《C专家编程》:编译器的金科玉律(一)
  3. ios苹果越狱教程(奥德赛)
  4. Maxwell安装与配置
  5. 苹果cmsV10资源站模板
  6. DNS DDNS NBNS mDNS LLMNR LLDPDU SSDP协议
  7. IOS视频播放器VKVideoPlayer
  8. linux如何设置环境变量
  9. python图片转excel_利用python将图片转换成excel文档格式
  10. FFmpeg命令行工具学习(二):播放媒体文件的工具ffplay