Generational Heap Memory模型:

Android系统里的Generational Heap Memory的模型是一个三级Generation的内存模型,它包括Young Generation,Old Generation,Permanent Generation三个部分。最新分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间超过某个值的时候,会被移动到Old Generation,最后到Permanent Generation区域。三个区域对象创建速度和GC执行的速度是不一样的Young Generation最快,Old Generation次之,Permanent Generation最慢。整个结构如下图所示:


三个区域的存储空间都有一个固定的大小,当这些对象总的大小快达到阀值时,会触发GC的操作,以腾出空间来存放其他新的对象。

Android 开发过程中的常见内存异常:

在Android 开发过程中主要的内存异常有内存抖动,内存泄露,内存溢出(OOM)等几种。

内存抖动:

内存抖动就是因为在短时间内存在大量对象被创建和销毁,导致内存大小极度不稳定的情况,内存抖动可以给性能带来什么直观的感觉呢?我们知道界面卡顿一般都与CPU和GPU有关,但是内存抖动也会导致卡顿的问题,要知道这个原因首先需要明白在执行GC操作的时候,当前所有线程的任何操作都要暂停,等待GC操作完成之后,其他操作才能够继续运行。一般来说每个GC并不会占用太多时间,但是大量频繁的GC则有可能会占用帧间隔时间。导致卡顿现象。

那么为什么会出现频繁的GC现象呢?上面提到过导致GC频繁发生的原因是由于短时间内大量对象被创建,瞬间产生的大量对象会占用大量的Young Generation的内存区域很有可能导致该区域的内存达到阈值,从而触发GC。我们上面讲到GC过程中暂停当前线程的所有操作,影响到帧率,给用户照成卡顿的不好体验。

发生内存抖动常见的原因:
  • 在循环内部创建新的对象
  • 在自定义View的onDraw方法中创建新的对象
    我们知道onDraw方法是执行在UI线程的,在UI线程中创建对象本身是允许的,并且创建对象本身不会花费太多的系统资源,但是要考虑到一点就是设备存在刷新频率,每次刷新就有可能调用onDraw方法,如果太多频繁得调用onDraw方法极有可能照成内存抖动问题。
处理内存抖动问题的相关工具

那么怎么判断是否发生内存抖动呢?从代码角度比较难以看出存在问题的代码。为了解决这个问题AS为我们提供了Memory Monitor工具通过这个工具可以很直观得看出内存抖动的现象。比如下图中在红色框图中短时间内存在着大量起伏不定的内存曲线,这表明这个时间段发生了内存抖动,需要结合代码来查看到底是那部分代码导致了这个问题。

上面的方法只是从直观的角度来判断是否发生了内存抖动,但是具体是哪里发生了内存抖动还需要其他工具进行介入收集数据。我们可以通过Allocation Tracker来完成任务,如果短时间内,同一个栈中不断进出的相同对象。这很显然发生了内存抖动,需要引起足够的重视。

内存泄露:


内存泄漏表示的是不再用到的对象原本需要被回收的但是由于被错误引用(还有对象依旧持有这个对象)而无法被正常回收。举个常见的例子通常来说,View会保持Activity的引用,Activity同时还和其他内部对象也有可能保持引用关系。当屏幕发生旋转的时候Activity就很容易发生泄漏,这样的话里面的view也会发生泄漏。这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。显然,这会使得每级Generation的内存区域可用空间逐渐变小,GC就会更容易被触发,从而引起性能问题。要解决内存泄露的问题需要我们对代码十分熟悉,当然现在有很多第三方库可以帮我们检测内存泄露的问题,比如LeakCanary。

避免内存泄露的措施:
  1. 避免使用异步回调:
    异步回调被执行的时间是不确定的,有可能在回调的时候所在的Activity已经被销毁了,这不仅很容易会引起crash还很容易发生内存泄露。比如下面的例子:

  2. 避免使用static对象:
    因为static的生命周期过长,使用不当很可能导致内存泄露。

  3. 避免把View添加到没有清除机制的容器里
    假如把view添加到WeakHashMap,如果没有执行清除操作,很可能会导致泄漏。

  4. Activity的泄漏
    通常来说,Activity的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广:

  • 匿名内部类/非静态内部类和Handler使用不当导致内存泄露

我们先来看下一个非静态内部类的一个例子:

 public class MainActivity extends AppCompatActivity {

private static TestNoneStaticClass mTestClass = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mTestClass == null){
mTestClass = new TestNoneStaticClass();
}
//...
}

class TestNoneStaticClass {
//...
}
}

上面这个看出问题的所在了吗?
首先我们知道 mTestClass 是一个静态变量它的生命周期是整个应用周期,但是它是一个非静态内部类,所以会持有外部类的引用,换句话说MainActivity被一个生命周期为整个应用周期的对象所持有,所以它退出后就不能被GC回收。从而导致了内存泄露。

接下来我们再来看下Handler导致的内存泄露的例子:

Handler 的不正确使用造成的内存泄漏问题是比较常见的,我们知道当Android应用程序启动时,framework会为该应用程序的主线程创建一个Looper对象。这个Looper对象包含一个简单的消息队列Message Queue,它能够循环的处理队列中的消息。这些消息都会被添加到消息队列中并被逐个处理。
这个主线程的Looper对象会伴随该应用程序的整个生命周期。当我们向这个Looper发送一个消息后所有发送到消息队列的消息Message都会拥有一个对Handler的引用,当Looper来处理消息时,会回调Handler#handleMessage(Message)方法来处理消息。那么这又有什么问题呢?
问题有两点:

  1. 我们有可能会将Handler声明为非静态的内部类,这样的话它就持有外部Activity的引用,如果使用这种Handler向Looper发送消息后如果在Activity退出后仍然没有被处理,那么Message将会保留在Looper内,由于上面所说Message持有Handler的引用,Handler由于是非静态内部类所以也会持有Activity的引用,那么这样就导致了Activity退出后,内存不能被回收,也就是内存泄露了。
    下面就是一个错误引用导致的问题:

    public class SampleActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    // ...
    }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
    @Override
    public void run() { /* ... */ }

    }, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
    }
    }
  2. 还有个问题就是如果Handler当中还需要Activity的引用呢?
    为了解决这个问题我们最常用的就是使用静态内部类+弱引用来断开与外部Activity的引用关系,代码如下:

然后,当主线程里,实例化一个Handler对象后,它就会自动与主线程Looper的消息队列关联起来。所有发送到消息队列的消息Message都会拥有一个对Handler的引用,所以当Looper来处理消息时,会据此回调[Handler#handleMessage(Message)]方法来处理消息。
另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。

由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

举个例子:

在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:

public class SampleActivity extends Activity {

/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {

private final WeakReference<SampleActivity> mActivity;

public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}

private final MyHandler mHandler = new MyHandler(this);

/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() {

}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}

除了上面的例子外在Aactivity退出之前,需要注意执行remove Handler消息队列中的Message与Runnable对象从而达到彻底的退出。

  • Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。
    内部类引起的泄漏不仅仅会发生在Activity上,其他任何内部类出现的地方,都需要特别留意。我们可以考虑尽量使用static类型的内部类,同时使用WeakReference的机制来避免因为互相引用而出现的泄露。
  • 考虑使用Application Context而不是Activity Context
    在开发的时候除了必须使用Activity Context的情况,尽量使用Application Context而不是Activity Context,由于Application Context的生命周期较长所以使用它可以有效避免Activity 被持有而导致内存泄露的可能。
  • 对于临时Bitmap对象要及时回收
    临时创建的某个相对比较大的 bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap
  • 在退出Activity的时候不要忘记注销监听器
    在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。并且要注意添加和注销必须对应起来。比如在onCreate上监听的,需要在onDestroy中注销监听。
  • 缓存对象没有及时清理导致的泄露问题
    为了提高对象的复用性我们会将某些对象放置到缓存容器而不是直接销毁,这样可以在下次使用的时候直接使用。但是这有可能导致一个问题就是如果存储在缓存里面的对象没有及时从容器中清除,并且缓存容器中存储的对象持有View或者Activity的引用极有可能导致Activity发生泄漏。
  • Cursor对象没有及时关闭导致的泄漏
    在程序中有的时候会发现我们查询完数据库后,返回结果的Cursor使用结束后没有及时关闭的情况,这时候就会导致Cursor的泄漏。这个是比较初级的泄漏,但也是最常见的泄漏。

内存溢出

在Android系统中每个app所能够使用的堆大小是有限的,不同RAM大小的设备堆大小是不同的,一旦我们的app在超过了这个限制的情况下继续分配内存的话会引起OOM.如果你想要查询当前设备的堆大小显示可以调用getMemoryClass()来查询。它会返回一个用于表示当前应用堆大小限制的数据。如果你确定你的应用需要耗费较大的堆空间,可以通过在manifest的application标签下添加largeHeap=true的属性。但是这样做有个不好的地方是,它会使得每次GC的运行时间更长,在任务切换时,系统的性能会变得大打折扣,所以在遇到内存溢出的时候不应该只借助申请大堆栈的方式来解决,而应该从根本上节省内存的消耗。
因此防止内存溢出的问题也可以转换为如何减少内存的消耗。

  • 使用更加轻量的数据结构
    利用Android 系统中专门优化过的容器类,例如SparseArray, SparseBooleanArray, 与 LongSparseArray。 通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。
  • 避免在Android里面使用Enum
    Enums的内存消耗通常是static constants的2倍。应该尽量避免在Android上使用enums。
  • 适当的代码抽象,但是不要过分的抽象。
  • 避免使用依赖注入框架:
    使用依赖注入框架能够简化我们的代码,但是那些框架会通过扫描你的代码执行许多初始化的操作,这会导致你的代码需要大量的RAM来mapping代码,而且mapped pages会长时间的被保留在RAM中,从而增加了内存消耗。
  • 谨慎使用第三方库:
    不要为了少量的功能而导入整library。如果没有一个合适的库满足你的需求的情况下, 我们应该考虑自己去实现或者对一个相近库进行移植,而不是直接导入一整个库方案。
  • 使用ProGuard来剔除不需要的代码
    Android为我们提供了Proguard的工具来帮助应用程序对代码进行瘦身,优化,混淆的处理。它能够通过移除不需要的代码,重命名类,域与方法等方对代码进行压缩,优化与混淆。使用ProGuard可以使得你的代码更加紧凑,这样能够使用更少mapped代码所需要的RAM。使用Proguard只需要在build.gradle文件中配置minifEnable为true即可。

    但是我们有的时候并不需要所有的类都混淆而Proguard还不够智能到判断哪些类,哪些方法是不能够被混淆的,所以,我们需要手动的把这些需要保留的类名与方法名添加到Proguard的配置文件中,如下图所示:
  • 删除无效资源:
    上面介绍的是如何去除无效的代码,接下来要介绍的将是如何自动删除无效的资源,对于那些没有被引用到的资源,会在编译阶段被排除在APK安装包之外,要实现这个目的,只需要在build.gradle文件中配置shrinkResource为true即可,如下图所示:

    和混淆一样有时候有特殊的需求,这时候我们可以通过tools:keep或者tools:discard标签来实现对特定资源的保留与废弃,如下图所示:

    Gradle目前无法对values,drawable等根据运行时来决定使用的资源进行优化,对于这些资源,需要我们自己来确保资源不会有冗余。
  • 序列化角度节省内存资源:
    序列化一般发生在网络之间,不同进程间以及不同类之间的数据传递。通常的做法是通过实现Serializable接口,但是这种传统的做法会消耗更多的内存。

但是我们如果使用GSON库来处理这个序列化的问题,不仅仅执行速度更快,内存的使用效率也更高。

除了使用GSon来进行序列化这个措施来减小内存损耗外,可以通过优化数据呈现的顺序以及结构来优化内存的损耗。通常来说,一般的数据序列化的过程如下图所示:

上面的过程,存在两个不足,第一个是重复的属性名称:

另外一个是GZIP没有办法对上面的数据进行更加有效的压缩,假如相似数据间隔了32k的数据量,这样GZIP就无法进行更加有效的压缩:

但是我们稍微改变下数据的记录方式,就可以得到占用空间更小的数据,如下图所示:


通过上述优化,有了如下的优化:
(1) 减少了重复的属性名:
(2) 使得GZIP的压缩效率更高:

  • 使用zipalign对最终的Apk进行对其操作
    在完成编码阶段并编译出Apk后等待释放的版本之前需要使用zipalign对Apk进行资源对其操作来节省RAM空间。
  • 图像资源占用空间的优化
    Android 的Heap空间中的图片被收回之后,这块区域并不会和其他已经回收过的区域做重新排序合并处理,那么当一个更大的图片需要放到heap之前,很可能找不到那么大的连续空闲区域,那么就会触发GC,使得heap腾出一块足以放下这张图片的空闲区域,如果无法腾出,就会发生OOM。因此在使用图片资源的时候要十分注意节省图片资源对内存空间的损耗。

节省图片资源对内存空间的占用有如下几种途径:
(1) 从图片资源本身角度可以通过减小图片尺寸,降低图片的分辨率清晰度等
在设计给到资源图片的时候,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用一张更小的图片。尽量使用更小的图片不仅仅可以减少内存的使用,还可以避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图的时候就会因为内存不足而发生InflationException。
除了在尺寸上优化图片资源外,还可以考虑选择不同的图片格式,目前主流的图片格式有PNG,JPEG,WEBP三种,三种主流格式在占用空间与图片质量之间的对比如下所示:

对于JPEG与WEBP格式的图片,不同的清晰度对占用空间的大小也会产生很大的影响,适当的减少JPG Quality,可以大大的缩小图片占用的空间大小。
另外,我们需要为不同的使用场景提供当前场景下最合适的图片大小,例如针对全屏显示的情况我们会需要一张清晰度比较高的图片,而如果只是显示为缩略 图的形式,就只需要服务器提供一个相对清晰度低很多的图片即可。服务器应该支持到为不同的使用场景分别准备多套清晰度不一样的图片,以便在对应的场景下能够获取到最适合自己的图片。
(2) 合理分配图片资源
我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100 x 100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200x200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。
(3) 同一张图片还可以通过选用不同的解码方式和格式来减少对空间的占用
在Android开发过程中最常用的图片格式有png,jpeg,webp等,在这些图片被设置到UI界面之前都需要解码过程,在进行解码的时候使用不同的解码方式对内存的损耗是有极大差别的,因此在图像质量能够满足要求的情况下,尽量选用对内存要求较小的解码方式,但是要注意的是不同的解码格式,清晰度也会存在较大差别,因此需要在二者之间做出权衡。

在Android里面可以通过下面的代码来设置解码率:

在Android开发过程中我们会发现大多数的图片资源都是PNG格式的,这很大程度上是由于PNG相比于JPEG格式能够提供更加清晰无损的图片效果,但是正如上面提到的图片对内存的消耗以及图片的清晰度是一个对立面,在选用资源的过程中需要在二者之间做一个权衡。对于对于那些使用JPEG就可以达到视觉效果的,可以考虑采用JPEG。除了上述的解决方案外,还可以考虑使用Webp格式,它是由Google推出的一种既保留png格式的优点,又能够减少图片大小的一种新型图片格式。
(4) 除了上述两种还可以通过特殊处理来达到这个目的。
比如在缩放图片的时候Android提供了现成的bitmap缩放的API createScaledBitmap(),使用这个方法返回的是一张经过缩放的图片。createScaledBitmap方法能够快速的得到一张经过缩放的图片,可是这个方法能够执行的前提是原图片需要事先加载到内存中,如果原图片过大,很可能导致OOM。

为了避免这个问题的发生我们可以考虑使用inSampleSize这个属性:
inSampleSize能够等比的缩放显示图片,同时还避免了需要先把原图加载进内存的缺点。我们会使用类似像下面一样的方法来缩放bitmap:


另外,我们还可以使用inScaled,inDensity,inTargetDensity的属性来对解码图片做处理,源码如下图所示:

如果只要获取图片的大小尺寸数据可以使用inJustDecodeBounds属性,使用这个属性去尝试解码图片,可以事先获取到图片的大小而不至于占用什么内存。如下图所示:

  • Bitmap对象的复用
    利用inBitmap的高级特性提高Android系统在Bitmap分配与释放执行效率上的提升。使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的bitmap会尝试去使用之前那张bitmap在 heap中所占据的pixel data内存区域,而不是向内存重新申请一块区域来存放bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数 量的内存大小。

    但是使用inBitmap时有几个限制条件:
    (1) 在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为 100-100才能够被重用。从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。
    (2) 新申请的bitmap与旧的bitmap必须有相同的解码格式。

将Bitmap重用和对象池技术结合起来可以创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。如下图所示:

在Android系统中唯一完整释放内存的方法是释放那些你可能持有的对象的引用,当这个对象没有被任何其他对象引用的时候,它才能被GC回收。但是如果系统在其他某个地方重用某个对象的话,就会导致它不能被完全回收。

  • 使用对象池来减少需要创建的对象。
    我们在开发过程中时常会遇到短时间内需要大量对象的情况,比如一个多用户的Apk,可能在某个时间点会有多个用户注册,这就有可能需要在短时间内创建大量对象,但是短时间创建大量对象会导致内存紧张,GC过程十分耗时,还有可能照成内存抖动问题。为了避免这个问题最常用的解决方式就是使用对象池。

    在使用对象池的时候当使用某个对象的时候先去对象池查询是否存在,如果不存在则创建一个对象然后加入对象池,但是我们也可以在程序刚启动的时候就事先为对象池填充一些即将要使用到的数据,这样可以在需要使用到这些对象的时候提供更快的首次加载速度,这种行为就叫做预分配。使用对象池也有不好的一面,程序员需要手动管理这些对象的分配与释放,这个极有可能造成内存泄露。为了确保所有的对象能够正确被释放,我们需要保证加入对象池的对象和其他外部对象没有互相引用的关系。

  • 复用系统自带的资源
    Android系统本身内置了很多的资源,例如字符串/颜色/图片/动画/样式以及简单布局等等,这些资源都可以在应用程序中直接引用。这样做不仅 仅可以减少应用程序的自身负重,减小APK的大小,另外还可以一定程度上减少内存的开销,复用性更好。
    注意在ListView/GridView等出现大量重复子组件的视图里面对ConvertView的复用

  • Try catch某些大内存分配的操作
    在某些情况下,我们需要事先评估那些可能发生OOM的代码,对于这些可能发生OOM的代码,加入catch机制,可以考虑在catch里面尝试一次降级的内存分配操作。例如decode bitmap的时候,catch到OOM,可以尝试把采样比例再增加一倍之后,再次尝试decode。
    留意单例对象中不合理的持有
    由于单例对象的生命周期和应用的生命周期相一致所以要特别注意单例中所持有的对象,如果持有的对象不合理极有可能照成内存的泄露

一般我们会在Application 类中定义一个getContext方法,这样在任何需要Application Context的时候就可以通过getContext来获得

context = getApplicationContext();

public static Context getContext(){
return context;
}
public class SingletonClass {
private static SingletonClass instance;
private Context context;
private SingletonClass() {
this.context = MyApplication.getContext();
}
public static SingletonClass getInstance() {
if (instance == null) {
instance = new SingletonClass();
}
return instance;
}
}

但是有时候我们并不需要继承Application这样我们就可以通过下面的方式,直接通过getApplicationContext方法来获取。

public class SingletonClass {
private static SingletonClass instance;
private Context context;
private SingletonClass(Context context) {
this.context = context.getApplicationContext();
}
public static SingletonClass getInstance(Context context) {
if (instance == null) {
instance = new SingletonClass(context);
}
return instance;
}
}

优化布局层次,减少内存消耗
越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的时候考虑使用自定义View来达到目的。

  • 在使用服务时候需要注意的问题:

如果你的应用需要使用到后台服务,要记住系统会倾向保留后台服务而一直保留服务所在的进程,并且系统没有办法把服务所占用的RAM空间让出来给其他部件,所以导致了你当前进程的运行代价很高。因此在使用服务的时候要注意两点:除非它被触发并执行一个任务,否则尽量保证其他时候service都处于停止状态;尽可能使用IntentService它会在处理完交代给它的intent任务之后尽快结束自己,不要在服务不需要的时候还让服务驻留在后台,这样极有可能照成资源浪费。
其中最主要的就是如何正确得启动和停止服务。

普通的Started Service,需要通过stopSelf()来停止Service

另外一种Bound Service,会在其他组件都unBind之后自动关闭自己

内存不足时的系统通知方式

和其他系统一样Android系统也是一个多任务系统,用户可以在不同的App之间快速切换,在Android系统中为了保证后台应用能够迅速切换到前台,在应用切换到后台的时候会将其缓存到后台占用一定的内存,这样做的好处就是将当前应用从后台移动到前台的时候不用重新创建,只要从缓存中直接获取并恢复。但是在系统资源不足的时候系统会尝试从LRU 缓存清除部分进程:
当系统开始清除LRU缓存中的进程时,尽管它首先按照LRU的顺序来操作,但是它同样会考虑进程的内存使用量。因此消耗越少的进程则越容易被留下来。

  • 通过onTrimMemory来控制释放内存的时间点:
    我们知道Android系统会在内存空间不足的情况下按照一定的规则杀死某些进程,但是Android系统也不是蛮讲理的不会在进程毫无防备的情况下杀死进程,还是会给进程一定的警告的,进程在收到这些警告的时候就应该采取一些必要的措施,比如尽可能得释放不需要的资源来腾出空间来缓解内存不足的情况。

onTrimMemory这个方法会在系统认为进程内存最佳释放的时间点的时候被回调,它接收一个内存级别的参数用于表示,当前处于那个内存阶段,我们可以通过传入的这个数值决定释放哪些资源onTrimMemory()的回调可以发生在Application,Activity,Fragment,Service,Content Provider。

  • TRIM_MEMORY_UI_HIDDEN:
    当用户打开了另外一个程序,我们的程序界面已经不可见的时候,我们应当将所有和界面相关的资源进行释放。重写Activity的onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发说明用户离开了程序,此时就可以进行资源释放操作了。
    当程序正在前台运行的时候可能会受到如下的内存等级参数:
  • TRIM_MEMORY_RUNNING_MODERATE:尽管目前设备处于低内存状态,系统已经开始触发杀死LRU 缓存中的进程的机制,但是你的app尚且处在安全阶段,尚未被列为可杀死的进程。
  • TRIM_MEMORY_RUNNING_LOW:目前设备正处于低内存状态,虽然你的app没有被列到系统杀死进程的列表中,但是我们在这个阶段应该开始释放我们不用的资源为提升系统资源做贡献。
  • TRIM_MEMORY_RUNNING_CRITICAL:目前设备内存已经十分紧缺,并且系统已经把缓存中的大部分进程都已经杀死,这时候我们的应用就要重视起来了,应该立刻释放所有非必须的资源,如果再不清除的话等到系统清除完所有LRU缓存中的进程后就会开始杀死哪些之前被认为不应该被杀死的进程,直到轮到杀死自己。

如果我们的进程处于LRU 缓存状态则会收到如下的的回调:

  • TRIM_MEMORY_BACKGROUND: 系统正运行于低内存状态并且你的进程正处于LRU缓存名单中最不容易杀掉的位置。尽管你的app进程并不是处于被杀掉的高危险状态,系统可能已经开始杀掉LRU缓存中的其他进程了。你应该释放那些容易恢复的资源,以便于你的进程可以保留下来,这样当用户回退到你的app的时候才能够迅速恢复。

  • TRIM_MEMORY_MODERATE: 系统正运行于低内存状态并且你的进程已经已经接近LRU名单的中部位置。如果系统开始变得更加内存紧张,你的进程是有可能被杀死的。

  • TRIM_MEMORY_COMPLETE: 系统正运行与低内存的状态并且你的进程正处于LRU名单中最容易被杀掉的位置。你应该释放任何不影响你的app恢复状态的资源。

Contents
  1. 1. Generational Heap Memory模型:
  2. 2. Android 开发过程中的常见内存异常:
    1. 2.1. 内存抖动:
      1. 2.1.1. 发生内存抖动常见的原因:
      2. 2.1.2. 处理内存抖动问题的相关工具
    2. 2.2. 内存泄露:
    3. 2.3. 避免内存泄露的措施:
  3. 3. 内存溢出
  4. 4. 内存不足时的系统通知方式