安卓Android高手笔记

Android高手笔记-内存优化

2021-07-21  本文已影响0人  今阳说
为什么图片的三级缓存,内存是第一位
  1. 硬件快:内存本身读取、存入速度快
  2. 复用快:解码成果有效保存,复用时,直接使用解码后对象,而不是再做一次图像解码。
解码:常见的jpg,png等图片格式,都是把 “像素缓冲” 使用不同的手段压缩后的结果,所以这些格式的图片,
要在设备上展示,就 必须经过一次解码,它的 执行速度会受图片压缩比、尺寸等因素影响。
(官方建议:把从内存中淘汰的图片,降低压缩比后存储到本地,以备后用,这样可以最大限度地降低以后复用时的解码开销。)
Android 内存管理机制
  1. Java 对象生命周期
    • Java代码编译后生成的字节码.class文件从文件系统中加载到虚拟机之后,便有了JVM上的Java对象,Java对象在JVM上运行有7个阶段:
    1. Created(创建): 分配存储空间,构造对象,超类及子类静态成员初始化,超类及子类成员变量按顺序初始化
    2. InUse(应用): 此时对象至少被一个强引用持有
    3. Invisible(不可见): 程序本身不再持有该对象的任何强引用, 但是,该对象仍可能被虚拟机下的某些已装载的静态变量线程或JNI等强引用持有,这些特殊的强引用称为“GC Root”, 导致该对象的内存泄漏,因而无法被GC回收;
    4. Unreachable(不可达): 该对象不再被任何强引用持有
    5. Collected(收集):当GC已经对该对象的内存空间重新分配做好准备时,对象进入收集阶段,如果该对象重写了finalize()方法,则执行它;
    6. Finalized(终结):等待垃圾回收器回收该对象空间
    7. Deallocated(对象空间重新分配):GC对该对象所占用的内存空间进行回收或者再分配,则该对象彻底消失;
  2. Java 内存分配模型
    • JVM 将整个内存划分为了几块:
    1. 方法区:存储类信息、常量、静态变量等,所有线程共享(存静态常量)
    2. 虚拟机栈:存储局部变量表、操作数栈等(存Java变量引用)
    3. 本地方法栈:存native变量引用
    4. 堆:内存最大的区域,对象的实际存储位置,栈中只是对象的引用,GC和内存泄漏的主战场,所有线程共享(存对象)
    5. 程序计数器:计算当前线程的当前方法执行到多少行
  3. Android 内存回收机制
    • Android 设备每打开一个 APP, 内存都是弹性分配的,并且其分配值与最大值受具体设备而定;
    • 注意区分如下两种 OOM 场景:
      1. 内存真正不足:例如 APP 当前进程最大内存上限为 512 MB,当超过这个值就表明内存真正不足了;
      2. 可用内存不足:手机系统内存极度紧张,就算 APP 当前进程最大内存上限为 512 MB,我们只分配了 200 MB,也会产生内存溢出,因为系统的可用内存不足了;
  4. GC的三种类型
    • kGcCauseForAlloc:分配内存不够引起的GC,会Stop World。由于是并发GC,其它线程都会停止,直到GC完成。
    • kGcCauseBackground:内存达到一定阈值触发的GC,由于是一个后台GC,所以不会引起Stop World。
    • kGcCauseExplicit:显示调用时进行的GC,当ART打开这个选项时,使用System.gc时会进行GC。
内存问题三个分类
  1. 内存抖动:内存波动图形呈锯齿状、GC导致卡顿,Dalvik上比较明显,ART在内存管理和回收策略上做了大量优化,内存分配和GC效率提升了5~10倍,抖动概率较小;
    • 为什么内存抖动会导致 OOM?
      1. 频繁创建对象,导致内存不足及碎片(不连续)
      2. 不连续的内存片无法被分配,导致OOM
  2. 内存泄漏:在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小
  3. 内存溢出:即OOM,Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM
内存可能造成的两个问题
  1. 异常: 包括 OOM、内存分配失败这些崩溃,也包括因为整体内存不足导致应用被杀死、设备重启等问题
  2. 卡顿: 除了频繁 GC 造成卡顿之外,物理内存不足时系统会触发 low memory killer 机制,系统负载过高是造成卡顿的另外一个原因
Low Memory Killer 机制

针对于手机系统所有进程而制定的,当我们手机内存不足的情况下,LMK 机制就会针对我们所有进程进行回收,而其对于不同的进程,它的回收力度也是有不同的,目前系统的进程类型主要有如下几种:前台进程, 可见进程, 服务进程, 后台进程, 空进程;从前台进程到空进程,进程优先级会越来越低,因此,它被 LMK 机制杀死的几率也会相应变大。此外,LMK 机制也会综合考虑回收收益,这样就能保证我们大多数进程不会出现内存不足的情况

内存优化主要包括两方面
  1. 优化RAM,即降低运行时内存
    • 手机不使用 PC 的 DDR内存,采用的是 LPDDR RAM,即 ”低功耗双倍数据速率内存“。
  2. 优化ROM,即降低程序占ROM的体积
内存泄露的检测与修改
  1. 监控方案: Square的开源库 LeakCanary
    • 或使用基于 LeakCanary 的改进版 ResourceCanary Matrix
  2. 对系统内存泄露的Hack Fix:
    • AndroidExcludedRefs列出了一些由于系统原因导致引用无法释放的例子,同时对于大多数的例子,都会提供建议如何通过hack的建议去修复
  3. 通过兜底回收内存
    • Activity泄漏会导致该Activity引用到的Bitmap、DrawingCache等无法释放,对内存造成大的压力,兜底回收是指对于已泄漏Activity,尝试回收其持有的资源,泄漏的仅仅是一个Activity空壳,从而降低对内存的压力。做法也非常简单,在Activity onDestory时候从view的rootview开始,递归释放所有子view涉及的图片,背景,DrawingCache,监听器等等资源,让Activity成为一个不占资源的空壳,泄露了也不会导致图片资源被持有。
使用MAT来查找内存泄漏
  1. https://eclipse.org/mat/downloads.php下载MAT客户端。
  2. 从Android Studio进入Profile的Memory视图,选择需要分析的应用进程,对应用进行怀疑有内存问题的操作,结束操作后,主动GC几次,最后export dump文件
  3. 因为Android Studio保存的是Android Dalvik/ART格式的.hprof文件,所以需要转换成J2SE HPROF格式才能被MAT识别和分析;
Android SDK自带了一个转换工具在SDK的platform-tools下,转换命令为:
./hprof-conv file.hprof converted.hprof
  1. 通过MAT打开转换后的HPROF文件。
优化内存的意义
  1. 减少OOM,提高应用稳定性。
  2. 减少卡顿,提高应用流畅度(如GC次数增多导致)。
  3. 减少内存占用,提高应用后台运行时的存活率(防止Low Memory Killer)。
  4. 减少异常发生和代码逻辑隐患。
常见内存泄漏场景
  1. 资源对象未关闭
    • 解决方法:应该在对象不再使用或Activity销毁时及时调用close方法,再置为null;
  2. 注册对象未注销
    • 如BroadcastReceiver、EventBus未注销;
    • 解决方法:Activity销毁时注销
  3. 类的静态变量持有大数据对象
    • 解决方法:应尽量避免使用静态变量存储数据,特别是大数据对象;
  4. 单例造成的内存泄漏
    • 解决方法:优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可;
  5. 非静态内部类的静态实例
    • 该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。
    • 解决方法:1. 静态内部类+弱引用; 2. Activity 关闭,即触发 onDestory 时解除内类和外部的引用关系
  6. Handler临时性内存泄漏
    • handler发送的Message存储在MessageQueue中,Message中的target是handler的一个引用,如果handler是非静态的,或持有Activity或service的强引用,Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,就会到主Activity的资源无法回收;
    • 解决方法:1. 静态内部类+弱引用;2. Destroy或者Stop时removeCallbacksAndMessages;
    • AsyncTask内部也是Handler机制, 对于类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。
  7. 容器中的对象没清理
    • 解决方法:退出之前,将集合clear,然后置为null
  8. WebView
    • 在应用中只要使用一次WebView,内存就不会被释放掉
    • 其 网络延时、引擎 Session 管理、Cookies 管理、引擎内核线程、HTML5 调用系统声音、视频播放组件等产生的引用链条无法及时打断,造成的内存问题基本上可以用”无解“来形容。
    • 解决方法:把 WebView 装入另一个进程。具体为在 AndroidManifest 中对当前的 Activity 设置 android:process 属性即可,最后,在 Activity 的 onDestory 中退出进程,这样即可基本上终结 WebView 造成的泄漏。
  9. 使用ListView时造成的内存泄漏
    • 解决方法:构造Adapter时,使用缓存的convertView 或使用RecyclerView
  10. 使用系统服务时产生的内存问题
    • getSystemService 方法来获取系统服务,但是当在 Activity 中调用时,会默认把 Activity 的 Context 传给系统服务,在某些不确定的情况下,某些系统服务内部会产生异常,从而 hold 住外界传入的 Context。
    • 解决方案是 直接使用 Application 的 Context 去获取系统服务
优化内存
  1. 尽量避免AutoBoxing)(自动装箱),减少字符串使用加号拼接,改为使用StringBuilder
  2. 读文件优化:读文件使用ByteArrayPool,初始设置capacity,减少expand
  3. 内存复用
    • 资源复用:通用的字符串、颜色定义、简单页面布局的复用。
    • 视图复用:可以使用ViewHolder实现ConvertView复用。
    • 对象池:显示创建对象池,实现复用逻辑,对相同的类型数据使用同一块内存空间。
    • Bitmap对象的复用:使用inBitmap属性可以告知Bitmap解码器尝试使用已经存在的内存区域,
      新解码的bitmap会尝试使用之前那张bitmap在heap中占据的pixel data内存区域。
  4. 减少不必要或不合理的对象:例如在onDraw、getView中应减少对象申请,尽量重用。更多是一些逻辑上的东西,例如循环中不断申请局部变量等
  5. 选用合理的数据格式:
    • 使用ArrayMap, SparseArray, SparseBooleanArray, and LongSparseArray来代替HashMap,
    • 使用 IntDef和StringDef 替代枚举类型;
    • 使用 LruCache 最近最少使用缓存
  6. Bitmap 优化 两种方法:
    1. 统一图片库
      • 收拢图片的调用,这样我们可以做整体的控制策略。
      • 例如低端机使用 565 格式、更加严格的缩放算法(如通过二次采样压缩,LuBan库,哈夫曼算法压缩),当显示小图片或对图片质量要求不高时可以考虑使用RGB_565,用户头像或圆角图片一般可以尝试ARGB_4444;
      • 可以使用 Glide、Fresco 或者采取自研(如通过inBitmap+LruCache+软引用实现图片的三级缓存)都可以,而且需要进一步将所有 Bitmap.createBitmap、BitmapFactory 相关的接口也一并收拢;
      • 图片资源优化:只需要UI提供一套高分辨率的图,图片建议放在drawable-xxhdpi文件夹下,这样在低分辨率设备中图片的大小只是压缩,不会存在内存增大的情况。如若遇到不需缩放的文件,放在drawable-nodpi文件夹下。
    2. 统一监控
      • 统一图片库后就非常容易监控 Bitmap 的使用情况,主要有三点需要注意
      1. 大图片监控:可以计算有多少比例的图片会超过屏幕的大小,也就是图片的“超宽率”
      2. 重复图片监控:Bitmap 的像素数据完全一致,但是有多个不同的对象存在
      3. 图片总内存:在 OOM 崩溃的时候,也可以把图片占用的总内存、Top N 图片的内存都写到崩溃日志中,帮助我们排查问题。
  7. 在App可用内存过低时主动释放内存:
    • onTrimMemory/onLowMemory 方法去释放掉图片缓存、静态缓存来自保;
  8. item被回收不可见时释放掉对图片的引用:
    • ListView: 在ImageView onDetachFromWindow的时候释放掉图片引用;
    • RecyclerView: 只有被回收进mRecyclePool中后拿出来复用才会重新绑定数据,因此重写Recycler.Adapter中的onViewRecycled()方法来使item被回收进RecyclePool的时候去释放图片引用;
  9. 其他:尽使用static final 优化成员变量,使用增强型for循环语法;
  10. 已经被用户使用物理“返回键”退回到后台的进程
    • 如果包含了以下 两点,则 不会被轻易杀死:
      1. 进程包含了服务 startService,而服务本身调用了 startForeground(低版本需通过反射调用)
      2. 主 Activity 没有实现 onSaveInstanceState 接口
    • 建议 在运行一段时间(如3小时)后主动保存界面进程(位于后台),然后重启它,这样可以有效地降低内存负载(详见下面的内存兜底策略)。
  11. 使用 ViewStub 进行占位:
    • 对那些没有马上用到的资源去做延迟加载,并且还有很多大概率不会出现的 View 更要去做懒加载
  12. 定时清理 App 过时的埋点数据
设置内存兜底策略
更深入的内存优化策略
  1. 使 bitmap 资源在 native 中分配(参考Fresco)
  2. 图片加载时的降级处理:
    • 使用 Glide、Fresco 等图片加载库,通过定制,在加载 bitmap 时,若发生 OOM,则使用 try catch 将其捕获,然后清除图片 cache,尝试降低 bitmap format(ARGB8888、RGB565、ARGB4444、ALPHA8)
  3. 前台每隔 3 分钟去获取当前应用内存占最大内存的比例,超过设定的危险阈值(如80%)则主动
    释放应用 cache(Bitmap 为大头),并且显示地除去应用的 memory,以加速内存收集的过程。
    WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
    
  4. 由于 webview 存在内存系统泄漏,还有 图库占用内存过多 的问题,可以采用单独的进程。
  5. 用类似 Hack 的方式修复系统内存泄漏:(参考booster)
  6. 当应用使用的Service不再使用时应该销毁它,建议使用 IntentServcie
  7. 当UI隐藏时释放内存:在所有 UI 组件都隐藏的时候会接收到 Activity 的 onTrimMemory() 回调并带有参数 TRIM_MEMORY_UI_HIDDEN
  8. 谨慎使用第三方库,避免为了使用其中一两个功能而导入一个大而全的解决方案。
内存优化和架构设计时的两个误区
  1. 内存占用越少越好
    • 当系统内存充足的时候,我们可以多用一些获得更好的性能。当系统内存不足的时候,希望可以做到“用时分配,及时释放”
    • 回顾一下 Android Bitmap 内存分配的变化
      1. 在 Android 3.0 之前,Bitmap 对象放在 Java 堆,而像素数据是放在 Native 内存中。需要手动调用 recycle
      2. Android 3.0~Android 7.0 将 Bitmap 对象和像素数据统一放到 Java 堆中,无需手动调用 recycle; 不过 Bitmap 是内存消耗的大户,把它的内存放到 Java 堆中似乎不是那么美妙。即使华为 Mate 20,最大的 Java 堆限制也才到 512MB,可能我的物理内存还有 5GB,但是应用还是会因为 Java 堆内存不足导致 OOM
      既然讲到了将图片的内存放到 Native 中,我们比较熟悉的是 Fresco 图片库在 Dalvik 会把图片放到 Native 内存中。
      事实上在 Android 5.0~Android 7.0,也能做到相同的效果,只是流程相对复杂一些。
      不过这个“黑科技”有两个主要问题,一个是兼容性问题,另外一个是频繁申请释放 Java Bitmap 容易导致内存抖动
      // 步骤一:调用 libandroid_runtime.so 中的构造函数申请一张空的 Native Bitmap
      Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22);
      // 步骤二:申请一张普通的 Java Bitmap
      Bitmap srcBitmap = BitmapFactory.decodeResource(res, id);
      // 步骤三:使用 Java Bitmap 将内容绘制到 Native Bitmap 中
      mNativeCanvas.setBitmap(nativeBitmap);
      mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint);
      // 步骤四:释放 Java Bitmap 内存
      srcBitmap.recycle();
      srcBitmap = null;
      
      1. Android 8.0 使用NativeAllocationRegistry辅助回收 Native 内存的机制,来实现像素数据放到 Native 内存中。Android 8.0 还新增了硬件位图 Hardware Bitmap,它可以减少图片内存并提升绘制效率
  2. Native 内存不用管
    • 当系统物理内存不足时,lmk 开始杀进程,从后台、桌面、服务、前台,直到手机重启
测量方法
  1. Java 内存分配
    • 跟踪 Java 堆内存的使用情况,这个时候最常用的有 Allocation Tracker 和 MAT 这两个工具。
    • Allocation Tracker 的三个缺点:
      1. 获取的信息过于分散,中间夹杂着不少其他的信息
      2. 跟 Traceview 一样,无法做到自动化分析,每次都需要开发者手工开始 / 结束
      3. 在停止的时候,直到把数据 dump 出来之前,经常会把手机完全卡死
  2. Native 内存分配
    1. Malloc 调试可以帮助我们去调试 Native 内存的一些使用问题,例如堆破坏、内存泄漏、非法地址等。Android 8.0 之后支持在非 root 的设备做 Native 内存调试,不过跟 AddressSanitize 一样,需要通过wrap.sh做包装
    2. Malloc 钩子是在 Android P 之后,Android 的 libc 支持拦截在程序执行期间发生的所有分配 / 释放调用,这样我们就可以构建出自定义的内存检测工具。
内存优化工具

除了常用的内存分析工具 Memory Profiler、MAT、LeakCanary 之外

  1. top
    • top 命令是 Linux 下常用的性能分析工具,能够 实时显示系统中各个进程的资源占用状况,类似于 Windows 的任务管理器。top 命令提供了 实时的对系统处理器的状态监视。它将 显示系统中 CPU 最“敏感”的任务列表。该命令可以按 CPU使用、内存使用和执行时间 对任务进行排序。
    • 输入adb shell top --help 查看它的帮助文档
  2. dumpsys meminfo
    • 输入 adb shell dumpsys meminfo -h 查看它的帮助文档
  3. LeakInspector
    • 腾讯内部的使用的 一站式内存泄漏解决方案,它是 Android 手机经过长期积累和提炼、集内存泄漏检测、自动修复系统Bug、自动回收已泄露Activity内资源、自动分析GC链、白名单过滤 等功能于一体,并 深度对接研发流程、自动分析责任人并提缺陷单的全链路体系。
  4. JHat
    • Oracle 推出的一款 Hprof 分析软件,它和 MAT 并称为 Java 内存静态分析利器。不同于 MAT 的单人界面式分析,jHat 使用多人界面式分析。它被 内置在 JDK 中,在命令行中输入 jhat 命令可查看有没有相应的命令。
  5. ART GC Log
    • GC Log 分为 Dalvik 和 ART 的 GC 日志
  6. Chrome DevTool
    • 对于 HTML5 页面而言,抓取 JavaScript 的内存需要使用 Chrome Devtools 来进行远程调试。方式有如下两种:
      1. 直接把 URL 抓取出来放到 Chrome 里访问。
      2. 用 Android H5 远程调试
    • 纯H5:
      1. 手机安装 Chrome,打开 USB 调试模式,通过 USB 连上电脑,在 Chrome 里打开一个页面,比如百度页面。
        然后在 PC Chrome 地址栏里访问 Chrome://inspect
      2. 直接点击 Chrome 下面的 inspect 选项即可弹出开发者工具界面
    • Hybrid H5 调试
      • (就是我们应用中内嵌h5)
      • Android 4.4 及以上系统的原生浏览器就是 Chrome 浏览器,可以使用 Chrome Devtool 远程调试 WebView,前提是需要在 App 的代码里把调试开关打开,如下代码所示:
      if (Build.VERSION_SDK_INT >= Build.VERSION_CODES.KITKAT && 是debug模式) {
          WebView.setWebContentsDebuggingEnabled(ture);
      }
      
      打开后的调试方法跟纯 H5 页面调试方法一样,直接在 App 中打开 H5 页面,再到 PC Chrome 的 inpsector 页面就可以看到调试目标页面。
内存优化,应该从哪里着手
  1. 设备分级

    • 内存优化首先需要根据设备环境来综合考虑,当然这需要有一个良好的架构设计支撑,在架构设计时需要做到以下几点
    1. 设备分级
      • 低端机用户可以关闭复杂的动画,或者是某些功能;使用 565 格式的图片,使用更小的缓存内存等,
      • 低端机避免使用多进程,一个空进程 也会占用 10MB 内存
        (可以根据手机的内存、CPU 核心数和频率等信息决定设备属于哪一个年份)
      • 针对低端机用户推出 4MB 的轻量版本,例如各种极速版app,如抖音极速版,腾讯视频极速版
    2. 缓存管理
      • 需要有一套统一的缓存管理机制,可以适当地使用内存;当“系统有难”时,也要义不容辞地归还。
      • 我们可以使用 OnTrimMemory 回调,根据不同的状态决定释放多少内存。
      • 方便监控每个模块的缓存大小;
    3. 进程模型
      • 减少应用启动的进程数、减少常驻进程、有节操的保活,对低端机内存优化非常重要
    4. 安装包大小
      • 安装包中的代码、资源、图片以及 so 库的体积,跟它们占用的内存有很大的关系。
  2. 统一图片库,重复图片检测,大图检测等,详见上面的 Bitmap 优化 两种方法:

  3. 内存泄漏

    • 就是没有回收不再使用的内存,主要分两种情况
      1. 同一个对象泄漏
      2. 每次都会泄漏新的对象
    • 优秀的框架设计可以减少甚至避免程序员犯错,当然这不是一件容易的事情,所以我们还需要对内存泄漏建立持续的监控:
      1. Java 内存泄漏:建立类似 LeakCanary 自动化检测方案,至少做到 Activity 和 Fragment 的泄漏检测;
      2. OOM 监控:美团有一个 Android 内存泄露自动化链路分析组件Probe,不过有二次崩溃风险
      3. Native 内存泄漏监控
内存监控
  1. 采集方式
    • 用户在前台的时候,可以每 5 分钟采集一次 PSS、Java 堆、图片总内存, 建议通过采样只统计部分用户;
  2. 计算指标
    • 内存异常率: 内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集 UV
    • 触顶率: 内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集 UV
        long javaMax = runtime.maxMemory();
        long javaTotal = runtime.totalMemory();
        long javaUsed = javaTotal - runtime.freeMemory();
        // Java 内存使用超过最大限制的 85%
        float proportion = (float) javaUsed / javaMax;
    
  3. GC 监控
    • Debug.startAllocCounting 来监控 Java 内存分配和 GC 的情况
        long allocCount = Debug.getGlobalAllocCount();
        long allocSize = Debug.getGlobalAllocSize();
        long gcCount = Debug.getGlobalGcInvocationCount();
        //上面的这些信息似乎不太容易定位问题,在 Android 6.0 之后系统可以拿到更加精准的 GC 信息。
        // 运行的GC次数
        Debug.getRuntimeStat("art.gc.gc-count");
        // GC使用的总耗时,单位是毫秒
        Debug.getRuntimeStat("art.gc.gc-time");
        // 阻塞式GC的次数
        Debug.getRuntimeStat("art.gc.blocking-gc-count");
        // 阻塞式GC的总耗时
        Debug.getRuntimeStat("art.gc.blocking-gc-time");
        //阻塞式 GC 会暂停应用线程,可能导致应用发生卡顿
    
课后作业

参考文章

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

上一篇下一篇

猜你喜欢

热点阅读