RAM对于软件开发环境而言是有价值的资源,但它对受限于物理内存限制的操作系统具有更大的价值。即使Android RuntimeDalvik virtual machein执行常规的垃圾回收,但这并不意味着你可以忽略app在何时何地指派和释放内存。你仍然需要去避免产生内存泄露。比如长期持有静态成员变量常常引起内存泄露。我们应该在合适的时间,比如在生命周期回调函数处释放一些引用对象,来避免内存泄露的发生。

  这篇文章将阐述怎样在app中主动的降低内存消耗。关于java编程中清理资源的一般实践,请参照其他关于资源引用管理的书籍或者在线文档。如果你想分析一个运行中app的内存情况,请阅读文章“Tools for analyzing RAM usage”。如果你想知道AndroidRuntimeDalvik virtual machine如何管理内存,请参阅文章“Overview of Android Memory Management”。


(一)监控内存的使用情况:

  AndroidframeworkAndroid Studio能够帮助你分析和调整app的内存使用情况。Android framework 暴露了几个在app运行时动态降低内存消耗的APIAndroid Studio 提供了几个观察app内存使用情况的工具。


分析内存使用的工具:

  想要解决app产生的内存问题,我们首先得知道内存问题由什么引起。Android Studio提供了如下几个分析内存的工具:

  1.MemoryMonitor展示app 在一个单独回话过程中如何指派内存。这个工具会实时绘制包括垃圾回收事件等可用内存和已占用内存的模拟图像。在程序运行时,你仅仅能通过初始化一个垃圾回收事件,获取Java 堆快照。这个Memory Monitor 的输出图像能帮你定位到app在什么地方因产生过多的垃圾回收事件导致程序变慢。

关于Memory  Monitor的具体使用方法,请参阅“ViewingHeap Updates

  2.AllocationTracker”能够让我们详细的跟踪到app如何分配内存。这个工具能够记录app的内存分配情况,而且能够列出所有被分配的对象以及分析快照。用这个工具你能够追踪到分配太多对象的代码。

  关于AllocationTracker的具体使用方法,请参阅“Allocation Tracker Walkthrough


(二)在合适的时间释放内存     

  Android设备在运行时的可用内存会根据物理内存总量以及用户操作方法不断变化。当系统内存有压力的情况下会发送信号来通知程序。app应监听这些通知,并依据监听结果来调整内存使之使用适度。

APIComponentCallbacks2 来响应app生命周期事件 设备事件,根据监听相关信号的结果调整内存的使用。方法onTrimMemory()能够监听app运行在前台或后台时候的相关内存事件。

Activity中,实现回调方法onTrimMemory()来监听这些内存变化的事件,如下代码片段:

import android.content.ComponentCallbacks2;
// Other import statements ...
publicclassMainActivityextendsAppCompatActivity
    implementsComponentCallbacks2{

    // Other activity code ...

    /**
     * Release memory when the UI becomes hidden or whensystem resources become low.
     * @param level the memory-related event that wasraised.
     */
    publicvoid onTrimMemory(int level){

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

            caseComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                  Release any UI objects that currently hold memory.

                   Theuser interface has moved to the background.
                */

                break;

            caseComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            caseComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            caseComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

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

                   Thedevice is running low on memory while the app is running.
                   Theevent raised indicates the severity of the memory-related event.
                   Ifthe event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                   beginkilling background processes.
                */

                break;

            caseComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            caseComponentCallbacks2.TRIM_MEMORY_MODERATE:
            caseComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                  Release as much memory as the process can.

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

                break;

            default:
                /*
                  Release anynon-critical data structures.

                  The appreceived an unrecognized memory level value
                  from thesystem. Treat this as a generic low-memory message.
                */
                break;
        }
    }
} 

备注:回调方法onTrimMemory()Android4.0中添加。更早的版本,能用回调方法onLowMemory()作为替代,大约和 TRIM_MEMORY_COMPLETE 事件相同。


(三)检查你需要用到的内存:

  为了支持多进程,android为每一个app设置了一个堆内存上限。不同设备为app分配的具体上限值,取决于RAM的总大小。如果程序到达了这个上限值还想占用更多的内存,系统将会抛出异常 OutOfMemoryError

  为了避免内存溢出,我们可以查询当前设备为app指定的内存上限值。我们可以通过调用方法getMemoryInfo()来查询当前系统的这个属性值。这个方法将会返回一个包含可用内存、总内存、内存临界值(如果系统可用内存低于这个临界值将会杀掉部分进程)等当前设备状态的ActivityManager.MemoryInfo对象。ActivityManager.MemoryInfo也提供了一个boolean类型的字段lowMemory,记录当前设备是否运行在低内存情况下。

下面的代码片段展示了一个使用geMemoryInfo()方法的例子:

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.
privateActivityManager.MemoryInfo getAvailableMemory(){
   
ActivityManager activityManager =(ActivityManager)this.getSystemService(ACTIVITY_SERVICE);
   
ActivityManager.MemoryInfo memoryInfo =newActivityManager.MemoryInfo();
    activityManager
.getMemoryInfo(memoryInfo);
   
return memoryInfo;
}

(四)使用高效内存的代码结构

    一些android特性、java类、代码结构趋向于用更多的内存。我们可以通过选择更高效的替代方案来使我们app用最少的内存。

 

(1)少用Services

 在不必要的时候运行一个服务,是app最糟糕的内存管理错误之一。如果app需要运行一个服务在后台执行工作,除非有必要的任务,否则不要一直运行。当Service完成它的任务后一定要记得停这个Service。否则,可能一不留神就产生一个内存泄露。

 开启一个Service后,系统喜欢为这个service开启一个进程使其持续运行。这种情况使Service非常耗费资源,因为其他进程无法使用被service所在进程占用的RAM资源。系统缓存在LRU catche的进程数量减少后,使app运行效率降低。更有甚者,当内存紧张,系统无法维持足够的进程支持所有service的时候,service可能被销毁。

 通常情况下我们应该避免长期使用service,因为它们会一直占用内存。我们推荐使用可供选择的类比如JobScheduler 去调度后台进程。

 关于如何使用JobScheduler调度后台进程,参阅“BackgroundOptimizations


(2)使用最优化的数据容器:

  编程语言提供的部分类对于移动设备不是最优化的。例如一般的HashMap实现能使很多内存低效率工作,因为它需要为每一个映射生成一个单独的条目对象。

Androidframework包含几个最优化的数据容器,包括SparseArraySparseBooleanArrayLongSparseArray。例如 AparseArray之所以更高效,是由于它们避免系统的需要,对key(有时候也对value)进行自动装箱。

如果有必要,我们可以总是选择原始的数组作为最精炼的数据结构。

(3)谨慎使用抽象代码:

  开发者通常仅仅把抽象当成一种好的编程实践,因为抽象可以提升代码的灵活性和可维护性。然而,抽象伴随着巨大的资源耗费而来:通常它们会需要更多的时间和更多的内存,需要执行相当数量的代码,花费更多的时间和内存。所以如果抽象不能带来巨大好处,我们应该尽可能去避免。

例如:枚举需要消耗的内存通常是静态变量的两倍多。我们应该严格避免在android上使用枚举。

(4)使用nanoprotobufs序列化数据

    Protocol buffers 是一个谷歌设计的具有语言中立、平台中立、可扩展、用来序列化结构数据的类似于XML的机制,但其比XML快,比XML小,比XML简单。如果你决定使用protobufs处理数据,那么我们应该在客户端代码中一直使用nano protobufs。普通的protobufs通常生成非常多的代码,这样会引起app产生很多问题,比如占用更多内存、APK尺寸大幅度增加、运行缓慢。

更多运行,请参阅文章"protobuf readme""Nanoversion"的段落。

 

(5)避免内存抖动

  就像上文提醒到的,GC事件通常不会影响app的性能。然而一些发生在短时间内的GC事件能迅速耗尽你的帧像时间。系统花费在GC上的时间越多,那么它处理其他诸如渲染或者音频流的事件。

  通常情况下下,内存抖动能引起大量的的GC事件。在实践中,内存抖动描绘的是,在一个给定时间内,分配临时对象的数目。

  例如,你可能通过for循环分配多个临时对象。或者在一个ViewonDraw方法中创建一个Paint或者Bitmap对象。如上两个例子,app迅速地创建了大量的大体积对象。年青一代的这些行为将会迅速的消耗所有的可用内存,强制一个垃圾回收事件产生。

  当然,在你修复内存抖动之前,需要先发现导致内存抖动发生的代码所在地方。请用在文章"Analyze your RAM usage"中讨论的工具。

  一旦你识别代码出现问题的区域,试着降低处于性能临界区域的配置数目。考虑移除内循环的东西,或者将它们移到基于分配结构的 Factory(参阅文章:)。

 

(五)降低内存占有量——累赘的资源和库


  一些资源和库在你的代码能在你不知道的情况下贪婪的消耗内存。apk的大小、包括第三方库以及嵌入的资源、能够影响app消耗的内存量。我们能通过移除代码中多余的、不必要的、臃肿的组件、资源或者库的方式来改善app的内存消耗。


(1)降低apk大小

  我们能通过降低apk大小的方式显著的降低app的内存使用情况。bitmap尺寸、资源、动画的帧、第三方库都能导致apk变大。android sdk android studio 提供了多种工具来帮助我们降低资源和外部依赖的尺寸。

  关于降低apk大小的更多信息,参阅"ReduceAPK Size"

(2)如果需要依赖注入,建议使用Dagger

  依赖注入框架能够简化你写的代码以及为测试和测试改变提供一个适配的环境。如果你打算在app中用一个依赖注入框架,考虑使用Dagger2(参阅https://google.github.io/dagger/.

Dagger不会用反射扫描app代码。它是静态的、编译时实现的框架,这意味着在运行时没有不必要的运行时花费或者内存使用。

  其他的那些依赖注入框架通过扫描代码和注解,运用反射去初始化进程。这个产生的进程会需要大量的CPU周期以及内存,能在app启动时引起一个显而易见的延迟。

 

(3)谨慎使用依赖库:

  外部库的代码通常不是为手机环境所写,使其在手机客户端上工作时可能是很低效的。当我们决定用一个依赖库,可能需要针对移动设备进行优化。在决定真正使用这个依赖库之前,应预先做好针对移动设备优化的计划,依据代码尺寸和内存大小进行分析。

  即使一些移动优化的库也能由于不同的实现引起问题。例如,当一个库使用了micro protobufs而一个库使用了nano protobufs,这样app中有两个不同的 protobuf实现。当问题发生时,原因可能是logginganalyticsp_w_picpath loading frameworkscaching或者一些其他我们无法预料的事情的不同实现导致的。

  尽管ProGuard(参阅:https://developer.android.com/studio/build/shrink-code.html)能根据正确的标记移除APIs和资源,但是它不能移除一个库大型的依赖。你在库中想要的特性可能需要低版本的依赖....

  翻译整理,更多内容,敬请期待....