盒子
盒子
文章目录
  1. 原理解析
  2. 主动监测
  3. 被动监测
    1. 在第三方图片加载库回调中进行大图监测
    2. 在网络加载图片时进行大图监测
  4. 使用ASM插桩进行大图监控
  5. 注意事项与优化技巧
  6. 总结
  7. 推荐

Android大图监测的这三种实现方式,你最喜欢哪种?

Android应用中,大图的加载和显示可能导致内存占用过高,进而引发OOM(Out Of Memory)异常,影响应用的稳定性和用户体验。为了更好地管理大图资源,我们需要建立起一套可靠的大图监测系统。

原理解析

  1. 内存占用计算

首先,我们需要了解如何计算一张图片在内存中的占用大小。Android中,图片占用的内存主要由其宽、高和每个像素的位数决定。我们可以使用以下公式计算:

[ 内存占用大小 = 宽 \times 高 \times 像素位数 / 8 ]

  1. 大图判定标准

一般情况下,大图的定义是指超过一定阈值的图片。这个阈值可以根据应用的实际需求来设定,通常建议根据设备的内存情况和应用场景动态调整。

  1. 监测策略

大图监测一般采用两种策略:主动监测被动监测。主动监测通过周期性地扫描内存中的图片资源,识别大图,进行处理。而被动监测则是在图片加载过程中实时判断是否为大图。

主动监测

主动监测只要获取到内存中的图片资源,通过扫描判断是否超过设置的阈值即可。

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
class LargeImageScanner {

fun scanLargeImages() {
// 遍历内存中的图片资源
for (image in MemoryManager.getAllImages()) {
val imageSize = calculateImageSize(image)

// 判断是否为大图
if (imageSize > LARGE_IMAGE_THRESHOLD) {
// 进行处理,如压缩、裁剪或异步加载
handleLargeImage(image)
}
}
}

private fun calculateImageSize(image: Bitmap): Int {
// 计算图片占用的内存大小
return image.width * image.height * (image.config.bitsPerPixel / 8)
}

private fun handleLargeImage(image: Bitmap) {
// 实现大图的处理逻辑,例如压缩、裁剪或异步加载
// ...
}
}

被动监测

被动监测的目的是,让图在加载的过程中,自动获取到加载图片的大小。所以切入的时机就非常重要。

在第三方图片加载库回调中进行大图监测

如果你使用的是第三方图片加载库Glide,最简单的直接的是在图片加载的成功的时机进行监测。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class GlideImageLoader {

fun loadWithLargeImageCheck(context: Context, url: String, target: ImageView) {
Glide.with(context)
.asBitmap()
.load(url)
.listener(object : RequestListener<Bitmap> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Bitmap>?,
isFirstResource: Boolean
): Boolean {
// 图片加载失败处理
// ...
return false
}

override fun onResourceReady(
resource: Bitmap?,
model: Any?,
target: Target<Bitmap>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
// 图片加载成功,检查是否为大图
resource?.let {
val imageSize = calculateImageSize(it)
if (imageSize > LARGE_IMAGE_THRESHOLD) {
// 处理大图逻辑,如压缩、裁剪或异步加载
handleLargeImage(it)
}
}
return false
}
})
.into(target)
}

private fun calculateImageSize(image: Bitmap): Int {
// 计算图片占用的内存大小
return image.width * image.height * (image.config.bitsPerPixel / 8)
}

private fun handleLargeImage(image: Bitmap) {
// 实现大图的处理逻辑,例如压缩、裁剪或异步加载
// ...
}
}

但上面这种方式存在几个弊端

  1. 适用性低,强制要求所以图片加载都要调用loadWithLargeImageCheck方法,如果是一个现有的大项目,将无法改造。
  2. 强依赖于第三方加载库Glide,后续换库也不兼容

所以为了解决上面的这几个问题,我们要想的是,能否不依赖于第三方图片加载库呢?

于是就有了下面这种方式

在网络加载图片时进行大图监测

现在使用网络请求基本都是使用Okhttp,在这种情况下,你可以考虑使用拦截器(Interceptor)来实现通用的大图监测逻辑。拦截器是OkHttp 中的一种强大的机制,可以在请求发起和响应返回的过程中进行拦截、修改和监测。

以下是一个使用OkHttp拦截器进行大图监测的示例:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException

class LargeImageInterceptor : Interceptor {

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

// 发起请求前的处理,可以在这里记录请求时间等信息

val response = chain.proceed(request)

// 请求返回后的处理
if (response.isSuccessful) {
val contentType = response.body()?.contentType()?.toString()

// 检查是否为图片资源
if (contentType?.startsWith("image/") == true) {
// 获取图片大小并进行大图监测
val imageSize = calculateImageSize(response.body()?.byteStream())
if (imageSize > LARGE_IMAGE_THRESHOLD) {
// 处理大图逻辑,如压缩、裁剪或异步加载
handleLargeImage()
}
}
}

return response
}

private fun calculateImageSize(inputStream: InputStream?): Int {
// 通过输入流计算图片占用的内存大小
// ...
}

private fun handleLargeImage() {
// 实现大图的处理逻辑,例如压缩、裁剪或异步加载
// ...
}
}

然后,在创建OkHttpClient时,添加这个拦截器:

1
2
3
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(LargeImageInterceptor())
.build()

通过这种方式,你只需要在OkHttp中添加一次拦截器,即可在每个图片请求中进行通用的大图监测处理,而不用在每个请求的响应回调中添加监测代码。这样使得代码更加清晰、易于维护。

可能又有人会说,我网络加载库换了,那不是一样无法兼容吗?

确实,虽然概率比直接换第三方图片加载库还低,但既然有可能,就要尽可能的解决。

于是就是了下面的这种终极方法。

使用ASM插桩进行大图监控

这就升级到图片加载的本质了,任何图片加载最终都是要填充到ImageView上。而在这过程中自然避免不了使用ImageView的方法进行填充图片。

例如:setImageDrawable等等。

当然也可以直接hook整个ImageView,全局将其替换成HookImageView,再到其内部实现大图监测。
这两种都是通过ASM,只是对象不一样,但原理都基本一致。

以下是一个简单的示例,使用ASMAndroid中的 ImageViewsetImageDrawable 方法进行拦截:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import org.objectweb.asm.*;

public class ImageViewInterceptor implements ClassVisitor {

private final ClassVisitor cv;

public ImageViewInterceptor(ClassVisitor cv) {
this.cv = cv;
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("setImageDrawable") && desc.equals("(Landroid/graphics/drawable/Drawable;)V")) {
return new ImageViewMethodVisitor(mv);
}
return mv;
}

// 其他方法省略,你可以根据需要实现其他 visitX 方法
}

class ImageViewMethodVisitor extends MethodVisitor {

public ImageViewMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}

@Override
public void visitCode() {
super.visitCode();
// 在方法开头插入大图监测逻辑的字节码
// ...
}

@Override
public void visitInsn(int opcode) {
if (opcode == Opcodes.RETURN) {
// 在 RETURN 指令前插入大图监测逻辑的字节码
// ...
}
super.visitInsn(opcode);
}
}

// 在某处,使用 ASM 进行字节码修改
ClassReader cr = new ClassReader("android/widget/ImageView");
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ImageViewInterceptor interceptor = new ImageViewInterceptor(cw);
cr.accept(interceptor, 0);

....

这个示例中,ImageViewInterceptorImageViewsetImageDrawable 方法进行了拦截,ImageViewMethodVisitor 中插入了大图监测逻辑的字节码。

需要注意的是。在实际应用中,需谨慎考虑因字节码操作而引起的潜在问题和兼容性风险。

注意事项与优化技巧

在实现大图监测时,我们需要注意以下事项:

  • 灵活设置阈值: 根据不同设备和应用场景,动态调整大图的阈值,以保证监测的准确性和及时性。
  • 合理选择处理方式: 对于大图,可以选择合适的处理方式,如压缩、裁剪或异步加载,以降低内存占用。
  • 异步处理: 将大图的处理放在异步线程中,避免阻塞主线程,提高应用的响应性。

总结

通过本文的学习,相信你已经对Android大图监测有了深入的理解,并可以在实际项目中应用这些知识,提升应用的性能和用户体验。

推荐

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack\&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。

支持一下
赞赏是一门艺术