App内存占用优化

RAM(Random-access memory)在任何软件开发中都是非常宝贵的资源,移动操作系统由于其物理内存的局限性更是如此。尽管ART(Android Runtime)与Dalvik虚拟机会执行常规的垃圾回收,但这并不意味着可以忽略App中的内存分配与释放。我们应当避免引起内存泄露,如持有静态成员变量而导致无法释放,应当在应用的生命周期回调中释放掉所有的引用。

本文主要介绍如何减少App中的内存使用。

监控可用内存及内存使用状况

Android 框架与Android Studio可以帮助我们来分析和调整App的内存使用,其中Android框架提供了一些API来帮助App在运行时动态减少内存占用,Android Studio包括一些工具来查看内存的使用情况。

RAM使用分析工具

在优化内存问题之前,需要先找到这些问题,Android Studio及Android SDK提供了几个工具用来分析App中的内存使用:

  1. Android Studio中的Memory Monitor

    该工具可以显示一个会话过程中的内存分配情况,有一个可视化的图形界面,可以看到Java内存随时间的变化情况以及GC事件。当App运行时,可以启动GC操作并且获取Java Heap的快照。该工具的输出可以帮助我们定位哪里容易导致频繁的垃圾回收,从而导致应用程序变慢。

  2. Android Studio中的Allocation Tracker工具

    该工具记录了一个App的内存分配情况并在分析快照中列出了所有分配的对象。可以使用此工具找到分配过多对象的部分代码。

响应回调释放内存

不同的Android设备或不同的用户操作会导致不同的内存占用状况,Android系统在遇到内存压力的情况下会发出信号预警,App需要监听这些信号来调整内存的使用。

可以使用ComponentCallbacks2 API来监听回调以调整内存使用状态。onTrimMemory()可以允许App监听内存相关的事件,无论App是在前台运行还是在后台运行。下面是一个示例,通过实现Activity的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;
        }
    }
}

onTrimMemory()回调是在Android4.0(API Level 14)添加的,对于之前的版本,可以使用onLowMemory()回调替代,它大致相当于TRIM_MEMORY_COMPLETE事件。

检测应该使用多少内存

Android为了支持多进程,因此为每个App占用的内存做了限制。由于不同设备的RAM大小不同,因此分配给每个App的Heap大小也会有差异。当App已经到达了特定的Heap限制,如果再进行内存分配的话,就会抛出 OutOfMemoryError异常。

为了避免内存溢出,我们可以通过getMemoryInfo()查询当前设备上还有多少可用的内存空间,该方法返回一个ActivityManager.MemoryInfo对象,它包含了设备当前的内存状态,如可用内存、总内存以及内存阈值(当可用内存低于该阈值时,系统就会杀死部分进程)等信息。ActivityManager.MemoryInfo有一个叫做lowMemory的布尔属性,表示设备是否处于低内存状态。

下面是一个使用getMemoryInfo()的示例:

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;
}

使用高效、内存占用少的代码结构

一些Android特性、Java类、代码结构会使用更多的内存,可通过使用更高效的代码结构来节省内存。

有节制地使用Service

让一个不再需要的Service保持运行是Android开发中最糟糕的内存管理错误之一。如果App需要Service来执行后台任务,需要在它完成任务时终止它,否则可能导致内存泄露。

当启动一个Service时,系统会保持该服务所在的进程,这样该服务占用的内存将不能被其他进程使用。同时系统通过LRU Cache缓存的的进程数量也将减少,从而降低进程间切换的效率。当内存紧张或系统无法为当前所运行的Service提供足够的进程时还会发生系统抖动。

应当避免使用持久运行的Service,因为它们对内存有持续的需求,建议使用JobScheduler

如果必须使用Service,可以使用IntentService来限制其生命周期,IntentService会在处理完任务之后终止。

使用优化的数据容器

一些编程语言提供的类可能并未针对移动设备做优化,例如通用的HashMap实现是比较低效的,因为每一个映射都需要一个单独的Entry对象。

Android框架提供了一些优化过的数据容器,如SparseArraySparseBooleanArrayLongSparseArray。例如SparseArray更加高效,是因为它避免了对key及一些value的自动装箱操作。

谨慎使用代码抽象

开发者经使用抽象来简化编程,因为抽象可以提高代码的灵活性,也更方便维护。但是抽象会带来明显的内存消耗:抽象一般来说需要执行更多的代码、需要更多的时间以及RAM空间来将代码映射到内存。所以如果抽象不能带来明显的好处,应当避免使用代码抽象。

例如,枚举占用的内存通常是静态常量的两倍,需要严格避免在Android中使用枚举。

使用Nano版本protobufs序列化数据

Protocol buffers是Google出品的独立于平台及语言的、可拓展的结构化数据序列化技术,它类似于XML,但更加轻量级、快速、简洁。如果决定使用protobufs来序列化数据,应该在客户端选择使用Nano版本,因为常规版本的protobufs会生成极其冗长的代码,从而导致App端出现各种问题,如内存溢出、APK大小增加、执行速度变慢等。

Nano版本Protobufs的相关参考:protobuf readme

避免内存泄露

垃圾回收通常不会影响应用的性能,但是短时间内的垃圾收集将会占用帧时间,垃圾回收占用的时间越多,用到其他事情上的时间就越少。

通常,内存泄露会导致频繁地垃圾回收事件的发生,在实践中,内存泄露描述了给定时间内分配的临时对象的数量。

例如,可能在for循环中分配多个临时对象,或者在View的onDraw()方法中创建多个Paint、Bitmap对象。上述情况下,App会快速创建大量对象,从而迅速消耗掉新生代中的内存,导致GC的发生。

我们需要找到内存泄露的地方并进行修复,如将实例化操作移出for循环,不要在onDraw()这种频繁调用的方法中创建对象。

移除内存密集型的资源和库

代码中的一些资源及Library可能会在我们不知情的情况下吞噬内存。一个APK中,第三方库或者嵌入的资源会影响到App占用的内存总量。可以通过移除冗余的资源、臃肿的组件及不必要的Library来优化内存消耗。

减小APK大小

通过减小APK的大小可以明显减低App对内存的占用。Bitmap大小、资源、动画帧图像以及第三方库影响APK的大小,Android Studio及Android SDK提供了一些工具用来减少资源大小及外部依赖。

更多关于APK的瘦身方案,可参考Reduce APK Size

使用Dagger2实现依赖注入

依赖注入框架可以简化代码并为测试及其他配置变化提供适配环境。

如果打算在App中使用依赖注入框架的话,建议使用Dagger2。Dagger没有使用反射,它的静态、编译时实现意味着它不需要运行时成本及内存消耗。

其他使用反射的依赖注入框架需要扫描代码来寻找注解,这个过程可能需要更多的CPU周期和RAM,并可能导致应用程序启动的明显滞后。

谨慎使用外部Library

第三方的Library代码可能并非为移动环境所写,当应用到移动客户端时可能导致性能降低。当决定使用一个第三方库时,需要针对移动环境做优化,另外需要分析Library的代码大小及内存占用情况,最后再决定是否应用该Library。

哪怕是一些针对移动环境做过优化的Library因为不同的实现也可能导致一些问题,如一种情况使用了Nano版的protobufs,另一种情况使用了Micro版的protobufs,不同的Library实现有可能导致意想不到的问题。

尽管Proguard可以移除无效的API及资源,但是它无法移除一个Library大的内部依赖。这些库中的功能可能需要较低的依赖项,例如当使用一个库提供的Activity子类时,可能会引入大量的依赖。

需要避免只使用一个库中的有限的功能而引入库的情况,我们不希望引入大量不需要的代码。当决定是否使用一个Library时,尽可能高度匹配我们的需求,否则,可以考虑自己实现。

参考文献Manage Your App’s Memory

发布了115 篇原创文章 · 获赞 143 · 访问量 47万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章