美文网首页
Android内存优化

Android内存优化

作者: 34sir | 来源:发表于2019-02-13 11:05 被阅读3次

    内存产生的问题

    • 异常
    • 卡顿 频繁GC 物理内存不足触发 low memory killer

    通过发送SIGQUIT 信号获得ANR日志

    adb shell kill -S QUIT PID
    adb pull /data/anr/traces.txt
    

    包含ANR转储信息 以及GC的详细性能信息

    sticky concurrent mark sweep paused:    Sum: 5.491ms 99% C.I. 1.464ms-2.133ms Avg: 1.830ms Max: 2.133ms     // GC 暂停时间
    
    Total time spent in GC: 502.251ms     // GC 总耗时
    Mean GC size throughput: 92MB/s       // GC 吞吐量
    Mean GC object throughput: 1.54702e+06 objects/s 
    

    两个误区
    误区1 内存占用越少越好
    系统内存足时多用一些以获得更好的性能;
    内存不足时 希望用时分配 及时释放

    关于Bitmap内存分配的变化

    • Android 3.0之前 Bitmap对象存在Java堆 像素数据存在Native内存中
      不手动调用recycle Bitmap Native内存的回收完全依赖finalize 时机不太可控
    • Android 3.0~Android 7.0 Bitmap对象和像素数据统一放到Java堆中 不调用recycle Bitmap内存也会是随对象一起被回收
      不过Bitmap消耗内存太大 还是不合适
    • Android 8.0 利用NativeAllocationRegistry实现像素数据存放到Native中 并且新增了硬件位图 Hardware Bitmap 可以减少图片内存并且提升绘制效率
      NativeAllocationRegistry:
      一种实现 可以将 Bitmap内存放到 Native中 也可以做到和对象一起快速释放 同时GC的时候也能考虑到这些内存防止被滥用

    误区二: Native内存不用管
    将图片的内存放到 Native

    // 步骤一:申请一张空的 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;
    

    测量方法

    adb shell dumpsys meminfo <package_name|pid> [-d]
    

    Java 内存分配

    自定义 Allocation Tracker 需要考虑很多兼容性问题 在 DalvikARTAllocation Tracker的处理流程差异较大

    Native 内存分配

    Android 8.0 之后使用 AddressSanitize 检测内存
    Native内存问题的两种方法:

    • Malloc 调试
      可以帮助我们调试 Native 内存的问题 例如 堆破坏 内存泄漏 非法地址
      Android 8.0 之后支持在非root的设备做Native内存调试 和AddressSanitize 一样 需要通过 wrap.sh做包装
    adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
    
    • Malloc 钩子
      在Android P 之后 Android 的libc 支持拦截在程序执行期间所有 分配/释放 调用 这样我们可以构建出自定义的内存检测工具
    adb shell setprop wrap.<APP> '"LIBC_HOOKS_ENABLE=1"'
    
    代码 图片 资源以及so库和内存的关系.png

    怎么优化?

    Bitmap优化

    • 统一图片库
      图片内存优化的前提是收拢图片的调用 这样可以做整体的控制策略
    • 统一监控
      1⃣️ 大图监控
      我们需要注意某张图片内存占用是否过大 例如长宽远远大于View
      开发过程中 检测到不合规的图片使用 应该立即弹出对话框提示图片所在的Activity和堆栈
      2⃣️重复图片监控
      重复图片指的是 Bitmap 的像素数据完全一致 但是有多个不同的对象存在

    内存泄漏

    简单的说就是没有回收不再使用的内存
    内存泄漏分为两种情况:1⃣️ 同一个对象泄漏 2⃣️ 泄漏新的对象

    • Java内存泄漏
      建立类似 LeakCanary 自动化检测方案 至少做到ActivityFragment的泄漏检测
      内存泄漏线上优化: 对生成的Hprof 内存快照文件做一些优化 裁剪大部分图片对应的byte数组减少文件大小
    • OOM监控
      美团的组件:Probe 在发生OOM的时候生成Hprof内存快照 然后通过单独进程对这个文件做进一步分析 但是线上使用风险较大
    • Native 内存泄漏监控
      Malloc 但不是很稳定
    • 针对无法重编so的情况
      使用了 PLT Hook(Native Hook 的一种方案) 拦截库的内存分配函数 然后重定向到我们自己到实现后记录分配到内存地址 大小 来源so库路径等信息 定期扫描分配和释放是否配对 对于不配对等分配输出我们记录的信息
    • 针对可重编的so
      通过GCC的-finstrument-functions参数给所有函数插桩 桩中模拟入栈出栈操作;
      通过Id的-wrap 参数拦截内存分配和释放函数 重定向到我们自己到实现后记录分配到内存地址 大小 来源so以及插桩记录的调用栈的内容 定期扫描分配和释放是否配对 不配对的输出记录的信息

    开发过程中 内存泄漏排查可以使用 Androd ProfilerMAT 工具配合使用

    内存监控

    采集方式

    用户在前台的时候 可以每5分钟采集一次PSS(比例分配共享库占用的内存 ) Java堆 图片总内存
    建议只统计部分用户

    计算指标

    内存异常率 可以反映内存占用的异常情况
    PSS 的值可以通过 Debug.MemoryInfo 获取

    内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集 UV
    

    UV 指独立访客
    触顶率 可以反映Java内存的使用情况 如果超过85%最大堆限制 GC会变的更加频繁 造成OOM和卡顿

    内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集 UV
    

    是否触顶可以通过下面的方法计算得到:

    内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集 UV
    

    一般客户端只上报数据 所有计算放在后台

    GC监控

    测试环境 可以通过 Debug.startAllocCounting来监控Java内存分配和GC
    对性能有影响 已经被标记为 deprecated
    可以拿到内存分配的次数 大小 以及GC发起的次数

    long allocCount = Debug.getGlobalAllocCount(); //内存分配次数
    long allocSize = Debug.getGlobalAllocSize(); //内存分配大小
    long gcCount = Debug.getGlobalGcInvocationCount(); //GC次数
    

    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的次数和耗时 它会暂停应用线程 可能导致应用发生卡顿

    相关文章

      网友评论

          本文标题:Android内存优化

          本文链接:https://www.haomeiwen.com/subject/xmydsqtx.html