剑客
关注科技互联网

Android性能优化-内存优化

前言

在任何软件开发环境中,RAM都是比较珍贵的资源。在移动操作系统上更是这样,因为它们的物理内存通常受限。尽管在ART和Dalvik虚拟机都会进行垃圾回收的巡航,但这并不意味着你可以忽略何时,何地分配和释放内存。你应该避免内存泄露,通常此后又一些静态成员变量导致,也应该在恰当的时间(定义的一些生命周期回调的方法里)释放所有 Reference
对象。

这里见识了你如何减少app内存的使用。因为android是基于java的,所以对于内存的管理,可以参考java相关的书籍,下面章节中也会有所讲解。关于如何分析app运行中的内存使用,可以参考 Tools for analyzing RAM usage
。关于ART和Dalvik虚拟机管理内存的更多细节,可以参考 Overview of Android Memory Management
.

一 监测可用内存和内存使用

Android 框架,AndrStudio和Android SDK都提供了分析app内存使用的途径。Android框架暴露了几个API,允许你的app动态的减少内存使用、AndroidStudio和Android SDK提供了几种工具帮你分析app的内存使用情况。

分析RAM使用的工具

  1. Device Monitor 拥有一个 Dalvik Debug Monitor Server (DDMS) 工具,可以帮助你检测app进程中内存的分配。你可以通过该信息去分析app的总体内存使用情况。比如,先执行垃圾回收事件然后再去看那些仍然保留在内存中的对象。通过这种方式去定位app中所进行的内存分配或者遗留在内存中的对象。

    更多关于DDMS的使用请参考 Using DDMS

  2. Android Studio中的Memory Monitor 可以向你展示某一个过程中的内存分配情况。该工具以图形化的方式展示了某一时段可用的和已经分配的java内存,以及发送的垃圾回收事件。也可以触发垃圾回收事件并获取app运行期间java堆内存的快照。Memory Monitor tool 的输出信息也可以帮你定位到app密集发生垃圾回收事件的点,这些点会降低了app速度。

    关于如何使用Memory Monitor tool的更多信息可以参考 Viewing Heap Updates
    .

  3. 垃圾回收事件也会展示在 Traceview viewer中。 Traceview 允许你以时间线的方式查看trace log文件,并可以分析一个事件段内都发生了什么。你可以使用该工具确定在你的代码在垃圾回收事件发生时都做了什么操作。

    更多信息关于如何使用Traceview viewer, 可以参考 Profiling with Traceview and dmtracedump
    .

  4. Android Studio中的Allocation Tracker tool可以帮助你分析app是如何分配内存的。Allocation Tracker 记录了app内存的分配并在快照中列出了所有的分配对象。可以使用该工具追踪哪些地方分配了过多的对象。

    更多关于如何使用Allocation Tracker tool,可以参考 Allocation Tracker Walkthrough
    .

依据事件释放内存

根据RAM的物理内存和设备的操作行为,Android设备可以在变化的可用内存中运行。在内存压力的情况下,系统的广播信号会提示,app可以监听这些信号然后对内存的使用做恰当的处理。

可以使用 ComponentCallbacks2
来监听内存信号响应app生命周期或者设备的事件。 onTrimMemory()
方法可以帮助你监听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()
方法是在Android 4.0(API 14)中加入的,你可以使用在其它的低版本中使用 onLowMemory()
回调,相当于 TRIM_MEMORY_COMPLETE
事件。

确定你应该使用多少内存

为了允许多进程,Android对每个app在堆内存的大小上设置了严格的限制。由于不同的设备的总的可用RAM内存不同,准确的堆内存的大小限制也不同。如果你的app达到了堆的内存限制,并且尝试分配更多内存,系统就会抛出OOM。

为了避免OOM,可以通过调用 getMemoryInfo()
方法去查询当前设备的可用内存堆内存空间。该方法会返回一个 ActivityManager.MemoryInfo
对象,该对象提供了设备的内存状态信息,包括可用内存,总内存和内存阀值(当内存低于该值得时候,系统将会杀死进程)。 ActivityManager.MemoryInfo
类也暴露了一些简单的boolean字段, 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 classes ,以及代码结构有时候会占用更多内存,我们应该通过选择更高效的方案来减少内存的使用。

节省的使用Service

在后台保留一个不需要的service,是最糟糕的内存管理方式之一。如果你需要通过service在后台去执行任务,除非当它将要去执行一个任务的时候,否则不应该一直在后台驻留。在完成任务后记得将service停掉,否则可能在不经意间导致了内存泄露(这里应该是指service占用了无用的资源)。

当你启动一个service的时候,系统会更好的保留service所运行的进程。这样会导致service 进程非常的消耗资源,因为被一个service占用的RAM部分,其它Servie就不可用了。这样会减少系统在LRU cache中缓存的进程数量,降低了app的切换效率。当内存紧张时,甚至会导致内存抖动,并且系统无法维护足够的进程来托管当前运行的所有服务。。

尽量避免使用持久化的service,因为它持有占有了可用内存。建议使用其它替代方案,比如 JobScheduler
。关于 JobScheduler
如何调度后台进程,可以参考 Background Optimizations

如果必须使用service,最好是使用 IntentService
来限制service的生命周期,一旦处理完启动它的Intent,该IntentService就会将自己停掉。更多信息可以参考 Running in a Background Service
.

使用内存更高效的代码结构

一些编程语言中的类没有针对移动设备进行优化。比如,通用HashMap实现可能是相当的内存低效,因为它需要为每个映射关系创建单独的对象。

Android框架中提供了几种优化过的数据结构,比如 SparseArray
, SparseBooleanArray
LongSparseArray
,比如 SparseArray
避免了对key的自动装箱(导致每个实体会多创建对象),所以更高效。

小心的使用代码抽象

开发人员经常简单地使用抽象作为一个好的编程实践,因为抽象可以提高代码的灵活性和维护性。但是抽象会带来严重的开销:通常它们需要额外执行相当多的更多代码,需要更多的时间和RAM将代码映射到内存中。因此,如果你的抽象不能带来比较大的好处,那么请避免使用抽象。

比如,枚举相对于静态通常需要两倍甚至更多的内存。你应该严格避免在android中使用枚举。

使用nano protobufs进行序列化

Protocol buffers
是由Google研发,跨语言、跨平台,扩展性非常好的序列化数据结构,类似XML,但是更小更快,更简单。如果你决定使用protobufs进行序列化,你应该在客户端代码中使用nano protobufs。因为一般的protobufs会生成冗余的代码,导致各种问题,比如增加RAM的使用,APK大小,比较低的执行效率。

更多信息可以参考 protobuf readme
“Nano version”部分

避免内存抖动

上面提到,通常垃圾回收事件不会影响到你的app性能。但是,在短时间内突然发生很多的垃圾回收事件就会占用了帧的时间。系统花费在垃圾回收上的时间越多,那么用于渲染或者音频的时间就越少。

通常,内存抖动会导致大量的垃圾收集事件发生。在实践中,内存抖动是指在在一个特定时间内分配了很多临时对象。

比如,你可能在for 循环中分配了多个临时对象。或者在onDraw中创建了新的Paint或者Bitmap对象。这两种情况下,app都会快速创建大量的对象。这样会快速消耗掉young generation中的可用内存,强制垃圾回收器触发回收事件。

通过 Analyze your RAM usage
可以帮你找到代码中哪些地方导致了内存抖动。

一旦定位到了问题,就应该试着在性能问题严重的地方减少对象的分配。考虑将他们移到内部循环外面,可能的话,也可以通过 Factory模式
来实现。

三 移除内存敏感的资源和库

一些资源和库会在你不知道的情况下占用掉很多内存。总览下apk的大小,包含了哪些可能导致内存浪费的第三方的库和内嵌资源。通过移除冗余的。不必要的资源和库来提高内存的使用。

减小总体的APK大小

您可以通过减少应用程序的整体大小来显着降低应用程序的内存使用量。Bitmap 大小、资源、动画帧数,和第三方库都可能增大了APK的大小。Android Studio 和 Android SDK 提供了一些工具可以帮你减少资源和外部依赖。

如何如何减少APK的大小,可以参考 Reduce APK Size
.

小心的使用注解框架

比如Guice或者RoboGuice的依赖注解框架虽然简化了你的代码书写,并为测试或者其它的可能变化配置信息提供了适配。但是这些依赖框架并没有针对移动设备做优化。

比如,这些框架通常会通过扫描你的代码或者注解来进行初始化。系统会将这些映射页面分配到内存中,以便Android可以删除它们; 但这些映射页面所占用的内存会在很长一段时间之后才会被删除。

如果你需要使用注解框架,考虑使用 Dagger
。比如,Dagger不会使用反射扫描代码。Dagger的严格实现意味着它可以用于Android应用中而不会增加不必要的内存使用。

小心的使用外部库

外部库的代码通常不会为移动环境而写,执行在移动设备上也更低效。当你决定使用一个外部库的时候,可能需要为移动设备做优化。在你决定使用之前,先考虑代码量的大小和RAM占用空间。

有时一些针对移动设备优化的库由于不同的实现也会产生问题。比如,某个库可能使用了nano版本的protobufs,然而另一个使用了micro protobufs,就会导致两个版本的实现。这样就会导致两份

logging,analytics,Image loading框架、缓存以及其它一些不期望的问题。

尽管 ProGuard
会帮助你移除API和资源,但是不会移除一个库的内部依赖。如果你使用了依赖库中的Activity子类(会导致比较宽泛的依赖关系,和你现有的Activity就会有很大冲突),问题将尤为严重。又或者,如果这个库中使用了反射等技术,你还要花大量时间去处理混淆。所以在你决定使用一个库的时候,需要慎重的考虑它是否非常匹配你的需求,否则你应该考虑自己实现一套。

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址