[TOC]
内存优化应该优先去做见效快的地方,图片内存优化是内存优化的重点,可能一张图片没有回收就会造成几M内存的浪费
# 常规的图片内存优化方法
我们都知道,图片所占内存=宽*高*一像素占用内存
所以优化图片内存主要有以下几个思路
1.缩放减小宽高
2.减少每个像素所占用的内存
3.内存复用,避免重复分配内存
4.对于大图,可以采取局部加载的策略
## 减少图片宽高
有时图片宽高为`200*200`,而`View`宽高为`100*100`,这种时候如果展示`200*200`的图片没有意义,应该对图片进行缩放
这种情况一般通过`inSampleSize`实现
~~~java
BitampFactory.Options options = new BitmapFactory.Options();
// 设置为4就是宽和高都变为原来1/4大小的图片
options.inSampleSize = 4;
BitmapFactory.decodeSream(is, null, options);
~~~
## 减少每个像素所占用的内存
在`API29`中,将`Bitmap`分为`ALPHA_8`, `RGB_565`, `ARGB_4444`, `ARGB_8888`, `RGBA_F16`, `HARDWARE`六个等级。
* `ALPHA_8`:不存储颜色信息,每个像素占1个字节;
* `RGB_565`:仅存储`RGB`通道,每个像素占2个字节,对`Bitmap`色彩没有高要求,可以使用该模式;
* `ARGB_4444`:已弃用,用`ARGB_8888`代替;
* `ARGB_8888`:每个像素占用4个字节,保持高质量的色彩保真度,默认使用该模式;
* `RGBA_F16`:每个像素占用8个字节,适合宽色域和`HDR`;
* `HARDWARE`:一种特殊的配置,减少了内存占用同时也加快了`Bitmap`的绘制。
每个等级每个像素所占用的字节也都不一样,所存储的色彩信息也不同。同一张100像素的图片,`ARGB_8888`就占了400字节,`RGB_565`才占200字节
所以在某些场景中,修改图片格式可以达到减少一半内存的效果
## 内存复用,避免重复分配内存
`Bitmap`所占内存比较大,如果我们频繁创建与回收`Bitmap`,那么很容易造成内存抖动,所以我们应该尽量复用`Bitmap`内存
在 `Android 3.0(API 级别 11)`开始,系统引入了 `BitmapFactory.Options.inBitmap` 字段。如果设置了此选项,那么采用 `Options` 对象的解码方法会在生成目标 `Bitmap` 时尝试复用 `inBitmap`,这意味着 `inBitmap` 的内存得到了重复使用,从而提高了性能,同时移除了内存分配和取消分配。不过 `inBitmap` 的使用方式存在某些限制,在 `Android 4.4(API 级别 19)`之前系统仅支持复用大小相同的位图,4.4 之后只要 `inBitmap` 的大小比目标 `Bitmap` 大即可
## 大图局部加载策略
对于图片加载还有种情况,就是单个图片非常巨大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图等
首先不压缩,按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性整图加载到内存中
所以这种情况的优化思路一般是局部加载,通过`BitmapRegionDecoder`来实现
~~~java
//设置显示图片的中心区域
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);
mImageView.setImageBitmap(bitmap);
复制代码
~~~
## 小结
上面所说的这些关于`Bitmap`的内存优化策略其实都比较简单,而且我们在开发中可能很少用到
因为我们常用的图片框架比如`Glide`已经将这些都封装在里面了,所以一般情况下我们加载图片时不需要做这些特殊操作
关于`Glide`对于加载图片都做了哪些优化,有兴趣的同学可以参考:[【带着问题学】Glide做了哪些优化?](https://juejin.cn/post/6970683481127043085 "https://juejin.cn/post/6970683481127043085")
# 图片兜底策略
针对因`activity`、`fragment`泄漏导致的图片泄漏,我们可以在`onDetachedFromWindow`时机进行了监控和兜底,具体流程如下:
![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3a851196e2f5439fa05070ac0c37eb90~tplv-k3u1fbpfcp-watermark.awebp)
通过这种方式可以方便地解决因`Activity`导致的图片泄漏问题
#线上大图监控方案
当运营在线上配置了不合理大小的图片时,如果我们及时发现,也会带来内存问题
如果图片本身大小就不合理,我们在这个基础上谈图片优化也没有什么意义,因此大图监控这也是个比较常见的需求
下面介绍几种大图监控的方案:
## `ArtHook` 方案
该方案采用`weishu`大佬写的`epic`库实现,通过对`ART`虚拟机的`hook`,`hook ImageView`的 `setImageBitmap` 等方法
解析对比方法参数中的 `bitmap` 宽高和 `ImageView` 实例的宽高,也可以获得`bitmap`的实际大小
如果图片宽高比`ImageView`宽高大,或者图片大小超出了阈值,就可以把相关信息上报
这种方案的优点在于:
1.侵入性极低,一次初始化配置即可`hook`全局的目标`View`控件
2.可以获取代码调用堆栈,方便开发者快速定位
而缺点则在于:
兼容性存在问题,使用了`hook`系统`API` ,不能用于线上
## `BaseActivity` 方案
大部分应用工程在业务发展的过程中都会沉淀封装自己的`BaseActivity` ,通过在`BaseActivity onDestroy`中动态地检测各个`View`控件,从而获知图片加载情况
~~~kotlin
class BaseActivity : Activity(){
fun onDestory(){
if(isOpenCheckBitmap){
checkBitmapFromView()
}
}
fun checkBitmapFromView(){
//1、遍历activity中的各个View控件
//2、获取View控件加载的Bitmap
//3、对比Bitmap宽高与View宽高
}
}
复制代码
~~~
这种方案的优点在于:
1.兼容性强,无任何反射
2.加入简单,没有什么复杂逻辑
缺点在于:
1.侵入性太强,需要修改`BaseActivity`
2.`BaseActivity.onDestory`本身可能被重写,并不安全
## `ASM`方案
该方案在编译流程进行插桩,通过匹配`setImageBitmap` 、 `setBackground` 等关键方法,插入`Bitmap`大小检测逻辑
这种方案优点在于:
1.编译时期插桩,对开发过程无侵入性
缺点在于:
1.通过插桩的方式打点,可能会增加编译期耗时
2.`ASM`代码维护成本较高,使用起来不是那么方便
## `registerActivityLifecycleCallback`方案
通过`registerActivityLifecycleCallback`监听`Activity`生命周期,在`onStop`时进行`Bitmap`大小检测的逻辑
~~~kotlin
private fun registerActivityLifecycleCallback(application: Application) {
application.registerActivityLifecycleCallbacks(object :
Application.ActivityLifecycleCallbacks {
override fun onActivityStopped(activity: Activity) {
checkBitmapIsTooBig(childViews)
}
})
}
~~~
这种方案对原始代码无侵入性,同时使用起来比较简单,也没有兼容性问题,应该属于比较良好的方案
详细实现可见:[BitmapCanary 诞生](https://juejin.cn/post/6956138531789996040#heading-14 "https://juejin.cn/post/6956138531789996040#heading-14")
# 参考资料
[【从入门到实用】android内存优化深入解析](https://juejin.cn/post/6975876569990447134)
- Android
- 四大组件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介绍
- MessageQueue详细
- 启动流程
- 系统启动流程
- 应用启动流程
- Activity启动流程
- View
- view绘制
- view事件传递
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大数据
- Binder小结
- Android组件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 迁移与修复
- Sqlite内核
- Sqlite优化v2
- sqlite索引
- sqlite之wal
- sqlite之锁机制
- 网络
- 基础
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP进化图
- HTTP小结
- 实践
- 网络优化
- Json
- ProtoBuffer
- 断点续传
- 性能
- 卡顿
- 卡顿监控
- ANR
- ANR监控
- 内存
- 内存问题与优化
- 图片内存优化
- 线下内存监控
- 线上内存监控
- 启动优化
- 死锁监控
- 崩溃监控
- 包体积优化
- UI渲染优化
- UI常规优化
- I/O监控
- 电量监控
- 第三方框架
- 网络框架
- Volley
- Okhttp
- 网络框架n问
- OkHttp原理N问
- 设计模式
- EventBus
- Rxjava
- 图片
- ImageWoker
- Gilde的优化
- APT
- 依赖注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 协程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 运行期Java-hook技术
- 编译期hook
- ASM
- Transform增量编译
- 运行期Native-hook技术
- 热修复
- 插件化
- AAB
- Shadow
- 虚拟机
- 其他
- UI自动化
- JavaParser
- Android Line
- 编译
- 疑难杂症
- Android11滑动异常
- 方案
- 工业化
- 模块化
- 隐私合规
- 动态化
- 项目管理
- 业务启动优化
- 业务架构设计
- 性能优化case
- 性能优化-排查思路
- 性能优化-现有方案
- 登录
- 搜索
- C++
- NDK入门
- 跨平台
- H5
- Flutter
- Flutter 性能优化
- 数据跨平台