精华内容
下载资源
问答
  • Glide

    千次阅读 2018-07-26 21:27:57
    Glide是一款由Bump Technologies开发的图片加载框架,使得我们可以在Android平台上以极度简单的方式加载和展示图片。  Glide是一个快速高效的Android图片加载库,注重于平滑的滚动。Glide提供了易用的API,高性能、...

    一个图片加载库
    Glide是一款由Bump Technologies开发的图片加载框架,使得我们可以在Android平台上以极度简单的方式加载和展示图片。 
    Glide是一个快速高效的Android图片加载库,注重于平滑的滚动。Glide提供了易用的API,高性能、可扩展的图片解码管道(decode pipeline),以及自动的资源池技术。

    为什么使用Glide ? TT5
    多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
    生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求)
    高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力)
    高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不同,使得内存开销是Picasso的一半)

    (WebP(发音weppy,项目主页),是一种支持有损压缩和无损压缩的图片文件格式,派生自图像编码格式 VP8。根据 Google 的测试,无损压缩后的 WebP 比 PNG 文件少了 45% 的文件大小,即使这些 PNG 文件经过其他压缩工具压缩之后,WebP 还是可以减少 28%的文件大小。)

    Glide 的使用?
    Glide.with(Context)
              .load(Url)
              .into(imageView);

    目前,Glide最稳定版本是3.7.0   
                        最新版本是4.2.0       
     
    使用Glide 加载一张图片
    Glide.with(Context).load(Url) .into(imageView);
    这一行代码进可以做非常非常多的事情了,包括加载网络上的图片、加载手机本地的图片、加载应用资源中的图片等等。
    下面我们就来详细解析一下这行代码。

    首先,调用Glide.with()方法用于创建一个加载图片的实例。with()方法可以接收Context、Activity或者Fragment类型的参数。也就是说我们选择的范围非常广,
    不管是在Activity还是Fragment中调用with()方法,都可以直接传this。那如果调用的地方既不在Activity中也不在Fragment中,可以获取当前应用程序的ApplicationContext,
    传入到with()方法当中。注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,
    图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。

    接下来看一下load()方法,这个方法用于指定待加载的图片资源。Glide支持加载各种各样的图片资源,包括网络图片、本地图片、应用资源、二进制流、Uri对象等等。
    因此load()方法也有很多个方法重载,除了加载一个字符串网址之外,你还可以这样使用load()方法:
    // 加载本地图片
    File file = new File(getExternalCacheDir() + "/image.jpg");
    Glide.with(this).load(file).into(imageView);

    // 加载应用资源
    int resource = R.drawable.image;
    Glide.with(this).load(resource).into(imageView);

    // 加载二进制流
    byte[] image = getImageBytes();
    Glide.with(this).load(image).into(imageView);

    // 加载Uri对象
    Uri imageUri = getImageUri();
    Glide.with(this).load(imageUri).into(imageView);

    看一下into()方法,我们希望让图片显示在哪个ImageView上,把这个ImageView的实例传进去就可以了。

    现在我们来学一些Glide的扩展内容。其实刚才所学的三步走就是Glide最核心的东西,而我们后面所要学习的所有东西都是在这个三步走的基础上不断进行扩展而已。

    观察刚刚加载网络图片的效果,你会发现图片需要稍微等一会图片才会显示出来。这其实很容易理解,因为从网络上下载图片本来就是需要时间的。我们可以进一步的优化一下用户体验,Glide提供了各种各样非常丰富的API支持,其中就包括了占位图功能。

    顾名思义,占位图就是指在图片的加载过程中,我们先显示一张临时的图片,等图片加载出来了再替换成要加载的图片。
    Glide.with(this).load(url).placeholder(R.drawable.loading).into(imageView);
    我们只是在刚才的三步走之间插入了一个placeholder()方法,然后将占位图片的资源id传入到这个方法中即可。另外,这个占位图的用法其实也演示了Glide当中绝大多数API的用法,其实就是在load()和into()方法之间串接任意想添加的功能就可以了。
    运行完之后很可能是根本看不到占位图效果的。因为Glide有非常强大的缓存机制,我们刚才加载那张必应美图的时候Glide
    自动就已经将它缓存下来了,下次加载的时候将会直接从缓存中读取,不会再去网络下载了,因而加载的速度非常快,
    所以占位图可能根本来不及显示。可以使用diskCacheStrategy()方法,并传入DiskCacheStrategy.NONE参数,这样就可以禁用掉Glide硬盘缓存功能。
    除了这种加载占位图之外,还有一种异常占位图。异常占位图就是指,如果因为某些异常情况导致图片加载失败,比如说手机网络信号不好,这个时候就显示这张异常占位图。
    添加一个error()方法就可以指定异常占位图了。

    我们还需要再了解一下Glide另外一个强大的功能,那就是Glide是支持加载GIF图片的。而使用Glide加载GIF图并不需要编写什么额外的代码,Glide内部会自动判断图片格式。也就是说,不管我们传入的是一张普通图片,还是一张GIF图片,Glide都会自动进行判断,并且可以正确地把它解析并展示出来。

    asBitmap()方法,这个方法的意思就是说这里只允许加载静态图片,如果是gif则加载第一帧。
    asGif()方法,这个方法的意思就是说这里只允许加载动态图片 ,如果是非gif,则加载失败。
    override()方法指定了一个图片的尺寸

    使用Glide还有一个更重要的就是,完全不用担心图片内存浪费,甚至是内存溢出的问题。因为Glide从来都不会直接将图片的完整尺寸全部加载到内存中,而是用多少加载多少。Glide会自动判断ImageView的大小,然后只将这么大的图片像素加载到内存当中,帮助我们节省内存开支。

    Glide的缓存设计可以说是非常先进的,考虑的场景也很周全。在缓存这一功能上,
    Glide又将它分成了两个模块,一个是内存缓存,一个是硬盘缓存。

    内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,
    硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。

    内存缓存和硬盘缓存的相互结合才构成了Glide极佳的图片缓存效果

    首先要知道,默认情况下,Glide自动就是开启内存缓存的。
    也就是说,当我们使用Glide加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用Glide再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方说你在一个RecyclerView当中反复上下滑动,RecyclerView中只要是Glide加载过的图片都可以直接从内存当中迅速读取并展示出来,从而大大提升了用户体验。

    而Glide最为人性化的是,你甚至不需要编写任何额外的代码就能自动享受到这个极为便利的内存缓存功能,
    因为Glide默认就已经将它开启了。
    如果不想使用内存缓存的话调用skipMemoryCache()方法并传入true,就表示禁用掉Glide的内存缓存功能。

    内存缓存就是LruCache算法(LeastRecentlyUsed),也叫 近期最少使用算法。它的主要算法原理就是把最近使用的
    对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
    Glide内存缓存的实现自然也是使用的LruCache算法。不过除了LruCache算法之外,Glide还结合了一种弱引用的机制,
    共同完成了内存缓存功能
    了解:
      LruCache存储是在LinkedHashMap,因为LruCache中Lru算法的实现就是通过LinkedHashMap来实现的。LinkedHashMap继承于HashMap,它使用了一个双向链表来存储Map中的Entry顺序关系,这种顺序有两种,一种是LRU顺序,一种是插入顺序,这可以由其构造函数publicLinkedHashMap(intinitialCapacity,floatloadFactor,booleanaccessOrder)指定。
    所以,对于get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,还做些调整Entry顺序链表的工作。LruCache中将LinkedHashMap的顺序设置为LRU顺序来实现LRU缓存,每次调用get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。调用put插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。

    这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收四种参数:
    DiskCacheStrategy.NONE: 表示不缓存任何内容。
    DiskCacheStrategy.SOURCE: 表示只缓存原始图片。
    DiskCacheStrategy.RESULT: 表示只缓存转换过后的图片(默认选项)。
    DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。
    上面四种参数的解释本身并没有什么难理解的地方,但是有一个概念大家需要了解,就是当我们使用Glide去加载一张图片的时候,Glide默认并不会将原始图片展示出来,而是会对图片进行压缩和转换(我们会在后面学习这方面的内容)。总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。而Glide默认情况下在硬盘缓存的就是转换过后的图片,我们通过调用diskCacheStrategy()方法则可以改变这一默认行为。
    硬盘缓存的实现也是使用的LruCache算法,而且Google还提供了一个现成的工具类DiskLruCache。基本的实现原理都是差不多的。

    只需要通过skipMemoryCache()  跳过内存缓存
                  和diskCacheStrategy()  磁盘缓存策略
    这两个方法就可以轻松自如地控制Glide的缓存功能了。

    Glide 和 Picasso 区别

    从内存开销来说
    Picasso 加载图片时的内存是Glide 的两倍
    原因是Picasso是加载了全尺寸的图片到内存,然后让GPU来实时重绘大小。
    而Glide加载的大小和ImageView的大小是一致的,因此更小。
    在这个问题上Glide完胜Picasso。因为Glide可以自动计算出任意情况下的ImageView大小。

    从缓存问题来说
    不管大小如何Picasso只缓存一个全尺寸的。
    Glide则不同,它会为每种大小的ImageView缓存一次。尽管一张图片已经缓存了一次,但是假如你要在另外一个地方再次以不同尺寸显示,需要重新下载,调整成新尺寸的大小,然后将这个尺寸的也缓存起来。
    Glide的这种方式优点是加载显示非常快。而Picasso的方式则因为需要在显示之前重新调整大小而导致一些延迟,
    相对来说用户体验不太好。

    Glide可以加在GIF动态图,而Picasso不能。 

    总结

    Glide和Picasso都是非常完美的库。Glide加载图像以及磁盘缓存的方式都要优于Picasso,速度更快,
    并且Glide更有利于减少OutOfMemoryError(内存不足错误)的发生,GIF动画是Glide的杀手锏。不过Picasso的图片质量更高。

    如果想要更深入的了解Glide 可以去GSDN上搜郭霖的博客上边有更详细的讲解
    博客地址:https://blog.csdn.net/guolin_blog


     

    展开全文
  • Glide详解

    万次阅读 2019-07-02 10:04:26
    现在Android上的图片加载框架非常成熟,从最早的老牌图片加载框架UniversalImageLoader,到后来Google推出的Volley,再到后来的新兴军Glide和Picasso,当然还有Facebook的Fresco。每一个都非常稳定,功能也都十分...

    现在Android上的图片加载框架非常成熟,从最早的老牌图片加载框架UniversalImageLoader,到后来Google推出的Volley,再到后来的新兴军Glide和Picasso,当然还有Facebook的Fresco。每一个都非常稳定,功能也都十分强大。但是它们的使用场景基本都是重合的,也就是说我们基本只需要选择其中一个来进行学习和使用就足够了,每一个框架都尝试去掌握的话则有些浪费时间。Glide和Picasso 有90%相似度,而Glide在Picasso基础上进行的二次开发,可以其优势显而易见。

    一.添加依赖

    repositories {
      mavenCentral()
      google()
    }

    dependencies {
    implementation 'com.github.bumptech.glide:glide:4.8.0'//glide第三方图片加载所需要的包
    }

    二.添加权限

       <uses-permission android:name="android.permission.INTERNET"/>
        //它可以监听用户的连接状态并在用户重新连接到网络时重启之前失败的请求
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
        //用于硬盘缓存和读取
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

     

    三.用法

    3.1基本使用

    string url="图片地址";

    3.1.1加载图片三步走,1,with; 2,load; 3 into;

    Glide.with(this)             //with()方法可以接收Context、Activity或者Fragment类型的参数
                    .load(url)        //load方法中不仅可以传入图片地址,还可以传入图片文件File,resource,图片的byte数组等
                    .into(imageView);
        注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。

    // 加载本地图片
    File file = new File(getExternalCacheDir() + "/image.jpg");
    Glide.with(this).load(file).into(imageView);

    // 加载应用资源
    int resource = R.drawable.image;
    Glide.with(this).load(resource).into(imageView);

    // 加载二进制流
    byte[] image = getImageBytes();
    Glide.with(this).load(image).into(imageView);

    // 加载Uri对象
    Uri imageUri = getImageUri();
    Glide.with(this).load(imageUri).into(imageView);
     注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。


    加载圆形图

    Glide.with(this)
            .load(R.mipmap.ic_splash_bg)
            .apply(bitmapTransform(new CropCircleTransformation()))
            .into(mImageHead);
     模糊过滤

    Glide.with(this)
                    .load(R.mipmap.ic_splash_bg)
                    .apply(bitmapTransform(new BlurTransformation( 25, 4)))
                    .into(mImageView);


      
        3.1.2取消图片也是三步走,1,with; 2,load; 3 clear;
        Glide.with(this).load(url).clear();
        一般来很少会用到取消图片的,因为图会跟with(this)生命周期消亡而消亡的。


    3.2Glide中的大部分设置项都可以通过 RequestOptions 类和 apply() 方法来应用到程序中

    RequestOptions options = new RequestOptions()
                    .placeholder(R.mipmap.ic_launcher)                //加载成功之前占位图
                    .error(R.mipmap.ic_launcher)                    //加载错误之后的错误图
                    .override(400,400)                                //指定图片的尺寸
                    //指定图片的缩放类型为fitCenter (等比例缩放图片,宽或者是高等于ImageView的宽或者是高。)
                    .fitCenter()
                    //指定图片的缩放类型为centerCrop (等比例缩放图片,直到图片的狂高都大于等于ImageView的宽度,然后截取中间的显示。)
                    .centerCrop()
                    .circleCrop()//指定图片的缩放类型为centerCrop (圆形)
                    .skipMemoryCache(true)                            //跳过内存缓存
                    .diskCacheStrategy(DiskCacheStrategy.ALL)        //缓存所有版本的图像
                    .diskCacheStrategy(DiskCacheStrategy.NONE)        //跳过磁盘缓存
                    .diskCacheStrategy(DiskCacheStrategy.DATA)        //只缓存原来分辨率的图片
                    .diskCacheStrategy(DiskCacheStrategy.RESOURCE)    //只缓存最终的图片
                    ;
            Glide.with(this)
                    .load(url)
                    .apply(options)
                    .into(imageView);
    注意 实际上,使用Glide在大多数情况下我们都是不需要指定图片大小的,因为Glide会自动根据ImageView的大小来决定图片的大小,以此保证图片不会占用过多的内存从而引发OOM。不过,如果你真的有这样的需求,必须给图片指定一个固定的大小,Glide仍然是支持这个功能的。修改Glide加载部分的代码,仍然非常简单,这里使用override()方法指定了一个图片的尺寸。也就是说,Glide现在只会将图片加载成400*400像素的尺寸,而不会管你的ImageView的大小是多少了。如果你想加载一张图片的原始尺寸的话,可以使用Target.SIZE_ORIGINAL关键字,.override(Target.SIZE_ORIGINAL);这样的话,Glide就不会再去自动压缩图片,而是会去加载图片的原始尺寸。当然,这种写法也会面临着更高的OOM风险。
                这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收五种参数:
                DiskCacheStrategy.NONE: 表示不缓存任何内容。
                DiskCacheStrategy.DATA: 表示只缓存原始图片。
                DiskCacheStrategy.RESOURCE: 表示只缓存转换过后的图片。
                DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。
                DiskCacheStrategy.AUTOMATIC: 表示让Glide根据图片资源智能地选择使用哪一种缓存策略(默认选项)。
                    
        Glide其中一个非常亮眼的功能就是可以加载GIF图片,而同样作为非常出色的图片加载框架的Picasso是不支持这个功能的。而且使用Glide加载GIF图并不需要编写什么额外的代码,Glide内部会自动判断图片格式。
        但是如果我想指定加载格式该怎么办呢?想实现这个功能仍然非常简单,我们只需要再串接一个新的方法就可以了
            Glide.with(this)
                 .asBitmap()
                 .load(url)
                 .into(imageView);
            这里在with()方法的后面加入了一个asBitmap()方法,这个方法的意思就是说这里只允许加载静态图片,不需要Glide去帮我们自动进行图片格式的判断了。如果你传入的还是一张GIF图的话,Glide会展示这张GIF图的第一帧,而不会去播放它。对应的方法是asGif()。而Glide 4中又新增了asFile()方法和asDrawable()方法,分别用于强制指定文件格式的加载和Drawable格式的加载.

    另:Glide加载转换Bitmap 方法:

    try {
        Bitmap bitmap = Glide
                .with(this)
                .asBitmap()//这一个必须加
                .load("url")
                .into(100, 100)
                .get();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

     

    四.Glide4.8.0升级后的使用

    glide升级到4.8.0后原方法error/preload/fallback/signature无法使用

    现在改成:

     

    原方法error/preload/fallback等方法都在RequestOptions中添加,其中之前.signature()方法形参支持使用StringSignature,但是现在StringSignature完全被废掉了,不过你可以借用signature方法中的Key中的一个使用相对方便的子类ObjectKey,构造方法以Object为参数:

    signature它是用来刷新glide填充的图片刷新用的。
    简单使用,先整起来,具体原因下次分晓。

    五.Glide常见错误

    5.1java.lang.IllegalArgumentException: You must not call setTag() on a view

    使用Glide发现这个bug,发现glide要对ImageView设置url为tag会报错,解决方法:imageView的setTag()去掉

    或者:https://blog.csdn.net/yuangudashen/article/details/75287670

     

    展开全文
  • Glide适配Androidx 背景 项目中使用的Glide版本是3.7.0。因为项目整体要迁移到Androidx,所以开始了一场Glide的大版本升级旅程。 官方文档 https://muyangmin.github.io/glide-docs-cn/doc/download-setup.html 按照...

    Glide适配Androidx

    背景

    项目中使用的Glide版本是3.7.0。因为项目整体要迁移到Androidx,所以开始了一场Glide的大版本升级旅程。

    官方文档

    https://muyangmin.github.io/glide-docs-cn/doc/download-setup.html
    按照官方步骤大体可行,但总是会遇到一些意外情况,比如对于Androidx的兼容问题。

    兼容Androidx

    因为Glide.4.8.0系列的包并没有兼容Androidx,所以在项目迁移到Androidx时需要继续升级Glide版本。
    1、直接使用Glide最新版本4.11.0,此版本已经适配Androidx。
    2、需要引入两个关联的Androidx库:
    androidx.vectordrawable:vectordrawable-animated:1.1.0
    androidx.exifinterface:exifinterface:1.2.0

    libs依赖

    有些情况可能我们需要下载库包,而不是maven依赖
    1、如下地址下载Glide.4.11.0最新包
    https://mvnrepository.com/artifact/com.github.bumptech.glide/glide
    2、需要引入两个关联的Androidx库:
    androidx.vectordrawable:vectordrawable-animated:1.1.0
    androidx.exifinterface:exifinterface:1.2.0
    3、jar包会导致关联包无法引入,需要下载关联jar
    Glide GIF Decoder Library:
    https://mvnrepository.com/artifact/com.github.bumptech.glide/gifdecoder
    Glide Disk LRU Cache Library:
    https://mvnrepository.com/artifact/com.github.bumptech.glide/disklrucache
    4、我们的项目引入如上两个包已经编译成功。但是不排除其他项目有更多依赖,如果遇到 NoClassNotFound的情况,可以参考如下地址寻找关联包:
    https://mvnrepository.com/artifact/com.github.bumptech.glide

    一些消失的方法

    需要借用apply和RequestOptions:
    Glide.with(context).load(R.drawable.ic_transport_theme_art)
    .apply(new RequestOptions().skipMemoryCache(true).diskCacheStrategy(DiskCacheStrategy.NONE))
    .into(mThemeCardBgTarget);

    AppGlideModule

    这个东西利用注解帮你桥接了老方法,可以继续使用升级后消失的方法,如下:
    不过需要使用GlideApp
    GlideApp.with(context).load(R.drawable.ic_transport_theme_art)
    .skipMemoryCache(true).diskCacheStrategy(DiskCacheStrategy.NONE).into(mThemeCardBgTarget);

    神坑!!!! java.lang.IllegalStateException: You cannot call Glide.get() in registerComponents(), use the provided Glide instance instead

    Glide升级后,使用了单例模式,所以并发使用Glide.with会出现如上报错。
    解决办法:
    可以在并发使用Glide.with的地方加锁;或者继承Application,在应用刚起动的最开始,做一次无用的Glide.with初始化实例。

    展开全文
  • 深入解析Glide

    千次阅读 2021-04-06 10:02:25
    在深入了解Glide之前,我们要先来认识一下Glide到底是什么,不要只知道他是图片处理框架,只会其简单的使用,只知道这些是不够的,因为学东西就是要刨根问底,学以致用。今天我们就带着这样几个问题来学习Glide。...

    在深入了解Glide之前,我们要先来认识一下Glide到底是什么,不要只知道他是图片处理框架,只会其简单的使用,只知道这些是不够的,因为学东西就是要刨根问底,学以致用。今天我们就带着这样几个问题来学习Glide。Glide是什么?Glide的实现原理是什么?Glide对比其他图片加载框架有什么优缺点(也就是为什么要使用Glide)? 自己如何设计一款图片加载框架?

    一:Glide是什么?

    1.Glide是什么?

    Glide是Google在2014的IO大会发布一款图片处理框架,是目前android领域比较成熟的一款图片处理框架,也是Google官方推荐的图片处理框架,它主要支持网络图片、二进制流、drawable资源、本地图片显示,还支持本地视频显示。

    2. Glide 基本用法

    2.1 在app级别下面配置gradle引用

    添加引用

    dependencies {
      implementation 'com.github.bumptech.glide:glide:4.11.0' //图片加载框架
       annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'    //图片加载框架注解处理器
    }
    

    申请权限

    <uses-permission android:name="android.permission.INTERNET" />
    

    2.2 Glide类简单的链式调用

    Glide.with(getApplicationContext()).load(imageurl).into(imageview)
    

    上面的简单的链式调用调用我想大家都用过。链式调用的参数大概有如下几个

    • with()

      在使用过程中尽量要传入Applicaiton、Activity 、Fragment等类型的参数,因为glide加载图片的请求会与该参数的调用者的生命周期绑定在一起,如果onPaush时候,Glide就会暂停加载,重新onResume之后,又会继续加载。

    • load()
      支持网络图片网址、二进制流、drawable资源、本地图片的传入。

    • crossFade
      是否开启显示淡入淡出动画。

    • override
      如果获取的网络图片过大,我们通过它进行一个大小的裁剪,传入width和height参数进行宽高裁剪。

    • diskCacheStrategy
      磁盘缓存的设置,默认Glide会开启的。磁盘缓存又包括如下几种:

    DiskCacheStrategy.NONE 什么都不缓存
    
    DiskCacheStrategy.SOURCE 只缓存全尺寸图
    
    DiskCacheStrategy.RESULT 只缓存最终的加载图
    
    DiskCacheStrategy.ALL 缓存所有版本图(默认)
    
    

    Glide 不仅缓存了全尺寸的图,还会根据 ImageView 大小所生成的图也会缓存起来。比如,请求一个 800x600 的图加载到一个 400x300 的 ImageView 中,Glide默认会将这原图还有加载到 ImageView 中的 400x300 的图也会缓存起来。

    • error
      这里的设置是当加载图片出现错误时,显示的图片。
    • placeholder
      图片加载完成之前显示的占位图。也是预加占位图。
    • into()
      一般传 ImageView。就是要显示的控件view。

    2.3 Glide的缓存机制

    上面我们介绍过diskCacheStrategy 有四种缓存:

    1. DiskCacheStrategy.NONE 什么都不缓存
    2. DiskCacheStrategy.SOURCE 只缓存全尺寸图
    3. DiskCacheStrategy.RESULT 只缓存最终的加载图
    4. DiskCacheStrategy.ALL 缓存所有版本图(默认)

    缓存一般通过键值对的形式,所以存在缓存键。缓存的键包括图片的宽、高、signature等参数。

    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
            resourceClass, transcodeClass, options);
    

    2.4 Glide的图片加载策略

    在这里插入图片描述

    1. 首先从ActivateResource获取,是个值为弱引用的Map
    2. MemoryCache和DiskCache是LruCache

    MemoryCache和ActivateResource关系如下:
    在这里插入图片描述
    图片加载时会从MemoryCache移到ActivateResouce,生命周期结束后会缓存至MemoryCache,所以内存中至多有一份缓存。

    2.5 Glide的主要线程池

    public Glide build(@NonNull Context context) {
        if (sourceExecutor == null) {
            sourceExecutor = GlideExecutor.newSourceExecutor();//创建网络加载线程池对象
        }
    
        if (diskCacheExecutor == null) {
            diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();//创建磁盘加载线程池对象
        }
    
        if (animationExecutor == null) {
            animationExecutor = GlideExecutor.newAnimationExecutor();//创建动画加载线程池对象
        }
        
    }
    

    二:Glide的实现原理是什么?

    我们先解读一下源码。

    1. Glide.class和RequestManagerRetriever.class,主要用来获得RequestManager
    //with返回一个RequestManager
    public static RequestManager with(Activity activity) {
        return getRetriever(activity).get(activity);
    }
    //无论调用的是哪个with重载方法,最后都会到这里
    public RequestManager get(Activity activity) {
        if (Util.isOnBackgroundThread()) {
            return get(activity.getApplicationContext());
        } else {
            assertNotDestroyed(activity);
            android.app.FragmentManager fm = activity.getFragmentManager();
            return fragmentGet(activity, fm, null);
        }
    }
    
    //这里新建了一个没有视图的RequestManagerFragment 
    private RequestManager fragmentGet(Context context,
                                       android.app.FragmentManager fm,
                                       android.app.Fragment parentHint) {
        RequestManagerFragment current = getRequestManagerFragment(fm, parentHint);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
            Glide glide = Glide.get(context);
         //绑定requestManager和Fragment的Lifecycle
            requestManager =
                    factory.build(
                            glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
            current.setRequestManager(requestManager);
        }
        return requestManager;
    }
    
    1. RequestManagerFragment.class中持有一个lifecycle,在Fragment进入关键生命周期时会主动通知lifecycle执行相关方法
    public class RequestManagerFragment extends Fragment {
      ...
      private final ActivityFragmentLifecycle lifecycle;
      ...
     @Override
      public void onStart() {
        super.onStart();
        lifecycle.onStart();
      }
    
      @Override
      public void onStop() {
        super.onStop();
        lifecycle.onStop();
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        lifecycle.onDestroy();
      } 
    }
    
    1. ActivityFragmentLifecycle.class中持有一个lifecycleListeners,在Fragment进入关键生命周期时Lifecycle会通知他的所有Listener
    class ActivityFragmentLifecycle implements Lifecycle {
     ...
      private final Set<LifecycleListener> lifecycleListeners;void onStart() {
        isStarted = true;
        for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
          lifecycleListener.onStart();
        }
      }
    
      void onStop() {
        isStarted = false;
        for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
          lifecycleListener.onStop();
        }
      }
    
      void onDestroy() {
        isDestroyed = true;
        for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
          lifecycleListener.onDestroy();
        }
      }
      ...
    }
    
    1. RequestManger.class关键生命周期中处理加载任务
    @Override
    public void onStart() {
        resumeRequests();
        targetTracker.onStart();
    }
    
    @Override
    public void onStop() {
        pauseRequests();
        targetTracker.onStop();
    }
    
    @Override
    public void onDestroy() {
        targetTracker.onDestroy();
        for (Target<?> target : targetTracker.getAll()) {
            clear(target);
        }
        targetTracker.clear();
        requestTracker.clearRequests();
    }
    

    根据源码,你会发现,withGlide类的一个静态方法,重载方法很多可以接收Activity,Fragment,Contextwith方法里面,首先会调用RequestManagerRetriever的静态get方法得到RequestManagerRetriver对象。然后再调用该对象的get方法获取RequestManager对象。静态get方法中也有很多重载方法,主要分为传入Application参数和非Application参数,传入Application参数是最简单的情况,Glide只要持保和整个应用生命周期同步。

    Application参数不管是Activity,Fragment,最终都会向当前Activity传入一个隐藏的Fragment,因为Glide需要监控Activity的生命周期,Fragment依赖Activity生命周期并且是同步的,通过这个隐藏的Fragment就监听到Activity生命周期。

    load方法,with方法返回的是一个RequestManager对象,所以load方法在RequestManager类中,load方法也有很多重载,支持本地图片,内存图片,网络图片,只看加载urlload方法。首先调用了fromString方法,再调用load方法,传入图片urlfromString方法里调用了loadGeneric方法,这个方法创建并返回了DrawableTypeRequest对象。

    DrawableTypeRequest并没有load方法,loadDrawableTypeRequest的父类DrawableTypeRequestBuildle中。大部分操作都在这个类中,比如placeholder占位符,error,discacheStrategy等。

    into方法是Glide图片加载流程中逻辑最为复杂的方法。into方法在DrawableTypeRequestBuilder类中,里面调用了super.into方法,真正的实现在DrawableTypeRequestBuilder的父类GenericRequestBuilder中,这个类包括了网络请求,图片解析,图片解码,bitmap生成,缓存处理,图片压缩等大量逻辑操作,最后的最后才将图片展示出来。

    分析之后,大致可以总结如下:

    1:Glide在加载绑定了Activity的生命周期。
    :2:在Activity内新建一个无UI的Fragment,这个特殊的Fragment持有一个Lifecycle。通过Lifecycle在Fragment关键生命周期通知RequestManger进行相关的操作。
    3:在生命周期onStart时继续加载,onStop时暂停加载,onDestory是停止加载任务和清除操作。

    三:为什么要使用Glide

    现在主流的图片加载框架有很多,比如我们大家熟悉的Fresco,我把Glide和Fresco做了对比,他们优缺点大致如下:

    Glide的优势:

    1. 多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
    2. 生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求)
    3. 高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力)
    4. 高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不同,使得内存开销是Picasso的一半)

    Fresco的优势:

    1. 最大的优势在于5.0以下(最低2.3)的bitmap加载。在5.0以下系统,Fresco将图片放到一个特别的内存区域(Ashmem区)
    2. 大大减少OOM(在更底层的Native层对OOM进行处理,图片将不再占用App的内存)
    3. 适用于需要高性能加载大量图片的场景

    总结:
    对于一般App来说,Glide完全够用,而对于图片需求比较大的App,为了防止加载大量图片导致OOM,Fresco 会更合适一些。并不是说用Glide会导致OOM,Glide默认用的内存缓存是LruCache,内存不会一直往上涨。

    四:自己如何设计一款图片加载框架?

    借鉴了Glide的设计原理和现实使用场景,如果要我们自己设计一款图片加载框架我觉得可能要从如下几个角度考虑。

    1. 异步加载:线程池管理
    2. 切换线程:Handler(当然也可以使用kotlin的协程)
    3. 缓存:LruCache、DiskLruCache
    4. 防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置
    5. 内存泄露:注意ImageView的正确引用,生命周期管理
    6. 列表滑动加载的问题:加载错乱、队满任务过多问题
    7. 适当加入加载动画特性,毕竟大家都是爱美的物种

    好了,我就对着上面的几个角度来实现一下。

    1:异步加载:线程池管理

    线程池,多少个?我们知道,缓存一般有三级,内存缓存、硬盘、网络。

    由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。

    读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。

    Glide 必然也需要多个线程池,上面我们看过源码,发现Glide具备如下几个线程池:

    public Glide build(@NonNull Context context) {
        if (sourceExecutor == null) {
            sourceExecutor = GlideExecutor.newSourceExecutor();//创建网络加载线程池对象
        }
    
        if (diskCacheExecutor == null) {
            diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();//创建磁盘加载线程池对象
        }
    
        if (animationExecutor == null) {
            animationExecutor = GlideExecutor.newAnimationExecutor();//创建动画加载线程池对象
        }
        
    }
    

    如果不考虑动画,实际上就有俩个,一个是读内存和硬盘可以放在一个线程池diskCacheExecutor,另外一个是网络线程池sourceExecutor ,由于网络可能都塞,就把网络放在另外一个线程池。

    2:线程切换

    我们知道耗时操作是不能放在主线程的,不然会阻塞主线程,导致陈序异常退出,所以在图片异步加载成功前,是需要放在子线程的,图片异步加载成功后,需要在主线程去更新ImageView。因此必须具备线程切换调度的功能。

    无论是RxJava、EventBus,还是Glide,只要是想从子线程切换到Android主线程,都离不开Handler。看下Glide 相关源码。

        class EngineJob<R> implements DecodeJob.Callback<R>,Poolable {
          private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory();
          //创建Handler
          private static final Handler MAIN_THREAD_HANDLER =
              new Handler(Looper.getMainLooper(), new MainThreadCallback());
    

    3:缓存

    我们常说的图片三级缓存:内存缓存、硬盘缓存、网络。所以必须具备以上缓存。

    3.1 内存缓存

    3.1.1 LruCache缓存

    一般都是用LruCache。Glide 默认内存缓存用的也是LruCache,只不过并没有用Android SDK中的LruCache,不过内部同样是基于LinkHashMap,所以原理是一样的。

    // -> GlideBuilder#build
    if (memoryCache == null) {
      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
    }
    

    既然说到LruCache ,那么就了解一下LruCache的特点和源码。

    LruCache 采用最近最少使用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。

    LruCache 构造方法里创建一个LinkedHashMap,accessOrder 参数传true,表示按照访问顺序排序,数据存储基于LinkedHashMap。看一下源码就知道了。

        public class LruCache<K, V> {
        // 数据最终存在 LinkedHashMap 中
        private final LinkedHashMap<K, V> map;
        ...
        public LruCache(int maxSize) {
            if (maxSize <= 0) {
                throw new IllegalArgumentException("maxSize <= 0");
            }
            this.maxSize = maxSize;
            // 创建一个LinkedHashMap,accessOrder 传true
            this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
        }
        ...
    

    我们还是看一下LinkedHashMap 的实现原理吧,LinkedHashMap 继承 HashMap,在 HashMap 的基础上进行扩展,put 方法并没有重写,说明LinkedHashMap遵循HashMap的数组加链表的结构。
    在这里插入图片描述
    LinkedHashMap重写了 createEntry 方法。在看下HashMap 的 createEntry 方法,HashMap的数组里面放的是HashMapEntry 对象.

    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        size++;
    }
    

    看下LinkedHashMap 的 createEntry方法,LinkedHashMap的数组里面放的是LinkedHashMapEntry对象.

    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> old = table[bucketIndex];
        LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
        table[bucketIndex] = e; //数组的添加
        e.addBefore(header);  //处理链表
        size++;
    }
    

    LinkedHashMapEntry

    private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        LinkedHashMapEntry<K,V> before, after; //双向链表
    
        private void remove() {
            before.after = after;
            after.before = before;
        }
    
        private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }
    

    LinkedHashMapEntry继承 HashMapEntry,添加before和after变量,所以是一个双向链表结构,还添加了addBefore和remove 方法,用于新增和删除链表节点。

    addBefore,将一个数据添加到Header的前面.

    private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
    }
    

    existingEntry 传的都是链表头header,将一个节点添加到header节点前面,只需要移动链表指针即可,添加新数据都是放在链表头header 的before位置,链表头节点header的before是最新访问的数据,header的after则是最旧的数据。

    下面在看看LinkedHashMapEntry的remove方法。

    private void remove() {
            before.after = after;
            after.before = before;
        }
    

    链表节点的移除比较简单,改变指针指向即可。

    再看下LinkHashMap的put 方法。

    public final V put(K key, V value) {
    
        V previous;
        synchronized (this) {
            putCount++;
            //size增加
            size += safeSizeOf(key, value);
            // 1、linkHashMap的put方法
            previous = map.put(key, value);
            if (previous != null) {
                //如果有旧的值,会覆盖,所以大小要减掉
                size -= safeSizeOf(key, previous);
            }
        }
    
        trimToSize(maxSize);
        return previous;
    }
    

    LinkedHashMap 结构可以用这种图表示:
    在这里插入图片描述
    LinkHashMap 的 put方法和get方法最后会调用trimToSize方法,LruCache 重写trimToSize方法,判断内存如果超过一定大小,则移除最老的数据。

    LruCache#trimToSize,移除最老的数据

    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
    
                //大小没有超出,不处理
                if (size <= maxSize) {
                    break;
                }
    
                //超出大小,移除最老的数据
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }
    
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                //这个大小的计算,safeSizeOf 默认返回1;
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
    
            entryRemoved(true, key, value, null);
        }
    }
    

    对LinkHashMap 还不是很理解的话可以参考:图解LinkedHashMap原理。写的是非常好。

    LruCache小结:

    • LinkHashMap 继承HashMap,在 HashMap的基础上,新增了双向链表结构,每次访问数据的时候,会更新被访问的数据的链表指针,具体就是先在链表中删除该节点,然后添加到链表头header之前,这样就保证了链表头header节点之前的数据都是最近访问的(从链表中删除并不是真的删除数据,只是移动链表指针,数据本身在map中的位置是不变的)。
    • LruCache 内部用LinkHashMap存取数据,在双向链表保证数据新旧顺序的前提下,设置一个最大内存,往里面put数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保证内存不超过设定的最大值。

    3.2 磁盘缓存 DiskLruCache

    使用前先添加如下依赖:

    implementation 'com.jakewharton:disklrucache:2.0.2'
    

    DiskLruCache 跟 LruCache 实现思路是差不多的,一样是设置一个总大小,每次往硬盘写文件,总大小超过阈值,就会将旧的文件删除。简单看下remove操作:

        // DiskLruCache 内部也是用LinkedHashMap
        private final LinkedHashMap<String, Entry> lruEntries =
            new LinkedHashMap<String, Entry>(0, 0.75f, true);
        ...
    
        public synchronized boolean remove(String key) throws IOException {
            checkNotClosed();
            validateKey(key);
            Entry entry = lruEntries.get(key);
            if (entry == null || entry.currentEditor != null) {
              return false;
            }
    
                //一个key可能对应多个value,hash冲突的情况
            for (int i = 0; i < valueCount; i++) {
              File file = entry.getCleanFile(i);
                //通过 file.delete() 删除缓存文件,删除失败则抛异常
              if (file.exists() && !file.delete()) {
                throw new IOException("failed to delete " + file);
              }
              size -= entry.lengths[i];
              entry.lengths[i] = 0;
            }
            ...
            return true;
      }
    

    可以看到 DiskLruCache 同样是利用LinkHashMap的特点,只不过数组里面存的 Entry 有点变化,Editor 用于操作文件。

    private final class Entry {
        private final String key;
    
        private final long[] lengths;
    
        private boolean readable;
    
        private Editor currentEditor;
    
        private long sequenceNumber;
        ...
    }
    

    4:防止OOM

    加载图片非常重要的一点是需要防止OOM,上面的LruCache缓存大小设置,可以有效防止OOM,但是当图片需求比较大,可能需要设置一个比较大的缓存,这样的话发生OOM的概率就提高了,那应该探索其它防止OOM的方法。

    4.1软引用

    回顾一下Java的四大引用:

    强引用: 普通变量都属于强引用,比如 private Context context;
    软应用: SoftReference,在发生OOM之前,垃圾回收器会回收SoftReference引用的对象。
    弱引用: WeakReference,发生GC的时候,垃圾回收器会回收WeakReference中的对象。
    虚引用: 随时会被回收,没有使用场景。

    简单理解就是:

    强引用对象的回收时机依赖垃圾回收算法,我们常说的可达性分析算法,当Activity销毁的时候,Activity会跟GCRoot断开,至于GCRoot是谁?这里可以大胆猜想,Activity对象的创建是在ActivityThread中,ActivityThread要回调Activity的各个生命周期,肯定是持有Activity引用的,那么这个GCRoot可以认为就是ActivityThread,当Activity 执行onDestroy的时候,ActivityThread 就会断开跟这个Activity的联系,Activity到GCRoot不可达,所以会被垃圾回收器标记为可回收对象。

    软引用的设计就是应用于会发生OOM的场景,大内存对象如Bitmap,可以通过 SoftReference 修饰,防止大对象造成OOM,看下这段代码。

        private static LruCache<String, SoftReference<Bitmap>> mLruCache = new LruCache<String, SoftReference<Bitmap>>(10 * 1024){
            @Override
            protected int sizeOf(String key, SoftReference<Bitmap> value) {
                //默认返回1,这里应该返回Bitmap占用的内存大小,单位:K
    
                //Bitmap被回收了,大小是0
                if (value.get() == null){
                    return 0;
                }
                return value.get().getByteCount() /1024;
            }
        };
    

    LruCache里存的是软引用对象,那么当内存不足的时候,Bitmap会被回收,也就是说通过SoftReference修饰的Bitmap就不会导致OOM。

    当然,这段代码存在一些问题,Bitmap被回收的时候,LruCache剩余的大小应该重新计算,可以写个方法,当Bitmap取出来是空的时候,LruCache清理一下,重新计算剩余内存;

    还有另一个问题,就是内存不足时软引用中的Bitmap被回收的时候,这个LruCache就形同虚设,相当于内存缓存失效了,必然出现效率问题。

    4.2. onLowMemory

    我们知道,当内存不足的时候,Activity、Fragment会调用onLowMemory方法,可以在这个方法里去清除缓存,Glide使用的就是这一种方式来防止OOM。

    //Glide
    public void onLowMemory() {
        clearMemory();
    }
    
    public void clearMemory() {
        // Engine asserts this anyway when removing resources, fail faster and consistently
        Util.assertMainThread();
        // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687.
        memoryCache.clearMemory();
        bitmapPool.clearMemory();
        arrayPool.clearMemory();
      }
    

    4.3 从Bitmap 像素存储位置考虑

    我们知道,系统为每个进程,也就是每个虚拟机分配的内存是有限的,早期的16M、32M,现在100+M。

    虚拟机的内存划分主要有5部分:

    虚拟机栈
    本地方法栈
    程序计数器
    方法区

    而对象的分配一般都是在堆中,堆是JVM中最大的一块内存,OOM一般都是发生在堆中。

    Bitmap 之所以占内存大不是因为对象本身大,而是因为Bitmap的像素数据, Bitmap的像素数据大小 = 宽 * 高 * 1像素占用的内存。

    1像素占用的内存是多少?不同格式的Bitmap对应的像素占用内存是不同的,具体是多少呢?在Fresco中看到如下定义代码。

      /**
       * Bytes per pixel definitions
       */
      public static final int ALPHA_8_BYTES_PER_PIXEL = 1;
      public static final int ARGB_4444_BYTES_PER_PIXEL = 2;
      public static final int ARGB_8888_BYTES_PER_PIXEL = 4;
      public static final int RGB_565_BYTES_PER_PIXEL = 2;
      public static final int RGBA_F16_BYTES_PER_PIXEL = 8;
    

    如果Bitmap使用 RGB_565 格式,则1像素占用 2 byte,ARGB_8888 格式则占4 byte。
    在选择图片加载框架的时候,可以将内存占用这一方面考虑进去,更少的内存占用意味着发生OOM的概率越低。 Glide内存开销是Picasso的一半,就是因为默认Bitmap格式不同。

    至于宽高,是指Bitmap的宽高,怎么计算的呢?看BitmapFactory.Options 的 outWidth。

    /**
         * The resulting width of the bitmap. If {@link #inJustDecodeBounds} is
         * set to false, this will be width of the output bitmap after any
         * scaling is applied. If true, it will be the width of the input image
         * without any accounting for scaling.
         *
         * <p>outWidth will be set to -1 if there is an error trying to decode.</p>
         */
        public int outWidth;
    

    看注释的意思,如果 BitmapFactory.Options 中指定 inJustDecodeBounds 为true,则为原图宽高,如果是false,则是缩放后的宽高。所以我们一般可以通过压缩来减小Bitmap像素占用内存。

    扯远了,上面分析了Bitmap像素数据大小的计算,只是说明Bitmap像素数据为什么那么大。那是否可以让像素数据不放在java堆中,而是放在native堆中呢?据说Android 3.0到8.0 之间Bitmap像素数据存在Java堆,而8.0之后像素数据存到native堆中,是不是真的?看下源码就知道了~

    8.0 Bitmap解析

    • java层创建Bitmap方法
        public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
                @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {
            ...
            Bitmap bm;
            ...
            if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) {
                //最终都是通过native方法创建
                bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null);
            } else {
                bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
                        d50.getTransform(), parameters);
            }
    
            ...
            return bm;
        }
    
    
    • JNI层
      Bitmap 的创建是通过native方法 nativeCreate。如下所示:
    //Bitmap.cpp
    static const JNINativeMethod gBitmapMethods[] = {
        {   "nativeCreate",             "([IIIIIIZ[FLandroid/graphics/ColorSpace$Rgb$TransferParameters;)Landroid/graphics/Bitmap;",
            (void*)Bitmap_creator },
    ...
    

    而JNI动态注册,nativeCreate 方法 对应 Bitmap_creator:

    //Bitmap.cpp
    static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                                  jint offset, jint stride, jint width, jint height,
                                  jint configHandle, jboolean isMutable,
                                  jfloatArray xyzD50, jobject transferParameters) {
        ...
        //1\. 申请堆内存,创建native层Bitmap
        sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap, NULL);
        if (!nativeBitmap) {
            return NULL;
        }
    
        ...
        //2.创建java层Bitmap
        return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));
    }
    

    主要两个步骤:

    • 申请内存,创建native层Bitmap,看下allocateHeapBitmap方法
    //Bitmap.cpp
    static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes,
            SkColorTable* ctable) {
        // calloc 是c++ 的申请内存函数
        void* addr = calloc(size, 1);
        if (!addr) {
            return nullptr;
        }
        return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable));
    }
    

    可以看到通过c++的 calloc 函数申请了一块内存空间,然后创建native层Bitmap对象,把内存地址传过去,也就是native层的Bitmap数据(像素数据)是存在native堆中。

    • 创建java 层Bitmap
    //Bitmap.cpp
    jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
            int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
            int density) {
        ...
        BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
         //通过JNI回调Java层,调用java层的Bitmap构造方法
        jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
                reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
                isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);
    
       ...
        return obj;
    }
    

    env->NewObject,通过JNI创建Java层Bitmap对象,gBitmap_class,gBitmap_constructorMethodID这些变量是什么意思,看下面这个方法,对应java层的Bitmap的类名和构造方法。

    //Bitmap.cpp
    int register_android_graphics_Bitmap(JNIEnv* env)
    {
        gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap"));
        gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J");
        gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZZ[BLandroid/graphics/NinePatch$InsetStruct;)V");
        gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V");
        gBitmap_getAllocationByteCountMethodID = GetMethodIDOrDie(env, gBitmap_class, "getAllocationByteCount", "()I");
        return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods,
                                             NELEM(gBitmapMethods));
    }
    

    8.0 的Bitmap创建就两个点:

    1. 创建native层Bitmap,在native堆申请内存。
    2. 通过JNI创建java层Bitmap对象,这个对象在java堆中分配内存。
      像素数据是存在native层Bitmap,也就是证明8.0的Bitmap像素数据存在native堆中。

    7.0 Bitmap
    直接看native层的方法:

    //JNI动态注册
    static const JNINativeMethod gBitmapMethods[] = {
        {   "nativeCreate",             "([IIIIIIZ)Landroid/graphics/Bitmap;",
            (void*)Bitmap_creator },
    ...
    
    static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                                  jint offset, jint stride, jint width, jint height,
                                  jint configHandle, jboolean isMutable) {
        ... 
        //1.通过这个方法来创建native层Bitmap
        Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
        ...
    
        return GraphicsJNI::createBitmap(env, nativeBitmap,
                getPremulBitmapCreateFlags(isMutable));
    }
    

    native层Bitmap 创建是通过GraphicsJNI::allocateJavaPixelRef,看看里面是怎么分配的, GraphicsJNI 的实现类是Graphics.cpp

    //JNI动态注册
    static const JNINativeMethod gBitmapMethods[] = {
        {   "nativeCreate",             "([IIIIIIZ)Landroid/graphics/Bitmap;",
            (void*)Bitmap_creator },
    ...
    
    static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                                  jint offset, jint stride, jint width, jint height,
                                  jint configHandle, jboolean isMutable) {
        ... 
        //1.通过这个方法来创建native层Bitmap
        Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
        ...
    
        return GraphicsJNI::createBitmap(env, nativeBitmap,
                getPremulBitmapCreateFlags(isMutable));
    }
    

    native层Bitmap 创建是通过GraphicsJNI::allocateJavaPixelRef,看看里面是怎么分配的, GraphicsJNI 的实现类是Graphics.cpp:

    android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                                 SkColorTable* ctable) {
        const SkImageInfo& info = bitmap->info();
    
        size_t size;
        //计算需要的空间大小
        if (!computeAllocationSize(*bitmap, &size)) {
            return NULL;
        }
    
        // we must respect the rowBytes value already set on the bitmap instead of
        // attempting to compute our own.
        const size_t rowBytes = bitmap->rowBytes();
        // 1\. 创建一个数组,通过JNI在java层创建的
        jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
                                                                 gVMRuntime_newNonMovableArray,
                                                                 gByte_class, size);
        ...
        // 2\. 获取创建的数组的地址
        jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
        ...
        //3\. 创建Bitmap,传这个地址
        android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
                info, rowBytes, ctable);
        wrapper->getSkBitmap(bitmap);
        // since we're already allocated, we lockPixels right away
        // HeapAllocator behaves this way too
        bitmap->lockPixels();
    
        return wrapper;
    }
    

    可以看到,7.0 像素内存的分配是这样的:

    1. 通过JNI调用java层创建一个数组
    2. 然后创建native层Bitmap,把数组的地址传进去。

    由此说明,7.0 的Bitmap像素数据是放在java堆的。当然,3.0 以下Bitmap像素内存据说也是放在native堆的,但是需要手动释放native层的Bitmap,也就是需要手动调用recycle方法,native层内存才会被回收。这个大家可以自己去看源码验证。

    native层Bitmap 回收问题
    Java层的Bitmap对象由垃圾回收器自动回收,而native层Bitmap印象中我们是不需要手动回收的,源码中如何处理的呢?
    先回忆一下下面三个关键字之间的关系。

    说说final、finally、finalize 的关系。三者除了长得像,其实没有半毛钱关系,final、finally大家都用的比较多,而 finalize 用的少,或者没用过,finalize 是 Object 类的一个方法,注释是这样的:

    /**
         * Called by the garbage collector on an object when garbage collection
         * determines that there are no more references to the object.
         * A subclass overrides the {@code finalize} method to dispose of
         * system resources or to perform other cleanup.
         * <p>
         ...**/
      protected void finalize() throws Throwable { }
    

    意思是说,垃圾回收器确认这个对象没有其它地方引用到它的时候,会调用这个对象的finalize方法,子类可以重写这个方法,做一些释放资源的操作。

    6.0 以前的Bitmap
    在6.0以前,Bitmap 就是通过这个finalize 方法来释放native层对象的。

    Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
                boolean isMutable, boolean requestPremultiplied,
                byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
            ...
            mNativePtr = nativeBitmap;
            //1.创建 BitmapFinalizer
            mFinalizer = new BitmapFinalizer(nativeBitmap);
            int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0);
            mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount);
    }
    
     private static class BitmapFinalizer {
            private long mNativeBitmap;
    
            // Native memory allocated for the duration of the Bitmap,
            // if pixel data allocated into native memory, instead of java byte[]
            private int mNativeAllocationByteCount;
    
            BitmapFinalizer(long nativeBitmap) {
                mNativeBitmap = nativeBitmap;
            }
    
            public void setNativeAllocationByteCount(int nativeByteCount) {
                if (mNativeAllocationByteCount != 0) {
                    VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
                }
                mNativeAllocationByteCount = nativeByteCount;
                if (mNativeAllocationByteCount != 0) {
                    VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
                }
            }
    
            @Override
            public void finalize() {
                try {
                    super.finalize();
                } catch (Throwable t) {
                    // Ignore
                } finally {
                    //2.就是这里了,
                    setNativeAllocationByteCount(0);
                    nativeDestructor(mNativeBitmap);
                    mNativeBitmap = 0;
                }
            }
        }
    

    在Bitmap构造方法创建了一个 BitmapFinalizer类,重写finalize 方法,在java层Bitmap被回收的时候,BitmapFinalizer 对象也会被回收,finalize 方法肯定会被调用,在里面释放native层Bitmap对象。

    6.0 之后做了一些变化,BitmapFinalizer 没有了,被NativeAllocationRegistry取代。

    例如 8.0 Bitmap构造方法。

        Bitmap(long nativeBitmap, int width, int height, int density,
                boolean isMutable, boolean requestPremultiplied,
                byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    
            ...
            mNativePtr = nativeBitmap;
            long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
            //  创建NativeAllocationRegistry这个类,调用registerNativeAllocation 方法
            NativeAllocationRegistry registry = new NativeAllocationRegistry(
                Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
            registry.registerNativeAllocation(this, nativeBitmap);
        }
    

    NativeAllocationRegistry 就不分析了, 不管是BitmapFinalizer 还是NativeAllocationRegistry,目的都是在java层Bitmap被回收的时候,将native层Bitmap对象也回收掉。 一般情况下我们无需手动调用recycle方法,由GC去盘它即可。

    上面分析了Bitmap像素存储位置,我们知道,Android 8.0 之后Bitmap像素内存放在native堆,Bitmap导致OOM的问题基本不会在8.0以上设备出现了(没有内存泄漏的情况下),那8.0 以下设备怎么办?赶紧升级或换手机吧

    我们换手机当然没问题,但是并不是所有人都能跟上Android系统更新的步伐,所以,问题还是要解决。

    Fresco 之所以能跟Glide 正面交锋,必然有其独特之处,文中开头列出 Fresco 的优点是:“在5.0以下(最低2.3)系统,Fresco将图片放到一个特别的内存区域(Ashmem区)” 这个Ashmem区是一块匿名共享内存,Fresco 将Bitmap像素放到共享内存去了,共享内存是属于native堆内存。

    Fresco 关键源码在 PlatformDecoderFactory 这个类:

    public class PlatformDecoderFactory {
    
      /**
       * Provide the implementation of the PlatformDecoder for the current platform using the provided
       * PoolFactory
       *
       * @param poolFactory The PoolFactory
       * @return The PlatformDecoder implementation
       */
      public static PlatformDecoder buildPlatformDecoder(
          PoolFactory poolFactory, boolean gingerbreadDecoderEnabled) {
        //8.0 以上用 OreoDecoder 这个解码器
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
          int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
          return new OreoDecoder(
              poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads));
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
          //大于5.0小于8.0用 ArtDecoder 解码器
          int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
          return new ArtDecoder(
              poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads));
        } else {
          if (gingerbreadDecoderEnabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            //小于4.4 用 GingerbreadPurgeableDecoder 解码器
            return new GingerbreadPurgeableDecoder();
          } else {
            //这个就是4.4到5.0 用的解码器了
            return new KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool());
          }
        }
      }
    }
    

    8.0 先不看了,看一下 4.4 以下是怎么得到Bitmap的,看下GingerbreadPurgeableDecoder这个类有个获取Bitmap的方法:

    //GingerbreadPurgeableDecoder
    private Bitmap decodeFileDescriptorAsPurgeable(
          CloseableReference<PooledByteBuffer> bytesRef,
          int inputLength,
          byte[] suffix,
          BitmapFactory.Options options) {
        //  MemoryFile :匿名共享内存
        MemoryFile memoryFile = null;
        try {
          //将图片数据拷贝到匿名共享内存
          memoryFile = copyToMemoryFile(bytesRef, inputLength, suffix);
          FileDescriptor fd = getMemoryFileDescriptor(memoryFile);
          if (mWebpBitmapFactory != null) {
            // 创建Bitmap,Fresco自己写了一套创建Bitmap方法
            Bitmap bitmap = mWebpBitmapFactory.decodeFileDescriptor(fd, null, options);
            return Preconditions.checkNotNull(bitmap, "BitmapFactory returned null");
          } else {
            throw new IllegalStateException("WebpBitmapFactory is null");
          }
        } 
      }
    

    总结一下,你会发现,4.4以下,Fresco 使用匿名共享内存来保存Bitmap数据,首先将图片数据拷贝到匿名共享内存中,然后使用Fresco自己写的加载Bitmap的方法。

    Fresco对不同Android版本使用不同的方式去加载Bitmap,至于4.4-5.0,5.0-8.0,8.0 以上,对应另外三个解码器,大家可以从PlatformDecoderFactory 这个类入手,自己去分析,思考为什么不同平台要分这么多个解码器,8.0 以下都用匿名共享内存不好吗?期待你在评论区跟大家分享?

    5. 内存泄露

    ImageView导致的内存泄露
    Glide的做法是监听生命周期回调,看 RequestManager 这个类。

    public void onDestroy() {
        targetTracker.onDestroy();
        for (Target<?> target : targetTracker.getAll()) {
          //清理任务
          clear(target);
        }
        targetTracker.clear();
        requestTracker.clearRequests();
        lifecycle.removeListener(this);
        lifecycle.removeListener(connectivityMonitor);
        mainHandler.removeCallbacks(addSelfToLifecycle);
        glide.unregisterRequestManager(this);
      }
    

    在Activity/fragment 销毁的时候,取消图片加载任务,细节大家可以自己去看源码。

    6.列表加载问题

    6.1图片错乱

    由于RecyclerView或者LIstView的复用机制,网络加载图片开始的时候ImageView是第一个item的,加载成功之后ImageView由于复用可能跑到第10个item去了,在第10个item显示第一个item的图片肯定是错的。

    常规的做法是给ImageView设置tag,tag一般是图片地址,更新ImageView之前判断tag是否跟url一致。

    当然,可以在item从列表消失的时候,取消对应的图片加载任务。要考虑放在图片加载框架做还是放在UI做比较合适。

    6.2 线程池任务过多

    列表滑动,会有很多图片请求,如果是第一次进入,没有缓存,那么队列会有很多任务在等待。所以在请求网络图片之前,需要判断队列中是否已经存在该任务,存在则不加到队列去。

    五:总结

    通过对Glide分析,分析出一个图片加载框架必要的需求,以及各个需求涉及到哪些技术和原理。

    1. 异步加载:最少两个线程池
    2. 切换到主线程:Handler
    3. 缓存:LruCache、DiskLruCache,涉及到LinkHashMap原理
    4. 防止OOM:软引用、LruCache、图片压缩没展开讲、Bitmap像素存储位置源码分析、Fresco部分源码分析
    5. 内存泄露:注意ImageView的正确引用,生命周期管理
    6. 列表滑动加载的问题:加载错乱用tag、队满任务存在则不添加
    展开全文
  • glide溯源

    千次阅读 2018-10-10 11:16:26
    最近有时间看了glide的源码,于是想写一下关于glide源码的一些解释和说明,一方面供后来者更好的去阅读glide源码,一方面也为自己的学习做一个记录,本次glide源码分析基于glide4.8.0版本(下载地址),以下是glide...
  • Glide ModulesGlide module 是一个抽象方法,全局改变 Glide 行为的一个方式。如果你需要访问 GlideBuilder,它要在你要做的地方创建 Glide 实例,这是要做的一种方法。为了定制 Glide,你需要去实现一个 ...
  • Glide原理

    千次阅读 2019-05-28 13:14:29
    Glide是google推荐的图片加载框架 使用简单: Glide.with(this).load(url).into(imageView); 加载流程总结如下: 1)首先,Glide.with(activity)方法得到RequestManager类,这个类的主要作用是将glide 与 整个...
  • Glide命令,如何使用glideglide.lock

    千次阅读 2017-02-13 17:24:01
    Desc:10分钟了解glide相关命令,做好包管理准备以下是Glide命令,其中大部分命令是帮助您管理工作区。glide create (别名 init)初始化新工作区。除此之外,这会创建一个glide.yaml文件,同时试图猜测包和版本。例如...
  • Glide 3到Glide 4

    千次阅读 2018-05-31 16:39:07
    基本用法:和Glide3一样Glide.with(this).load(url).into(img);设置占位图、缓存等参数:RequestOptions options = new ReqeustOptions() .placeholder(R,mipmap.loading) .error(R.mipmap.error) ....
  • Glide系列(一) — Glide 框架结构浅析

    万次阅读 2020-06-05 21:52:50
    Glide 是一个图片加载库,跟它同类型的库还有 Picasso、Fresco、Universal-Image-Loader 等。 Glide 框架的优点如下: 1. 加载类型多样化:Glide 支持 Gif、WebP 等格式的图片。 2. 生命周期的绑定:图片请求与页面...
  • Glide使用详解(一)

    万次阅读 多人点赞 2016-04-12 12:25:23
    Glide使用 Glide使用教程 Glide详解 Android图片加载 一. 下载在build.gradle中添加依赖: compile 'com.github.bumptech.glide:glide:3.7.0'需要support-v4库的支持,如果你的项目没有support-v4库(项目默认已经...
  • Glide-通过Modules定制Glide

    万次阅读 2017-02-15 13:08:36
    前言:我们一般情况下使用Glide都很简单,只用简单的调用几个方法就能够很好的显示图片了,但其实Glide在初始化的时候进行了一系列的默认配置,比如缓存的配置,图片质量的配置等等.接下来我们就介绍一下一个比较高级的...
  • 我想大多数人在自己的项目中还是使用Glide3.7.1这个版本吧!不过Glide版本现在已经到4.4.0了! 当我们把Glide3更换成Glide4,会发现大部分地方都报错了,那么该怎样快速替换Glide3为Glide4呢? 下面我们一起来看看...
  • 在Android开发过程中,我们常常需要涉及大量的图片加载,图片加载框架设计,是Android高级开发工程师必备的技能,本节将通过分析Glide图片加载框架,来学习如何设计一个图片加载框架。 注意:本节所使用的Glide版本...
  • Glide 简介

    2017-07-20 17:51:01
    Google在2014的开发者论坛上,向我们介绍了一个开源的图片加载的类库Glide。作者BumpTech(碰撞科技)。这个 库被广泛的运用在了Google的开源项目中。 能够被Google如此推崇一定有其过人之处。研究一下发现picaso...
  • Glide混淆设置

    2020-12-25 13:45:43
    ##Glide -dontwarn com.bumptech.glide.** -keep class com.bumptech.glide.**{*;} -keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * extends ...
  • Glide SimpleTarget 过时

    千次阅读 2020-12-11 15:05:21
    想利用Glide来获取图片的宽高 发现SimpleTarget方法已经过时了,百度了半天也没找到替代方法… 当前使用的Glide版本为: com.github.bumptech.glide:glide:4.11.0 SimpleTarget 已经过时了… 代替方法,此方法可以...
  • Glide 4.3.0 jar包下载

    2017-11-04 14:25:45
    包括glide-3.5.0.jar,glide-3.6.0.jar,glide-3.6.1.jar,glide3.7.0.jar,glide-full-4.0.0.jar,glide-full-4.3.0.jar
  • Glide源码解析06-Glide流程图
  • Glide框架

    千次阅读 2017-05-20 15:12:37
    Glide是一个面向Android快速和高效的异步图像加载框架。
  • Glide使用总结

    千次阅读 2020-05-07 00:26:41
    implementation 'com.github.bumptech.glide:glide:4.5.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.5.0' 之后添加访问网络权限 <uses-permission android:name="android.permission.INTER...
  • Glide使用

    千次阅读 2016-06-02 15:51:41
    1.compile 'com.github.bumptech.glide:glide:3.7.0'//图形处理框架,...compile 'jp.wasabeef:glide-transformations:2.0.1' // If you want to use the GPU Filters compile 'jp.co.cyberagent.android.gpuimage
  • Glide禁用缓存

    千次阅读 2020-06-02 16:01:41
    Glide反复加载同一个路径的图片,发现图片变化后,但是Glide重新加载后,图片却没有变化. 解决方案 禁用Glide缓存. Glide.with(this) .load(imageUri) .skipMemoryCache(true) .diskCacheStrategy...
  • Glide 缓存原理实现

    万次阅读 2020-03-24 21:29:28
    Glide 缓存原理实现 专注于Android开发,分享经验总结,欢迎加入 Glide使用方式如下: Glide.with(MainActivity.this) .load(path) .into(iv); Glide缓存分为:活动缓存、内存缓存、Bitmap复用池、磁盘缓存...
  • Glide的RequestOptions

    千次阅读 2020-09-16 10:47:18
    6. Glide的RequestOptions 6.1 RequestOptions的API: 官方API:https://muyangmin.github.io/glide-docs-cn/javadocs/400/com/bumptech/glide/request/RequestOptions.html 参考设置:...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 20,456
精华内容 8,182
关键字:

glide