内存优化总结

内存优化分为两部分,一部分是架构,包括设备分级,缓存管理,进程管理与Bitmap图片库的策略;另一部分是监控,而且尤为重要,因为大部分内存泄漏和OOM等问题,更多的时候难点不在解决它们而是如何发现它们。


内存优化总结_第1张图片
思维导图

内存优化方式

内存优化可以从架构设计Bitmap优化内存泄漏三方面进行优化。

1. 架构优化

内存优化首先需要根据设备环境来综合考虑,这依托于一个良好的架构支撑。在架构设计时要考虑以下几点:

设备分级

根据设备环境来综合考虑。对于低端机用户可以关闭复杂的动画或是某些功能;使用565格式的图片,使用更小的缓存内存等。在开发过程中要思考功能要不要对低端机开启,在系统资源吃紧的时候能不能做降级。
device-year-class这个fb的开源库可以根据手机内存,CPU核数和频率等信息来决定设备属于哪一个年份,我们可以有以下方式来做分级策略:

if (year >= 2013) {
    // Do advanced animation
} else if (year >= 2010) {
    // Do simple animation
} else {
    // Phone too slow, don't do any animations
}

缓存管理

需要有一套统一的缓存管理机制,可以适当地使用内存,而当系统内存不足时,要适当归还。可以使用ComponentCallbacks2.OnTrimMemory()回调,根据不同的状态决定释放多少内存。

进程模型

一个空的进程也会占用10MB的内存。减少应用启动的进程数,减少常驻进程,有节操地保活,对低端机内存优化很重要。

安装包大小

安装包中的代码,资源,图片以及so库的体积,跟它们占用的内存有很大的关系。一个80MB的应用很难在512MB内存的手机上流畅运行。这种情况我们需要考虑针对低端手机用户推出4MB的轻量版本,例如Facebook Lite,今日头条极速版都是这个思路。其关系如下:

classes.dex Bitmaps resources .so files
Java Heap class objs pixel data AssetManager
Native Heap pixel data ParsedZipFile
Code .dex mmap .apk mmap .so mmap
Stack
Graphics pixel data
Private Other runtime metadata

2. Bitmap优化

把所有图片都放到Native内存,可以提升系统内存利用率,减少GC带来的问题。例如Fresco:

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

统一图片库

图片内存优化的前提是收拢图片的调用,这样可以方便做整体的控制策略。例如低端机可以使用565格式,更加严格的缩放算法,可以使用Glide,Fresco等,而且需要进一步将所有Bitmap.createBitmap,BitmapFactory相关的借口也一并收拢。

统一监控

  1. 大图片监控
  2. 重复图片监控
  3. 图片总内存监控。统计应用内所有图片占用的内存,在线上按照不同的系统,屏幕分辨率等维度去分析图片内存等占用情况。当发生OOM时,也可以把图片占用的总内存和TopN的图片的内存都写到崩溃日志中帮助排查问题。

3. 内存泄漏

  1. Java内存泄漏。建立类似LeakCanary自动化检测方案,在泄漏发生时提示开发者。内存泄漏监控放到线上时,可以对生成的Hprof内存快照文件做一些优化,裁剪大部分图片对应的byte数组减少文件大小,提高文件上传率。
  2. OOM监控。美团有一个 Android 内存泄露自动化链路分析组件Probe,它在发生OOM时生成Hprof内存快照,然后通过单独进程对文件作进一步分析。
  3. Native内存泄漏监控。参考微信 Android 终端内存优化实践
  4. 针对无法重编so的情况,使用了 PLT Hook 拦截库的内存分配函数,重定向到我们自己的实现后记录分配的内存地址、大小、来源 so库路径等信息,定期扫描分配与释放是否配对,对于不配对的分配输出我们记录的信息。
  5. 针对可重编的so的情况,通过 GCC 的“-finstrument-functions”参数给所有函数插桩,桩中模拟调用栈入栈出操作。
    开发过程中内存泄漏排查可以使用Android Profiler和MAT配合使用,而日常监控关键是成体系化,做到及时发现问题。

内存监控

1. 采集方式

用户在前台的时候,可以每隔5min采集一次PSS,Java堆,图片总内存。通过采样只统计部分用户,需要注意的是按照用户抽样,而不是按次抽样。如果一个用户如果命中采集,那么在一天内都要持续采集数据。

2.计算指标

  • 内存异常率:反应内存占用的异常情况,如果出现新的内存使用不当或内存泄漏的场景,这个指标就会上涨。其中PSS的值可以通过Debug.MemoryInfo拿到。
内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集 UV
  • 触顶率:反应Java内存的使用情况。如果超过85%最大堆限制,GC会变得更加频繁,容易造成OOM和卡顿。
内存 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;

后台服务器取得这些数据之后,可以计算平均PSS,平均Java内存,平均图片占用等指标,它们可以反应内存等平均情况。通过平均内存和分区间内存占用这些指标,我们可以通过版本对比来监控有没有新增内存等相关问题。
结合前台上报时间,还可以按照时间维度看应用内存等变化曲线,观察应用的使用情况是否合理,还可以按照场景来对比内存的占用。

3. GC监控

通过监控,我们可以拿到内存分配的次数和大小,以及GC发起次数等信息。

long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();

在Android6.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的次数,因为GC会暂停应用线程,可能导致应用发生卡顿。


10.21 最近从Android Developer性能优化栏目中看到一些总结,稍微补充一下。

内存和内存使用

Release memory in response to events

这里主要针对的是上面说到的内存归还,我们可以通过onTrimMemory来监听内存事件。

import android.content.ComponentCallbacks2;
// Other import statements ...

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code ...

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that was raised.
     */
    public void onTrimMemory(int level) {

        // Determine which lifecycle or system event was raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory that your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                   begin killing background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                   the first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app received an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

Check how much memory you should use

Android为每个app分配的堆大小是有限的,这个限制基于当前RAM可用的总内存。我们可以通过ActivityManager#getMemoryInfo 获取``ActivityManager.MemoryInfo对象来查询当前内存的状态。后者暴露了一个简单的boolean值lowMemory用于告知你当前设备是否运行在一个低内存状态。

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work ...
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

use memory-efficient code constructs

这里讲解的就是从代码结构上进行优化。

慎用Service

当Service不在需要时仍然让其运行,是Android app最糟糕的内存管理错误之一。记得及时stop service,否则可能意外地触发内存泄漏。
当start 一个service时,系统会偏向于让service所在的进程存活以便让service能够运行。这个行为使得service进程变得更加昂贵,因为这个app占用的RAM别的进程是无法使用的。这使得系统中保持的LRU cache 进程数量减少,也就导致切换app时的效率降低。
如果不是必须使用长期运行的service的话,建议少用。官方推荐使用JobScheduler,以及IntentService

使用优化过的数据结构

使用Android提供的内存效率更高的数据结构可以帮助开发者减少自身APP的内存占用。包括SparseArray, SparseBooleanArray, and LongSparseArray.

慎用抽象

如果滥用abstract特性,反而会造成需要更多的时间和RAM去把code映射到内存。

Use lite protobufs for serialized data

Protocol buffers are a language-neutral, platform-neutral, extensible mechanism designed by Google for serializing structured data—similar to XML, but smaller, faster, and simpler.

避免内存抖动

这里只频繁的GC给app带来的影响。避免在for循环中创建对象,在onDraw中创建Paint等情况

移除内存敏感的资源和库

apk瘦身

详见官方说明

  • 移除未使用的资源
  • 最大限度减少库中资源的使用
  • 仅支持特定密度
  • 使用可绘制对象
  • 重复使用资源
  • 从代码进行渲染
  • 压缩图片文件
  • 使用WebPage文件格式
  • 使用矢量图形
  • 将矢量图形用于动画图片
    ...

使用Dagger2用于依赖注入

谨慎导入外部库

更多。。。

分享几篇写得不错的博客:

  • Android性能优化之内存优化
  • 深入探索Android内存优化

你可能感兴趣的:(内存优化总结)