图片、glide优化

图片优化

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

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

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

二、图片压缩的分类

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

三、Android6.0与 7.0压缩 JPEG 图片的区别

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

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

若发生OOM,则使用try catch将其捕获,然后清除图片cache,尝试降低bitmap format
由于webview存在内存系统泄漏,还有图库占用内存过多的问题,可以采用单独的进程。

Chrome Devtool 在浏览器调试H5

图片:html用webview加载了也就10几行代码,古董机也可以加载出来

view 的大小比图片小如何优化

PNG 图片部分:
我们修改 imageview 的大小并不会影响 PNG 图片占用内存大小

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

总结

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

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

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

  • 对于一些低端设备,我们可以将图片格式从 ARGB_8888 变为 RGB_565,这样一个简单的调整,可以让图片内存的占用减少一半;又例如在适当的时机,主动回收掉一些图片缓存

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

  • 不使用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;
}
}

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)

IcaChe 中存的是 Btmap 对象, reusepool 中存的是 bitmap 的对象可以复用, 不必再创建。

超长图

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

重复图片监控

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

三级缓存

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

GIF

还有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

  • inBitmap是在BitmapFactory中的内部类Options的一个变量,简单而言,使用该变量可以复用旧的Bitmap的内存而不用重新分配以及销毁旧Bitmap,进而改善运行效率。
  • 4.4之前的版本inBitmap只能够重用相同大小的Bitmap内存区域。简单而言,被重用的Bitmap需要与新的Bitmap规格完全一致,否则不能重用。
  • 4.4之后的版本系统不再限制旧Bitmap与新Bitmap的大小,只要保证旧Bitmap的大小是大于等于新Bitmap大小即可。
  • 除上述规则之外,旧Bitmap必须是mutable的,这点也很好理解,如果一个Bitmap不支持修改,那么其内存自然也重用不了
  • Glide内部也使用了inBitmap作为缓存复用的一种方式。
    unknown_filename.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private Set<WeakReference<Bitmap>> reusablePool;

// 复用池reusablePool = Collections.synchronizedSet(new
HashSet<WeakReference<Bitmap>>());

public Bitmap getReusable(int w, int h, int inSampleSize) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return null;
}
Bitmap reusable = null;
//有满足的就拿出来
Iterator<WeakReference<Bitmap>> iterator = reusablePool.iterator();
while (iterator.hasNext()) {
Bitmap bitmap = iterator.next().get();
if (bitmap != null) {
if (checkInBitmap(bitmap, w, h, inSampleSize)) {
reusable = bitmap;
iterator.remove();
break;
}
} else {
iterator.remove();
}
}

return reusable;

}

三级缓存

lruCache,磁盘缓存、bitmap复用

Glide 也支持异步加载 Bitmap,异步加载,就涉及到线程的切换问题
我们也通过 Glide 加载一个图片资源,然后获得缓存的图片文件。其实只需要将 asBitmap() 换成 asFile() 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
Glide.with(activity)
.asBitmap()
.load(imageUrl)
.into(object:CustomTarget<Bitmap>(){
override fun onLoadCleared(placeholder: Drawable?) {
}

override fun onResourceReady(resource: Bitmap,
transition: Transition<in Bitmap>?) {
val loadBitmap = resource
}
})

尺寸压缩

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
6
7
8
// 将图片保存在本地,
bitmap.compress(CompressFormat.JPEG, 100,
new FileOutputStream(file));//100是质量
Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));//decodeStream放的是输入输出流
return bitmap;

Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
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

图片、glide优化
http://peiniwan.github.io/2024/04/502127e875f1.html
作者
六月的雨
发布于
2024年4月6日
许可协议