4.图片、glide优化

图片优化

[[7-图片低配系统 oom 优化]]

一、图片压缩的意义是什么

1.节约流量、2.降低服务器带宽、3.降低 app 内存占用

二、图片压缩的分类

1.质量压缩(图片存储卡大小)
2.尺寸压缩 (内存)

如果是一张的话压缩处理,大量图片的话用 lru

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024)

  • 若发生OOM,则使用try catch将其捕获,然后清除图片cache,尝试降低bitmap format
  • 图库占用内存过多也可以采用单独的进程。
  • html用webview加载了也就10几行代码,古董机也可以加载出来

view 的宽高比图片小如何优化

PNG 图片部分:
我们修改 imageview 的大小并不会影响 PNG 图片占用内存大小:宽 X 高 X 像素占用字节

SVG 矢量图部分:
我们修改 imageview 的大小并不会SVG影响图片占用内存大小,需要注意的是 SVG 图片实际的显示分辨率是由SVG 的 xml文件中的 width/height 决定的,所以SVG 既然支持缩放,那我们就把宽高写小了好了,这样可以省内存。

总结

  • 图片压缩大概分为两类,质量压缩和尺寸压缩。例如一些固定宽高的图片,feed,头像等等就可以对尺寸压缩,或者指定宽高的图片(oss、svg)。修改图片的格式、质量、编码格式。在内存不足的时候可以清除内存缓存,在滚动的时候可以暂停图片加载,停下来的时候再去加载。

  • glide也可以裁剪(请求属性不申请内存)。

  • onTrimMemory,调用 Glide.cleanMemroy() 清理掉所有的内存缓存。(内部是LruBitmapPool )

  • 升级到 Glide4.0,使用 asDrawable 代替 asBitmap,drawable 更省内存。(或者下载下来,用空间换时间)

  • 对于一些低端设备,我们可以将图片格式从 ARGB_8888 变为 RGB_565,可以让图片内存的占用减少一半

    低端机判断:
    总内存小、Cpu (1.5G 低端)、SDK

1
2
3
4
5
6
7
8
9
10
11
12
if (view.getContext() != null) {
switch (scrollState) {
case SCROLL_STATE_IDLE:
Glide.with(view.getContext()).resumeRequests();
break;
case SCROLL_STATE_TOUCH_SCROLL:
case SCROLL_STATE_FLING:
Glide.with(view.getContext()).pauseRequests();
break;
}
}

createBitmap

Bitmap.createBitmap () 方法,从名字上就可以看出,它是为了创建一个 Bitmap 对象,绘制的时候用。
利用 Glide 的来优化此步骤,就需要用到 BitmapPool。BitmapPool 本身是一个接口,我们通常会使用到它的实现类 LruBitmapPool,从名称就可以看出,它基于 LRU 的规则,在一定的内存限制下,缓存和管理一些可供重用的 Bitmap 对象。不必重新创建。

1
2
3
4
5
6
7
8
9
val bitmapPool = Glide.get(this).bitmapPool
val bitmap = bitmapPool.get(100,100,Bitmap.Config.ARGB_8888)
if (bitmap==null) {
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
}
// 处理 → 使用 bitmap
// ......
// 用完回收 bitmap
bitmapPool.put(bitmap)

超长图

对于不失真的超长图,超大图,可以使用系统的API BitmapRegionDecoder ,它可以用来显示图片指定的一个矩形区域。通过自定义View重写onTouchEvent()判断手势滑动位置,从而改变矩形区域的位置,重新计算mRect(ruai kte)去获取部分bitmap绘制到屏幕上。invidedata刷新ondraw。
可以用开源库:subsampling-scale-image-view

重复图片监控

重复图片指的是 Bitmap 的像素数据完全一致,但是有多个不同的对象存在。这个监控不需要太多的样本量,一般只在内部使用。之前我实现过一个内存 Hprof 的分析工具,它可以自动将重复 Bitmap 的图片和引用链输出。下图是一个简单的例子,你可以看到两张图片的内容完全一样,通过解决这张重复图片可以节省 1MB 内存
unknown_filename.5

三级缓存

  • 先读取内存缓存, 因为优先加载, 速度最快,内存缓存没有再读取本地缓存, 次优先加载, 速度也快,本地没有再加载网络缓存, 速度慢,浪费流量在网络缓存中从网络下载图片,并且保存在本地和内存中,在下载的时候可以对图片进行压缩
  • 服务器端下载的图片是使用 Http的缓存机制,每次执行将本地图片的时间发送给服务器,如果俩次访问的时间间隔短,返回码是 304,会读取网络缓存(说明服务端的图片和本地的图片是相同的,直接使用本地保存的图片),如果返回码是 200,则开始下载新的图片并实现缓存。在从服务器获取到图片后,需要再在本地和内存中分别存一份,这样下次直接就可以从内存中直接获取了,这样就加快了显示的速度,提高了用户的体验。

GIF

FrameSequence是Android framework中里的一个工具包。 它封装了: libgif (gif编解码库c++ ),并提供Java API播放gif。使用它要比glide加载GIF效果效果要好,glide加载加载GIF图片CPU占用高,并且内存占用一直在增加。使用FrameSequence库就可以解决,不过每次判断去怎么加载很麻烦,可以使用glide的AppGlideModule apt注解解析器来自动生成代码。判断GIF图片时将InputStream转成FrameSequenceDrawable解析。这个还可以做播放Webp动画。

其他

自定义GlideModule。设置MemoryCache和BitmapPool大小,在未复用的情况下,每张图片都需要一块内存。而使用复用的时候,如果存在能被复用的图片会重复使用该图片的内存。所以复用不能减少程序正在使用的内存大小,而是解决了频繁申请内存导致的内存抖动、碎片等问题。

优化bitmap

inBitmap

  • inBitmapBitmapFactory.Options 中的一个参数
  • 作用:复用已有 Bitmap 的内存,而不是重新分配和回收内存
  • 优点:
    • 减少内存分配
    • 降低 GC 频率
    • 提升图片加载性能

二、使用 inBitmap 的基本套路(两步走)

  1. 先解码一张可复用的 Bitmap
  2. 后续解码时通过 inBitmap 复用这块内存

最简单示例(Android 4.4+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 解码第一张 Bitmap,作为可复用内存
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true; // 必须设置为 true
Bitmap reusableBitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.img_1, options
);

// 2. 使用 inBitmap 复用内存解码第二张 Bitmap
BitmapFactory.Options reuseOptions = new BitmapFactory.Options();
reuseOptions.inMutable = true;
options.inSampleSize = 2; // 图片缩放采样
reuseOptions.inBitmap = reusableBitmap;

Bitmap bitmap2 = BitmapFactory.decodeResource(
getResources(), R.drawable.img_2, reuseOptions
);

Android 版本差异

Android 4.4 之前(API < 19)

  • inBitmap 复用条件非常严格
  • 旧 Bitmap 和新 Bitmap 必须:
    • 宽度相同
    • 高度相同
    • Bitmap. Config 相同

结论:规格必须完全一致,否则无法复用


Android 4.4 之后(API ≥ 19)

  • 系统放宽了 inBitmap 的限制
  • 只要满足:
    • 旧 Bitmap 占用的内存大小 ≥ 新 Bitmap 所需内存大小

示例:

  • 大图 → 小图 ✅
  • 小图 → 大图 ❌

常见崩溃点(高频面试点)

忘记设置 inMutable

1
options.inMutable = true;

IllegalArgumentException: Problem decoding into existing bitmap

inBitmap 通过复用已存在 Bitmap 的内存,减少内存分配和 GC;Android 4.4 之后只要旧 Bitmap 的内存大于等于新 Bitmap 即可复用,但必须设置 inMutable。

尺寸压缩

options.insamplesize = calculateInsampleSize(options,reqWidth: 100,reqHeight: 100); //算出采样率(临近采样压缩算法)
unknown_filename.4
一般会这两种算法一起用压缩图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Bitmap compressBySampleSize(final Bitmap src,
final int maxWidth,
final int maxHeight,
final boolean recycle) {
if (isEmptyBitmap(src)) return null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
src.compress(CompressFormat.JPEG, 100, baos);
byte[] bytes = baos.toByteArray();
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
options.inJustDecodeBounds = false;
if (recycle && !src.isRecycled()) src.recycle();
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
}

质量压缩

bitmap.compress时可以指定它的的格式和图片质量
webp<jpeg<png(无损压缩)
unknown_filename.2

1
2
3
4
5
// 将图片保存在本地,
bitmap.compress(CompressFormat.JPEG, 100,
new FileOutputStream(file));//100是质量
Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));//decodeStream放的是输入输出流
return bitmap;

加载大图片

正常一张720x1080的图片在内存中占多少空间?怎么加载大图片?如何面对大的 bitmap如何处理?

  1. 看他的渲染方式,占用的字节不同的。RGB888每个像素占用4个字节,很容易内存溢出
  2. 图片的总大小 = 图片的总像素 * 每个像素占用的大小
  3. 计算机把图片所有像素信息全部解析出来,保存至内存,很容易内存溢出

加载大图片
可以对图片的宽高和质量进行压缩,步骤:
获取屏幕宽高,获取图片宽高,图片的宽高除以屏幕宽高,算出宽和高的缩放比例,取较大值作为图片的缩放比例,且大于1才缩放,然后按缩放比例加载图片
获取屏幕宽高 、获取图片宽高
//请求图片属性但不申请内存
opts.inJustDecodeBounds = true;

在不失真的条件下显示一张超高清的图片或者长图?
针对这个问题,我自己一般用以下两种方法解决:

  1. 使用WebView来加载该图片;
  2. 网络大图:Http协议中的Range / Accept-Range ,他可以指定获取body的一部分,从而达到分块加载图片的效果;
  3. subsampling-scale-image-view(底层就是BitmapRegionDecoder)
  4. BitmapRegionDecoder 可以用来显示图片指定的一个矩形区域。通过自定义View重写onTouchEvent()判断手势滑动位置,从而改变矩形区域的位置,重新计算mRect(ruai k te)去获取部分bitmap绘制到屏幕上。invidedata刷新ondraw
1
2
3
4
5
6
7
@Override 
protected void onDraw(Canvas canvas) {
BitmapRegionDecoder,options就是BitmapFactory.Options
Bitmap bm = mDecoder.decodeRegion(mRect, options);
canvas.drawBitmap(bm, 0, 0, null);
}

https://blog.csdn.net/lmj623565791/article/details/49300989/

GestureDetector 方便点

Glide

glide 问题

使用Glide时,注意对传入的Acticity与Fragment进行判断,避免传入已经销毁Acticity,造成IllegalArgumentException异常。可以参考这篇Glide类似You cannot start a load for a destroyed activity异常简单分析

优化GIF

FrameSequence是Android framework中里的一个工具包。 它封装了: giflib (gif编解码库c++ ),并提供Java API播放gif。使用它要比glide加载GIF效果效果要好,glide加载加载GIF图片CPU占用高,并且内存占用一直在增加。使用FrameSequence库就可以解决,不过每次判断去怎么加载很麻烦,可以使用glide的AppGlideModule apt注解解析器来自动生成代码。判断GIF图片时将InputStream转成FrameSequenceDrawable解析。这个还可以做播放Webp动画。

1
2
3
4
5
6
7
8
9
10
11
@GlideModule
public class GifGlideModule extends AppGlideModule {
@Override
public void registerComponents(
@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
super.registerComponents(context, glide, registry);
registry.append(Registry.BUCKET_GIF,
InputStream.class,
FrameSequenceDrawable.class, new GifDecoder(glide.getBitmapPool()));
}
}

Glide加载Gif图片的原理:将gif根据每一帧解析成很张图片,然后在依次设置给ImageView。

FrameSequenceDrawable原理
利用了两个Bitmap对象,其中一个用于绘制到屏幕上,另外一个用于解析下一张要展示的图片,利用了HandlerThread在子线程解析,每次解析的时候获取上一张图片的展示时间,然后使用Drawable自身的scheduleSelf方法在指定时间替换图片,在达到替换时间时,会调用draw方法,在draw之前先去子线程解析下一张要展示的图片,然后重复这个步骤,直到播放结束或者一直播放。

需要自己编译

unknown_filename.3

bitmap 复用

unknown_filename.1

加载进度需要自己实现

Glide默认缓存
内存缓存最大空间(maxSize)=每个进程可用的最大内存 * 0.4
磁盘缓存大小: 250 * 1024 * 1024(250MB)

1
int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;

磁盘缓存目录磁盘缓存目录: 项目/cache/image_manager_disk_cache

1
String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache";

优化方案

几乎所有的 OOM 错误都是因为宿主应用出了问题,而不是 Glide 本身。 应用里两种常见的 OOM 错误分别是:
过大的内存分配 (Excessively large allocations)
内存泄露(Memory leaks, 被分配的内存没有被释放)

  • 引入largeHeap属性,让系统为App分配更多的独立内存。
  • 禁止Glide内存缓存。设置skipMemoryCache(true)。内存缓存是CPU,速度更快。
  • 自定义GlideModule。设置MemoryCache和BitmapPool大小。
  • 升级到Glide4.0,使用asDrawable代替asBitmap,drawable更省内存。(或者下载下来,用空间换时间)
     Bitmap 储存的是 像素信息(把所有信息都保存下来了,一个像素4个字节,想想有多大),Drawable 储存的是 对 Canvas 的一系列操作。而 BitmapDrawable 储存的是「把 Bitmap 渲染到 Canvas 上」这个操作。
  • ImageView的scaleType为fitXY时,改为fitCenter/centerCrop/fitStart/fitEnd显示。
  • 不使用application作为context。当context为application时,会把imageView是生命周期延长到整个运行过程中,imageView不能被回收,从而造成OOM异常。
  • 当列表在滑动的时候,调用Glide的pauseRequests()取消请求,滑动停止时,调用resumeRequests()恢复请求。
1
2
3
4
5
6
7
8
9
10
11
12
if (view.getContext() != null) {
switch (scrollState) {
case SCROLL_STATE_IDLE:
Glide.with(view.getContext()).resumeRequests();
break;
case SCROLL_STATE_TOUCH_SCROLL:
case SCROLL_STATE_FLING:
Glide.with(view.getContext()).pauseRequests();
break;
}
}

  • Try catch某些大内存分配的操作。考虑在catch里面尝试一次降级的内存分配操作。例如decode bitmap的时候,catch到OOM,可以尝试把采样比例再增加一倍之后,再次尝试decode。
  • BitmapFactory.Options和BitmapFactory.decodeStream获取原始图片的宽、高,绕过Java层加载Bitmap,再调用Glide的override(width,height)控制显示。(??)
  • onTrimMemory,调用 Glide.cleanMemroy() 清理掉所有的内存缓存。(内部是LruBitmapPool )
  • 如果是处于 lowMemory 的时候,将图片的 DecodeFormat 设置为 RGB_565
  • 使用 glide 自己的圆角图片。bitmapTransfrom

4.图片、glide优化
http://peiniwan.github.io/2025/12/8d549286cbae.html
作者
六月的雨
发布于
2025年12月16日
许可协议