内存基本原理 & 常见优化手段 & 图片基础知识

基本原理

Java 引用类型

  1. 强引用:就是正常的引用,GC 是不会清理一个强引用引用的对象的,即使面临内存溢出的情况
  2. 软引用:SoftReference,GC 会在内存不足的时候清理引用的对象
  3. 弱引用:GC 会直接清理弱引用对象,不管内存是否够用
  4. 虚引用:和弱引用一样,会直接被 GC 清理,但是通过 get 方法无法获取对象引用,因此虚引用只能获取对象被回收的通知

关键内存指标

VSS(Virtual Set Size)

虚拟内存是进程可以访问的全部地址空间,包含了父进程(Zygote)继承来的(大头是Dalvik虚拟机)和自身申请的所有内存,实际上大部分内存都没有分配对应的物理页,典型场景有:

  1. malloc 会为进程分配虚拟内存,但是不会对应分配物理页
  2. Android 下几乎所有虚拟内存都是通过 mmap 来分配的,包括文件映射和匿名映射,加载动态库后如果没有实际访问,则不对应分配物理页,即使发生了数据访问(读/写),也只是加载需要的物理页(单位为4K)

RSS(Resident Set Size)

RSS 代表进程真实使用的所有内存,该指标应用场景很少,既无法反映总的虚拟内存占用,也无法反映进程本身的内存占用情况

PSS(Proportional Set Size)

和 RSS 不同,PSS 会均分共享库内存占用。在系统内存不足时,OOM Killer 主要通过 PSS 指标来衡量杀死哪个应用

USS(Unique Set Size)

USS 代表进程自身的内存占用,不包含共享库内存占用。该指标可以用来监测内存泄漏

Page

Page faults

如果虚拟内存没有和物理内存建立映射,则触发 Page Fault。这时如果对应文件没有加载到物理内存则触发 Major Page Fault,否则触发 Minor Page Fault,进而建立虚拟内存和物理内存的页表映射

Dirty page

对于通过 mmap 映射到内存中的文件,如果内容发生了修改(update),且未调用 msync,则对应为脏页

Zram

内存压缩区,Android 内存管理不支持交换区(Swap),由于安卓存储空间相对有限,系统没有选择通过交换区来无限扩大物理内存,而是通过 Zram 和 Low Memory Killer 回收内存

public interface ComponentCallbacks2 extends ComponentCallbacks {

    /** @hide */
    @IntDef(prefix = { "TRIM_MEMORY_" }, value = {
            // 后台应用对应四个状态
            TRIM_MEMORY_COMPLETE,   // LRU 队列尾端,即将被杀死,需要尽可能的释放资源
            TRIM_MEMORY_MODERATE,   // LRU 队列中部,适当释放资源
            TRIM_MEMORY_BACKGROUND, // 被移入 LRU 队列,释放一些可以被快速重建的资源
            TRIM_MEMORY_UI_HIDDEN,  // 应用被切到后台,UI 大对象应该被释放
            // 前台应用对应三个状态
            TRIM_MEMORY_RUNNING_CRITICAL, // 设备内存极度紧张,应该释放尽量多的不必要内存
            TRIM_MEMORY_RUNNING_LOW,      // 设备内存相对紧张,应该释放一些不必要的内存
            TRIM_MEMORY_RUNNING_MODERATE, // 设备内存有点紧张,有没有可能释放一些不必要的内存
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface TrimMemoryLevel {}
}

Memory-mapped File

Android 平台几乎所有虚拟内存都是通过 mmap 来申请,通过 prctl 修改匿名内存段名称、通过 ioctl 修改设备文件内存段名称

Mmap 文件状态:clean(mmap)、dirty(update)、clean(msync)、remove(munmap)

内存指标获取

  • adb shell dumpsys meminfo pid
  • ps -A | grep "xxx"
  • 获取进程物理内存:ActivityManager#getProcessMemoryInfo
  • 获取手机物理内存:ActivityManager#getMemoryInfo
  • Dalvik堆内存限制
    • ActivityManager#memoryClass
    • ActivityManager#largeMemoryClass
  • Dalvik堆内存大小
    • Runtime.getRuntime().freeMemory
    • Runtime.getRuntime().totalMemory
    • Runtime.getRuntime().maxMemory
  • Native堆内存大小
    • Debug.getNativeHeapSize
    • Debug.getNativeHeapFreeSize
    • Debug.getNativeHeapAllocatedSize
  • 获取进程虚拟内存:RandomAccessFile("/proc/" + Process.myPid() + "/status", "r")
  • 获取文件描述符:    RandomAccessFile("/proc/" + Process.myPid() + "/fd")
  • 获取虚拟内存段:    RandomAccessFile("/proc/" + Process.myPid() + "/maps", "r")

硬件加速

  • CPU 更擅长复杂逻辑控制,而 GPU 得益于大量 ALU 和并行结构设计,更擅长数学运算
  • 页面由各种基础元素(DisplayList)构成,渲染时需要进行大量浮点运算
  • 硬件加速条件下,CPU 用于控制复杂绘制逻辑、构建或更新 DisplayList;GPU 用于完成图形计算、渲染 DisplayList
  • 硬件加速条件下,刷新界面尤其是播放动画时,CPU 只重建或更新必要的 DisplayList,进一步提高渲染效率
  • 实现同样效果,应尽量使用简单的 DisplayList,从而达到更好的性能(Shape 代替 Bitmap等)

内存溢出

安卓系统常见 OOM 有两类:Java OOM、Native OOM

Java OOM

Java 堆内存大小达到了虚拟机规定的上限,上限值可以通过 ActivityManager#memoryClass、ActivityManager#largeMemoryClass 获取

Native OOM

虚拟内存达到了系统上限,虚拟内存上限值和 APP 位数以及机器位数有关

内存优化

  • 大图优化
  • 内存泄漏、线程泄漏、句柄泄漏
  • 对于复杂系统,及时回收不需要的功能,防止持续占用内存

内存泄漏

长周期对象持有短周期对象,常见的场景有:Handler 延时消息、注册未反注册、静态变量 / 单例直接或间接持有 UI 组件

句柄泄漏

  • 数据库: Cursor未关闭
  • 文件:FileInputStream、FileOutputStream、FileReader、FileWriter 未关闭
  • 网络: Socket 未关闭

线程泄漏

创建线程的方式

  1. 通过 Thread 类直接创建,通过 setName 方法设置线程名称
  2. 通过 Executors 工具类创建线程池,对应 ThreadFactory 可以设置线程名称
  3. 通过 HandlerThread 创建线程,构造方法中传入线程名
  4. 第三方 SDK 一般会创建特殊前缀开头的线程,例如,RxJava 通过内部 Scheduler 创建固定名称前缀的线程

关闭线程的方式

  1. Thread.interrupt
  2. HanderThread.quit
  3. ExecutorService.shutdown
  4. Future.cancel(boolean mayInterruptIfRunning)

线程泄露场景

  1. ExecutorService#commit 调用频率过快,导致任务堆积(不常见)
  2. 未调用 ExecutorService#shutdown 导致线程泄漏(Deamon线程 or 线程执行时间过长)
  3. HandlerThread 线程泄漏

大图优化

一张图片占用多少内存,需要考虑手机的 Dpi 信息、图片存放的资源目录、BitmapConfig(像素质量、是否裁剪)、图片的原始像素来综合计算

错误认知

  • 压缩图片大小(tinyPNG)可以降低图片内存占用
  • 下图像素大小为 718*701,由于一个像素占用 4 个字节,则内存占用为 718*701*4 字节(1.9M)

像素质量

首先一个像素点内存占用不一定是 4 个字节,解析图片时(BitmapFactory.decodeResource)可以设置 Bitmap 像素类型,常见 Config 有:

  • Bitmap.Config.ALPHA_8:只保存 alpha 通道,一个像素占用 1 个字节
  • Bitmap.Config.RGB_565:不保存 alpha 通道,一个像素占用 3 个字节
  • Bitmap.Config.ARGB_4444:Deprecated
  • Bitmap.Config.ARGB_8888:通用配置,一个像素占用 4 个字节
  • Bitmap.Config.RGBA_F16:一个像素占用 8 个字节,适用于广色域以及 HDR 内容
  • Bitmap.Config.HARDWARE:Android 8.0及以上可用,Bitmap只存储在显存中,不支持修改,整体可以减少一半内存占用(内存 + 显存 -> 显存)

屏幕像素比 DPI(dots per inch)

一英寸规定是 160dp(density-independent pixel),dpi 代表一英寸有多少像素(px),那么1dp 就等于 dpi / 160 个像素(px)

Android 资源匹配机制会用到 dpi 这个参数,正常手机的屏幕像素点是平均分布的,所以无论是宽、高、还是斜边,其 dpi 都是一样的,当然由于技术限制,会存在一些误差,获取方法如下:

float xdpi = getResources().getDisplayMetrics().xdpi;
float ydpi = getResources().getDisplayMetrics().ydpi;

像素大小

上图的像素大小是 718*701,但是真正加载到内存的图片像素大小不一定是 718*701

影响图片像素大小的主要是资源适配的 dpi 维度,以手机屏幕 dpi = 400,像素类型 ARGB_8888 为例:

  1. 系统会优先在 xxhdpi 文件夹下查找对应的图片,如果找到了就直接加载,且不会进行缩放,此时图片在手机上显示的就是它本身的像素大小(1.9M)
  2. 如果没有在对应 dpi 下找到对应文件,就去更高分辨率的文件夹中查找,一直找到最高。比如在 xxxhdpi 下找到对应图片,则进行像素裁剪,图片最终内存占用为(1.9M / 1.3 / 1.3 = 1.12M)
  3. 如果在高分辨率下没有找到对应图片,则依次查询低分辨率的目录,由高到低一直查到 ldpi。比如在 xhdpi 下找到对应图片,则进行像素填充,图片最终内存占用为(1.9M * 1.5 * 1.5 = 4.275M)
  4. 如果在各个分辨率的目录下都没有找到,则查找 drawable-nodpi 目录,此目录不进行缩放,图片最终占用内存为(1.9M)
  5. 如果还没有找到,则查找 drawable 目录,因为该目录没有指定 dpi 信息,默认为标准 dpi(mdpi),图片最终内存占用为(1.9M * 3 * 3 = 17.1M)

dpi范围

资源目录

0dpi ~ 120dpi

ldpi

120dpi ~ 160dpi

mdpi(标准Dpi)

160dpi ~ 240dpi

hdpi

240dpi ~ 320dpi

xhdpi

320dpi ~ 480dpi

xxhdpi(设计切图时对应的Dpi)

480dpi ~ 640dpi

xxxhdpi

图片解析

当 ImageView 可展示的像素点小于图片的像素点,为了避免浪费内存空间,我们需要对图片进行压缩。BitmapFactory 类提供了多个解析方法(decodeByteArray、decodeFile、decodeResource等)用于创建 Bitmap 对象,而且每一种解析方法都提供了一个可选的 BitmapFactory.Options 参数,将这个参数的 inJustDecodeBounds 属性设置为 true 就可以让解析方法禁止为 bitmap 分配内存,返回值也不再是一个 Bitmap 对象,而是 null。虽然 Bitmap 是 null,但是BitmapFactory.Options 的 outWidth 和 outHeight 属性都会被赋值,在计算压缩比例时,我们要考虑加载控件的大小和屏幕的大小,通过设置 BitmapFactory.Options 中 inSampleSize 的值就可以压缩图片

注意,Bitmap.compress 只是质量压缩,不会对内存产生影响

public static Bitmap decodeFile(String path, int width, int height) {
    int inSampleSize = getInSampleSize(path, width, height);
    BitmapFactory.Options newOpts = new BitmapFactory.Options();
    newOpts.inSampleSize = inSampleSize;
    newOpts.inJustDecodeBounds = false;
    newOpts.inPreferredConfig = Bitmap.Config.RGB_565;
    return BitmapFactory.decodeFile(path, newOpts);
}

private static int getInSampleSize(String path, float width, float height) {
    BitmapFactory.Options newOpts = new BitmapFactory.Options();
    newOpts.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(path, newOpts);
    int outWidth = newOpts.outWidth;
    int outHeight = newOpts.outHeight;
    return (int) getScale(width, height, outWidth, outHeight);
}

图片缩放类型

ScaleType 分为三个类型:以 FIT_ 开头的四种,共同点都是会对图片进行缩放;以 CENTER_ 开头的三种,共同点是图片的中心会与 ImageView 的中心点重叠;还有一种是 ScaleType.MATRIX

  • ScaleType.FIT_CENTER:图片会被等比例缩放到能完全展示出来,并居中显示,图片默认显示模式
  • ScaleType.FIT_START:图片会被等比例缩放到能完全展示出来,对齐方式为左上角
  • ScaleType.FIT_END:图片会被等比例缩放到能完全展示出来,对齐方式为右下角
  • ScaleType.FIT_XY:非等比例缩放,完全填充控件大小
  • ScaleType.CENTER:不使用缩放,居中显示
  • ScaleType.CENTER_CROP:等比例缩放至完全填充整个ImageView,并居中显示,小图片不进行缩放
  • ScaleType.CENTER_INSIDE:等比例缩放至完全展示出整个图片,并居中显示,小图片不进行缩放
  • ScaleType.MATRIX:自定义转换类型

图片内存存储位置

  • Android 3.0~8.0 之前的 Bitmap 像素数据基本存储在 Java Heap
  • Android 8.0 之后的 Bitmap 像素数据基本存储在 Native Heap
  • Android 4.4 可以通过 inInputShareable、inPurgeable 让 Bitmap 的内存在 Native 分配

Bitmap 复用

Android 3.0 引入了 BitmapFactory.Options.inBitmap 字段,设置此字段之后解码方法会尝试复用一张存在的 Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。不过,使用这个字段有几点限制:

  • 声明可被复用的 Bitmap 必须设置 inMutable = True
  •  Android 4.4 之前被复用的 Bitmap 的 inPreferredConfig 会覆盖待分配内存的 Bitmap 设置的inPreferredConfig
  •  Android 4.4 之前只有格式为 jpg、png,同等宽高,inSampleSize 为 1 的 Bitmap 才可以复用
  • Android 4.4 之后被复用的 Bitmap 的内存必须大于需要申请内存的 Bitmap 的内存

Bitmap 和 Drawable 的区别

Bitmap - 称作位图,一般位图的文件格式后缀为 bmp,当然编码器也有很多如 RGB565、RGB888。作为一种逐像素的显示对象执行效率高,但是缺点也很明显存储效率低。我们理解为一种存储对象比较好

Drawable - 作为 Android 平下通用的图形对象,它可以装载常用格式的图像,比如 GIF、PNG、JPG,当然也支持 BMP,当然还提供一些高级的可视化对象,比如渐变、图形等

A bitmap is a Drawable. A Drawable is not necessarily a bitmap. Like all thumbs are fingers but not all fingers are thumbs. Though usually not visible to the application, Drawables may take a variety of forms:

  • Bitmap: the simplest Drawable, a PNG or JPEG image.
  • Nine Patch: an extension to the PNG format allows it to specify information about how to stretch it and place things inside of it.
  • Shape: contains simple drawing commands instead of a raw bitmap, allowing it to resize better in some cases.
  • Layers: a compound drawable, which draws multiple underlying drawables on top of each other.
  • States: a compound drawable that selects one of a set of drawables based on its state.
  • Levels: a compound drawable that selects one of a set of drawables based on its level.
  • Scale: a compound drawable with a single child drawable, whose overall size is modified based on the current level.

在多个不同的 View 上使用相同的 Drawable 可行么

通常可以,但不是绝对的。任何无状态(non-stateful)的 Drawable(例如 BitmapDrawable )通常都是 OK 的。但是有状态的 Drawable 不一样,在同一时间多个 View 上展示它们通常不是很安全,因为多个 View 都会修改Drawable 。对于有状态的 Drawable ,建议传入一个资源ID,或者使用 newDrawable() 来给每个请求传入一个新的拷贝

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little-sparrow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值