【奥门威尼斯网址】Android 内存管理

版权声明:

本账号发布文章均来自公众号,承香墨影,版权归承香墨影所有。

未经允许,不得转载。

Android系统是基于Linux
2.6内核开发的开源操作系统,而linux系统的内存管理有其独特的动态存储管理机制。
不过Android系统对Linux的内存管理机制进行了优化,Linux系统会在进程活动停止后就结束该进程,而Android把这些进程都保留在内存中,直到系统需要更多内存为止。这些保留在内存中的进程通常情况下不会影响整体系统的运行速度,并且当用户再次激活这些进程时,提升了进程的启动速度。

Android内存管理

Android是一个基于Linux实现的操作系统。但对于Linux内核来说,Android也仅仅只是一个运行在内核之上的应用程序,与其他运行在内核之上的应用程序没有任何区别。所以Android需要一套机制管理运行在Linux进程中的APK应用程序。Android内存管理包含两部分,一部分是Framework对内存的管理,一部分是Linux内核对内存管理,这两部分共同决定应用程序的生命周期。本文主要阐述Android内存管理机制的实现原理,以及在应用开发中需要注意的一些事项,最后本文总结了如何实现杀不死进程的一种方法。

1. Android中的内存

在 Android 系统中,当运行的 App 被移动到后台的之后,Android
为了保证下次启动的速度,会将它移入 Cached 的状态,这个时候实际上,该 App
的进程依然存在,但是对应的组件是否存在,就不一定了。而既然该 App
的进程还存活着,下次启动的速度就会很快,这就是常说的热启动。

Android内存管理包含两部分,一部分是Framework对内存的管理一部分是Linux内核对内存管理,这两部分共同决定应用程序的生命周期。

APP默认分配内存大小

在Android里,程序内存被分为2部分:

  • native
  • dalvik

android程序内存(native+dalvik)一般限制在16M,或者24M,36M

gDvm.heapSizeStart = 2 * 1024 * 1024;   // heap初始化大小为2M
gDvm.heapSizeMax = 16 * 1024 * 1024;    // 最大的heap为16M

Linux 进程回收


在Android中,大部分应用程序都运行在一个独立的Linux进程中,每个进程都有独立的内存空间。随着各种应用程序启动,系统内存不断下降,为了保证新应用能够运行,Android需要一套机制杀死暂时闲置的进程。

Android
Framework并不能直接回收内存,其管理进程的服务(ActivityManagerService,以下简称AmS)也同应用程序一样运行在Java虚拟机环境里。Java虚拟机都运行在各自独立的内存空间,所以ActivityManagerService没有办法感知应用程序是否OOM。

Android系统中还运行了一个OOM进程。该进程启动时首先会在Linux内核中把自己注册为一个OOM
Killer。AmS需要把每一个应用程序的oom_adj值告知OOM
Killer,这个值的范围在-16到15之间,值越低,说明越重要,这个值类似于Linux中的nice值,只在标准的Linux中,有其自己的OOM
Killer。Android中的OOM Killer进程仅仅适用于Android应用程序。

当内核的内存管理模块检测到系统内存不足时就会通知OOM Killer,然后OOM
Killer根据AmS所告知的优先级强制退出优先级低的应用程序。

1.1 Android中的垃圾回收机制

Android
平台最吸引开发者的一个特性:有垃圾回收机制,无需手动管理内存,Android
系统会自动跟踪所有的对象,并释放那些不再被使用的对象

  • Young Generation 新生代

    1. 大多数新建对象都位于Eden(伊甸园)区
    • 当Eden区被对象填满时,就会执行Minor
      GC(轻量GC)。并把所有存活下来的对象转移到其中一个survivor区
    • Survivor Space:S0、S1有两个,存放每次垃圾回收后存活的对象
    • Minor GC
      同样会检查survivor区中存活下来的对象,并把他们转移到另一个survivor区,这样在一段时间内,总会有一个空的survivor区
  • Old Generation 老生代

    1. 存放长期存活的对象和经过多次Minor GC后依然存活下来的对象
    • 满了进行Major GC(较重GC)
  • Permanent Generation 永久代

  • 存放方法区,方法区中有,要加载的类信息、静态变量、final类型的常量、属性和方法信息

但是这些被退出到后台的 App
,也并不是完全安全不会被清理掉的,他们可能只是没有持有任何的组件,并且是不占用
CPU
资源的,但是它依然会占据内存空间。而当系统认为内存不足的时候,就会按照优先级清理掉一些优先级不那么高的进程,来回收一些内存空间,供新启动的程序使用,这就是
LowMemoryKiller 的策略。

在Android中,大部分应用程序都运行在一个独立的Linux进程中,每个进程都有独立的内存空间。随着各种应用程序启动,系统内存不断下降,为了保证新应用能够运行,Android需要一套机制杀死暂时闲置的进程。

dalvik的堆栈内存

Heap(堆) Stack(栈)
dalvik作用 dalvik大内存数据区 dalvik内存指令区
数据存储 对象实例数据 方法内部变量(复杂类型) 动态属性 基本数据类型

方法内部变量(简单数据变量) 静态属性 类方法 对象地址 常量

  • 堆是一个运行时数据区,类的对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分
    配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

  • 栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本
    类型的变量(,int, short, long, byte, float, double, boolean,
    char)和对象句柄。

应用程序在内存中的状态


Android官方声称,Activity退出后,其所在进程并不会被立即杀死,从而在下次启动Activity时,能够提高启动速度。这些Activity只有在内存紧张时才会被系统杀死。所以对于应用程序来说,关闭并不意味着释放内存。

Activity在内存中的状态
系统只有一个Activity处于与用户交互的状态,对于非交互状态的Activity,AmS会在内部暂时缓存起来而不是立即杀死,但如果后台Activity数目超过一定阈值,AmS则会强制杀死一些优先级低的Activity。以下是Activity在内存或者说在AmS中的状态:

  • AmS会记录最近启动的20个Activity,如果超过20则舍弃最早记录的Activity。
  • AmS会将所有正在运行的Activity保存在一个列表中,对于使用back返回的Activity则从列表中清除。
  • AmS使用Lru算法保存所有最近使用过的Activity。
  • AmS使用一个列表(mStoppingActivities)保存需要停止的Activity,这种情况
    发生在启动一个Activity时,AmS遵循先启动后停止的策略,将需要停止的Activity保存在此列表中,等AmS闲置下来后再停止Activity。
  • AmS使用一个列表保存处于finish状态(onDestory())的Activity,当一个Activity处于finish状态时(onDestory()执行后)不会被立即杀死,而是保存到该列表中直到超过系统设定的警戒线才会回收该列表中的Activity。

应用进程在内存中的状态
每个应用程序都对应着一个ActivityThread类,该类初始化后就进入Looper.loop()函数中无限循环。

Looper.prepareMainLooper();
...
ActivityThread thread = new ActivityThread();
thread.attach(false);
...
Looper.loop();

以后则依靠消息机制运行,既当有消息时处理消息,没有消息则应用进程进入sleep状态。loop()方法内部代码如下所示:

public static final loop() {
  Looper me = myLooper();
  MessageQueue queue = me.mQueue;
  while(true){
    Message msg = queue.next();// might block
    ...
  }
}

在Linux内核调度中,如果一个线程的状态为sleep,则除了占用调度本身的时间,不会占用CPU时间片。

有三种情况会唤醒应用线程,一种是定时器中断(比如我们设置的闹钟,在程序中可以设置定时任务),第二种是用户按键消息,第三种是Binder消息(Binder用于进程间通信,其在应用程序中会自动创建一个线程,Binder在接收到消息后会想UI主线程发送一个消息从而使queue.next()继续执行)这就是所谓的消息驱动模式。

所以设计良好的应用程序当处于后台时不会占用任何CPU时间,更不会拖慢系统运行速度。其所占用的仅仅是内存,即使释放所占用的内存也不会提高系统运行速度。当然这里说的是设计良好的应用程序,目前国内很多应用在处于后台状态时依然会偷偷干很多事情,这无疑就拖慢了系统运行速度。

1.2 垃圾回收

  • 内存占用过多,需要为新对象分配空间
  • 不同的虚拟机发生GC时采用的策略不同,可能会暂停当前程序的执行

那么,为了让我们的 App
在后台尽可能活的久一点,无非就是将内存降低,从而使得优先级提高,而实现不被系统回收的功能(这是一个常规的优化方案,而非保活方案)。那么,如果我们能明确当前
App
处于什么状态,就能在此时机,释放一些不需要持有的内存资源,来达到我们的目的。

Android
Framework并不能直接回收内存,其管理进程的服务(ActivityManagerService,以下简称AmS)也同应用程序一样运行在Java虚拟机环境里。Java虚拟机都运行在各自独立的内存空间,所以ActivityManagerService没有办法感知应用程序是否OOM。

Android的GC如何回收内存

Android的一个应用程序的内存泄露对别的应用程序影响不大。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都
会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。
Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视
为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会
引起系统重启)。

Android系统在运行多个进程时,如果系统资源不足,会强制结束一些进程。优先选择哪个进程来结束是有优先级的。

进程的优先级:

  • 1.Foreground process 前台进程
    用户可见,可以操作. 按钮可以点击(获取焦点) onResume方法被调用了.

  • 2.Visible process 可见进程 可视进程
    onpause方法被调用 但是activity任然界面可见.

  • 3.Service process 服务进程.
    系统会延长维护服务进程存活的周期,直到前台进程和可见进程内存不够用

  • 4.Background process 后台进程
    开启了activity后 最小化了.

  • 5.Empty process 空进程
    没有任何组件运行的进程 开启了activity之后,关闭了

Android 内存回收


Activity所占内存在一般情况下不会被回收,只有在系统内存不够用时才会回收,并且回收会遵循一定规则。大致可以概括为前台Activity最后回收,其次是包含前台的Service或者Provider,再其次是后台Activity,最后是空进程。

内存释放的三个地方

  • 第一个是在ActivityManagerService中运行,即Android所声称的当系统内存低时,优先释放没有任何Activity的进程,然后释放非前台Activity对应的进程。
  • 第二个是在OOM
    Killer中,此时AmS只要告诉OOM各个应用的优先级,然后OOM就会调用Linux内部的进程管理方法杀死优先级较低的进程。
  • 第三个是在应用进程本身之中,当AmS认为目标进程需要被杀死时,首先会通知目标进程进程内存释放。这包括调用目标进程的scheduleLowMemory()方法和processInBackground()方法。

关闭Activity的三种情况

  • 第一种,从调用startActivity()开始,一般情况下,当前都有正在运行的Activity,所以需要先暂停当前的Activity,而暂停完毕后,AmS会收到一个Binder消息,并开始从completePaused()处执行。在该函数中,由于上一个Activity并没有finishing,仅仅是stop,所以这里会把上一个Activity添加到mStoppingActivity列表中。当目标Activity启动后,会向Ams发送一个请求进行内存回收的消息,这会导致AmS在内部调用activityIdleInternal()方法,该方法中首先会处理mStoppingActivities列表中的Activity,这就会调用stopActivityLocked()方法。这又会通过IPC调用,通知应用进程stop指定的Activity,当stop完毕后,再报告给AmS,于是AmS再从activityStopped()出开始执行,而这会调用trimApplication()方法,该方法会执行内存相关的操作。
  • 第二种,当按Back键后,会调用finishActivityLocked(),然后把该Activity的finishing标识设为true,然后再调用startPausingLocked(),当目标Activity完成暂停后,就会报告AmS,此时AmS又会从completePaused()处开始执行。与第一种情况不同,由于此时暂停的Activity的finishing状态已经设置为true,所以会执行finishingActivityLocked(),而不是像第一种情况中仅仅把该Activity添加到mStoppingActivities列表。
  • 第三种,当Activity启动后,会向AmS发送一个Idle消息,这会导致AmS开始执行activityIdleInternal()方法。该方法会首先处理mStoppingActivities列表中的对象,接着处理mFinishingActivities列表,最后再调用trimApplication()方法。

以上就是关闭Activity的三种情况,包括stop和destory,客户进程中与之对应的就是onStop()和onDestory()的调用。

如果使用OOM还有AmS机制杀死后台进程后,此时运行的Activity数量依然超过MAX_ACTIVITIES(20),则需要继续销毁满足以下三个条件的Activity:

  1. Activity必须已经stop,但却没有finishing
  2. 必须是不可见的,既该Activity窗口上面有其他全屏的窗口,如果不是全屏,则后面的Activity是可见的。
  3. 不能是persistent类型,既常驻进程不能被杀死。

1.3 垃圾回收机制&FPS

  • Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,那么整个过程如果保证在16ms以内就能达到一个流畅的画面。60FPS
  • 如果某一帧的操作超过了16ms就会让用户感觉到卡顿
  • UI渲染过程发生GC,导致某一帧绘制时间超过16ms

这个时候,就需要用到 onTrimMemory() 这个回调方法了。

Android系统中还运行了一个OOM进程。该进程启动时首先会在Linux内核中把自己注册为一个OOM
Killer。AmS需要把每一个应用程序的oom_adj值告知OOM
Killer,这个值的范围在-16到15之间,值越低,说明越重要,这个值类似于Linux中的nice值,只在标准的Linux中,有其自己的OOM
Killer。Android中的OOM Killer进程仅仅适用于Android应用程序。

进程优先级


Android系统试图尽可能长时间地保持应用程序进程,但为了新建或者运行更加重要的进程,总是需要清除过时进程来回收内存。为了决定保留或终止哪个进程,根据进程内运行的组件及这些组件的状态,系统把每个进程都划入一个“重要性层次结构”中。重要性最低的进程首先会被清除,然后是下一个最低的,依此类推。

重要性层次结构共有5级,以下列表按照重要程度列出了各类进程(第一类进程是最重要的,将最后一个被终止):

1)前台进程

用户当前操作所必须的进程。满足以下任一条件时,进程被视作处于前台:
其中运行着正与用户交互的Activity(Activity对象的onResume()方法已被调用)。
其中运行着与用户交互的activity绑定的Service。
其中运行着前台Service,既该Service以startForeground()方式被调用。
其中运行着正在执行生命周期回调方法(onCreate()、onStart()或onDestory())的Service。
其中运行着正在执行onReceive()方法的BroadcastReceiver。

一般而言,任何时刻前台进程的数量都为数不多,只有当内存不足以维持它们同时运行时才会被终止。通常,设备这时候已经到了使用虚拟内存的地步,终止一些前台进程是为了保证用户界面的及时响应。

2) 可见进程

没有前台组件、但仍会影响用户在屏幕上所见内容的进程。满足以下任一条件时,进程被认为是可见的:
其中运行着非前台Activity,但用户仍然可见到此activity(onPause()方法被调用)。例如,打开了一个对话框,而activity还允许显示在对话框后面,对用户依然可见。
其中运行着被可见(或前台)activity绑定的Service。

可见进程被认为是非常重要的进程,除非无法维持所有前台进程同时运行了,否则它们是不会被终止的。

3) 服务进程

此进程运行着由startService()方法启动的服务,它不会升级为前台进程或可见进程。尽管服务进程不直接和用户所见内容关联,但他们通常在执行一些用户关心的操作(比如在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台、可见进程同时运行,系统会保持服务进程的运行。

4) 后台进程

包含用户不可见activity(Activity对象的onStop()方法已被调用)的进程。这些进程对用户体验没有直接的影响,系统可能在任意时间终止它们,以回收内存供前台进程、可见进程及服务进程使用。

通常系统会有很多后台进程在运行,所以它们被保存在一个LRU(最近最少使用)列表中,以确保最近被用户使用的activity最后一个被终止。如果一个activity正确实现了生命周期方法,并保存了当前的状态,则终止此类进程不会对用户体验产生可见的影响。因为在用户返回时,activity会恢复所有可见的状态。关于保存和恢复状态的详细信息,请参阅Activity文档。

5) 空进程

不含任何活动应用程序组件的进程。保留这种进程的唯一目的就是用作缓存,以改善下次在此进程中运行组件的启动时间。为了在进程缓存和内核缓存间平衡系统整体资源,系统经常会终止这种进程。

依据进程中目前活跃组件的重要程度,Android会给进程评估一个尽可能高的级别。例如,如果一个进程中运行着一个服务和一个用户可见的activity,则此进程会被评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会由于其它进程的依赖而被提高——为其它进程提供服务的进程级别永远不会低于使用此服务的进程。比如:如果A进程中的content
provider为进程B中的客户端提供服务,或进程A中的服务被进程B中的组件所调用,则A进程至少被视为与进程B同样重要。

因为运行服务的进程级别是高于后台activity进程的,所以,如果activity需要启动一个长时间运行的操作,则为其启动一个服务会比简单地创建一个工作线程更好些——尤其是该操作时间比activity的生存期还要长的情况下。比如,一个activity要把图片上传至Web网站,就应该创建一个服务来执行之,即使用户离开了此activity,上传还是会在后台继续运行。不论activity发生什么情况,使用服务可以保证操作至少拥有“服务进程”的优先级。同理,广播接收器broadcast
receiver也是使用服务来处理耗时任务的,而不是简单地把它放入线程中。

1.4 内存泄漏

在整个Android开发过程中,内存泄漏是导致OOM(Out Of
Memory内存溢出)的一个重要因素

  1. 应用程序分配了大量不能回收的对象
  2. 这导致可分配的内存越来越少
  3. 当新对象的创建需要的内存不够
  4. 当发现内存不够就会调用一次GC进行垃圾回收
  5. 结果:就会发生卡顿

1、onTrimMemory 的作用

前面提到,我们可以通过实现 onTrimMemory() 方法,来完成对当前 App
在内存中的优先级的简单管理。

onTrimMemory() 回调方法,是 Android Level 14(Android 4.0)
之后提供的一个
API,它主要的作用是提醒开发者,在系统内存不足的时候,应该通过释放部分不重要的内存资源,从而避免被
Android 系统服务杀掉。

可以看到,onTrimMemory() 本质上是一种告知 App
处于系统内存回收的不同阶段的时机,应该在这些时机,合理对自身持有的内存进行释放,以避免被系统直接杀掉,从而让保证下次用户启动
App 时候的速度。

onTrimMemory() 的完整方法签名如下:

public void onTrimMemory(int level) 

可以看到,它实际上,会有一个 level 参数来标记当前的 App
在内存中的级别,也就意味着 onTrimMemory() 方法,可能会被多次调用到。

当内核的内存管理模块检测到系统内存不足时就会通知OOM Killer,然后OOM
Killer根据AmS所告知的优先级强制退出优先级低的应用程序。

杀不死的Service


如何让应用在手机中存活更长时间?网上各种方法可谓是千奇百怪,有些简直异想天开。

  • 系统广播唤醒应用,比如手机开机,网络切换等
  • 接入第三方SDK唤醒应用,比如接入微信SDK会唤醒微信
  • 免杀白名单,比如360免杀白名单,MIUI系统免杀白名单
  • 全家桶,应用之间互相唤醒,比如百度系,阿里系应用
  • 两个Service互相唤醒(这个就别想了,不靠谱)
  • 使用Timer定时器(一样不靠谱)

设计良好的应用不应该在用户不使用的时候依然保持运行。一直在后台运行不光费电费流量,还是造成系统卡顿的主要原因之一(参见上文分析)。正常的做法是优化你的应用程序,减少不合理场景的情况,除一些必要服务应用外,大部分应用不需要一直在后台保存运行状态。

有正常的做法就有不正常的做法,让应用长时间停留在用户手机中无外乎就是增加所谓的活跃用户数等一些产品指标。这对于很多公司还是很有吸引力的。

如上文所说,无论应用怎么挣扎,当处于不可见进程的情况下随时都有可能被杀死。所以使用前台进程是最有效的方法。但前台进程必须有一个Notifcation显示在通知栏中,有没有办法让应用以前台进程的方式启动同时又不显示Notifcation?方法当然有,就是利用系统漏洞:

  • API<18,启动前台Service时直接传入new Notifcation();
  • API>=18,同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理

目前,QQ,微信,支付宝等知名应用都使用此方案。不过如果应用占用太多内存即使是前台进程也依然会被干掉。

这些所谓的实现进程杀不死的方案并不都是一劳永逸的方法,以牺牲用户体验为代价很有可能会激怒用户卸载你的应用,所以最好的方式还是遵循Android规范开发性能更优更合理的应用程序。

1.5 内存抖动

原因:内存抖动是因为应用程序在短时间内创建大量的对象,又被马上释放。

  1. 瞬间产生大量的对象会严重占用Young Generation的内存区域
  • 当达到阈值,剩余空间不够,就会触发GC从而导致刚产生的对象又很快被回收。
  • 即时每次分配的对象占用了很少的内存,频分GC叠加在一起会增加Heap的压力
  • 从而触发更多其他类型的GC。
  • 结果:这个操作有可能会影响到帧率,并使用户感知到性能问题

2、 onTrimMemory 回传的参数的意义

既然 onTrimMemory() 方法会传递一个 level 参数,那么就先来看看,各种
level 参数所代表的含义。

  • TRIM_MEMORY_UI_HIDDEN:App 的所有 UI 界面被隐藏,最常见的就是
    App 被 home 键或者 back 键,置换到后台了。
  • TRIM_MEMORY_RUNNING_MODERATE:表示 App
    正常运行,并且不会被杀掉,但是目前手机内存已经有点低了,系统可能会根据
    LRU List 来开始杀进程。
  • TRIM_MEMORY_RUNNING_LOW:表示
    App正常运行,并且不会被杀掉。但是目前手机内存已经非常低了。
  • TRIM_MEMORY_RUNNING_CRITICAL:表示 App
    正在正常运行,但是系统已经开始根据 LRU List
    的缓存规则杀掉了一部分缓存的进程。这个时候应该尽可能的释放掉不需要的内存资源,否者系统可能会继续杀掉其他缓存中的进程。
  • TRIM_MEMORY_BACKGROUND:表示 App 退出到后台,并且已经处于 LRU
    List 比较靠后的位置,暂时前面还有一些其他的 App
    进程,暂时不用担心被杀掉。
  • TRIM_MENORY_MODERATE:表示 App 退出到后台,并且已经处于 LRU
    List 中间的位置,如果手机内存仍然不够的话,还是有被杀掉的风险的。
  • TRIM_MEMORY_COMPLETE:表示 App 退出到后台,并且已经处于 LRU
    List
    比较考靠前的位置,并且手机内存已经极低,随时都有可能被系统杀掉。

其实从 level 值的取名来看,大致可以分为三类:

  1. UI 置于后台:TRIM_MEMORY_UI_HIDDEN
  2. App 正在前台运行时候的状态:TRIM_MEMORY_RUNNING_Xxx
  3. App 被置于后台,在 Cached 状态下的回调:TRIM_MEMORY_Xxx

这三类中,通常我们只需要关心 App 被置于 Cached
状态下的情况,因为系统是不会杀掉一个正在前台运行的 App 的(但可能会触发
OOM),但是如果该 App
有一些后台服务正在运行,这个服务也是有被杀的风险的。

而在 Cached 状态下的时候,当收到 TRIM_MEMORY_Xxx
的回调,就需要注意了,这些只是标记了当前 App 处于 LRU List
的位置,也就是说,如果回收了靠前的 App
进程之后,依然达不到内存使用的要求,可能会进一步去杀进程,也就是说,极端情况下,可能从
TRIM_MEMORY_BACKGROUND 到 TRIM_MEMORY_COMPLETE
是瞬间完成的事情,所以我们需要慎重处理它们,尽量对这三个状态都进行判断,然后做统一的回收内存资源的处理。

Android 内存管理机制

基于Linux内核OOM Killer的核心思想,Android
系统扩展出了自己的内存监控体系。因为Linux下的内存杀手需要等到系统资源”濒临绝境”的情况下才会产生效果。
而Android则实现了自己的Killer。Android
系统为此开发了一个专门的驱动,名为Low Memory
Killer(LMK)
。源码路径在内核工程的drivers/staging/android/Lowmemorykiller.c中。

它的驱动加载函数如下:

static int __init lowmem_init(void)
{
    register_shrinker(&lowmem_shrinker);
    return 0;
}

当系统的空闲页面低于一定阈值时,这个回调就会被执行。

Lowmemorykiller.c 中定义了两个数组,分别如下:

static short lowmem_adj[6] = {
    0,
    1,
    6,
   12,
 };
static int lowmem_adj_size = 4;//下面的数值以此为单位(页大小)
static int lowmem_minfree[6] = {
    3 * 512,    /* 6MB */
    2 * 1024,   /* 8MB */
    4 * 1024,   /* 16MB */
    16 * 1024,  /* 64MB */
};
  • 第一个数组lowmem_adj最多有6个元素,默认只定义了4个,它表示可用容量处于”某层级”时需要被处理的adj值;

  • 第二个数组则是对”层级”的描述。举个例子,lowmem_minfree
    的第一个元素是3*512*lowmem_adj_size=6MB,意味着当可用内存小于6MB时,Killer需要清理adj的值为0(即lowmem_adj的第一个元素)以下的那些进程。其中adj的取值范围是-17~15,数字越小表示进程级别越高,通常只有0-15被使用。

奥门威尼斯网址 1

2. 内存检测工具

3、哪些组件可以监听 onTrimMemory

既然说到了 onTrimMemory() 回掉,看样子它是和 App 相关的,所以最少在
Application 中,应该是可以对其进行重写来监听回调的。但是除了
Application,其他的一些组件中,也是可以监听它的。

这些可以监听 onTrimMemory 的组件有:

  • Application
  • Activity
  • Fragment
  • Service
  • ContentProvider

android进程优先级

android将进程的优先级分为5个层次,按照优先级由高到低排列如下:

  1. 前台进程(Foreground
    process):
    它表明用户正在与该进程进行交互操作,android系统依据下面的条件来将一个进程标记为前台进程:
  • 该进程持有一个用户正在与其交互的Activity(也就是这个activity的生命周期方法走到了onResume()方法)。
  • 该进程持有一个Service,并且这个Service与一个用户正在交互中的Activity进行绑定。
  • 该进程持有一个前台运行模式的Service(也就是这个Service调用了startForegroud()方法)。
  • 该进程持有一个正在执行生命周期方法(onCreate()、onStart()、onDestroy()等)的Service。
  • 该进程持有一个正在执行onReceive()方法的BroadcastReceiver。
    一般情况下,不会有太多的前台进程。杀死前台进程是操作系统最后无可奈何的做法。当内存严重不足的时候,前台进程一样会被杀死。
  1. 可见进程(Visible
    process):
    它表明虽然该进程没有持有任何前台组件,但是它还是能够影响到用户看得到的界面。android系统依据下面的条件将一个进程标记为可见进程:
  • 该进程持有一个非前台Activity,但这个Activity依然能被用户看到(也就是这个Activity调用了onPause()方法)。例如,当一个activity启动了一个对话框,这个activity就被对话框挡在后面。
  • 该进程持有一个与可见(或者前台)Activity绑定的Service。
  1. 服务进程(Service
    process):
    除了符合前台进程和可见进程条件的Service,其它的Service都会被归类为服务进程。

  2. 后台进程(Background
    process):
    持有不可见Activity(调用了onStop()方法)的进程即为后台进程。通常情况下都会有很多后台进程,当内存不足的时候,在所有的后台进程里面,会按照LRU(最近使用)规则,优先回收最长时间没有使用过的进程。

  3. 空进程(Empty
    process):
    不持有任何活动组件的进程。保持这种进程只有一个目的,就是为了缓存,以便下一次启动该进程中的组件时能够更快响应。当资源紧张的时候,系统会平衡进程缓存和底层的内核缓存情况进行回收。

前台>可见>服务>后台>空

如果一个进程同时满足上述5种优先级中的多个等级条件,android系统会优先选取其中最高的等级作为该进程的优先级。

2.1 Memory Monitor 内存监视器

  • 优点
    • 方便显示内存使用和GC情况
    • 快速定位卡顿是否和GC有关
    • 快速定位Crash崩溃是否和内存占用过高有关
    • 快速定位潜在的内存泄漏问题
    • 简单易用
  • 缺点
    • 不能准确定位问题

4、自定义 onTrimMemroy 监听

除了前面提到的系统默认可以监听 onTrimMemory()
的组件之外,我们还可以自定义 onTrimMemory 的监听。

自定义起来也非常的简单,只需要实现 ComponentCallbacks2 接口,然后调用
Application.registerComponentCallbacks() 方法注册即可。

奥门威尼斯网址 2

除了 registerComponentCallbacks()
方法进行注册监听之外,如果不使用了的话,还可以使用
unregisterComponentCallbacks() 进行解注。

那么这里是如何实现的呢?让我们来看看 Application 的对应源码。

奥门威尼斯网址 3

可以看到,它实际上是通过一个 mComponentCallbacks 的列表进行维护的。

而在 onTrimMemory() 的时候,又从 mComponentCallbacks 中获取到所有的
callbacks 对象,进行消息的分发。

奥门威尼斯网址 4

通过这种方式实现了对 onTrimMemory() 的自定义监听。

onTrimMemory() 方法同时被标记为
@CallSuper,也就严格要求了重写它的子类,必须调用父类中的
onTrimMemory() 方法,从而保证了消息的分发不会缺失。

内存管理机制的特点

  1. 更少的占用内存;
  2. 在合理的时候,合理的释放内存;
  3. 在系统内存紧张的情况下,能释放大部分不重要的的资源,来为Android提供可用的资源;
  4. 能够很合理的在特殊生命周期中,保存和回复还原重要数据,以至于系统能够正确的重新恢复该应用。

2.2 Allocation Tracker 分配跟踪器

  • 优点
    • 定位代码中分配对象的类型,大小,时间,线程,堆栈等信息
    • 定位内存抖动问题
    • 配合HeapViewer一起定位内存泄漏问题
  • 缺点
    • 使用复杂
  • 显示所有对象的信息(环形图)

5、onLowMemory()

onTrimMemory() 既然是 Android 4.0 才新增加的
Api,那么对于低版本的设备而言,可以监听 onLowMemory()
方法,它大概可以等同于 level 级别为 TRIM_MEMORY_COMPLETE 的回调。

当然,ComponentCallbacks2 接口继承的 ComponentCallback
接口,也是需要实现 onLowMemory() 方法的。

奥门威尼斯网址 5

一些内存优化的方法

  1. 当Service完成任务后,尽量停止它,或者用intentService代替;
  2. 在UI不可见的时候,释放掉一些只有UI使用的资源;
  3. 在系统资源内存紧张的时候,尽可能多的释放掉一些非重要资源;
  4. 避免滥用Bitmap导致的内存浪费,参见Here
  5. 使用针对内存优化过的数据容器
  6. 避免使用依赖注入的框架
  7. 使用ZIP对其APK
  8. 使用多进程

2.3 Heap Viewer 堆视图

  • 优点
    • 内存快照信息
    • 每次GC之后收集一次信息
    • 查找内存泄漏利器
  • 缺点
    • 使用复杂
  • 显示已分配的对象大小信息(包视图)

1、为什么需要 onTrimMemory()

Android
系统会在自身内存不足的情况下,清理掉一些不重要的进程来释放内存资源,以供优先级更高的进程使用。而这个顺序,主要是按照
LRU List
中的优先级来清理的,但是它也同时会考虑清理掉哪些占用内存较高的进程来让系统更快的释放跟多的内存。

所以,尽可能的让 App
在系统内,占用足够小的内存资源,就可以降低被杀的概率,从而下次启动的时候走热启动的方式,提升用户的体验。

换一个角度来说,让 App
占用较小的内存,也可以优化系统的速度,毕竟系统清理进程释放内存的过程,也是需要占用
CPU 资源的。在大环境下,也是有意义的。

所以,在 onTrimMemory() 的时机,对当前 App
的内存进行释放优化,就尤为重要了。

参考并感谢

  1. Android内存管理机制详解
  2. Android
    内存管理机制详解
  3. Android
    内存优化总结&实践

2.4 Leak Canary

https://github.com/square/leakcanary

  • 引用

      dependencies {
          debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
          releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
      }
    
  • 使用

      // 内存泄漏检测
      private RefWatcher refWatcher;
      public static RefWatcher getRefWatcher(Context context) {
          TTApplication application = (TTApplication) context.getApplicationContext();
          return application.refWatcher;
      }
      @Override
      public void onCreate() {
          super.onCreate();
          refWatcher = LeakCanary.install(this);        // 检测
      }
    
      public void onDestroy() {
          RefWatcher refWatcher = TTApplication.getRefWatcher(getActivity());
          refWatcher.watch(this);                        //内存泄露检测
      }</code>
    

2、在 onTrimMemory 回调中,应该释放哪些资源

onTrimMemory()
回调中,应该在一些状态下清理掉不重要的内存资源。在不考虑内存泄露的情况下,有一些资源是我们主动缓存起来,以便我们在使用的过程中可以快速获取,而这部分资源就是我们清理的重点。

对于这些缓存,只要是读进内存内的都算,例如最常见的图片缓存、文件缓存等。拿图片缓存来说,市场上,常规的图片加载库,一般而言都是三级缓存,所以在内存吃紧的时候,我们就应该优先清理掉这部分图片缓存,毕竟图片是吃内存大户,而且再次回来的时候,虽然内存中的资源被回收掉了,我们依然可以从磁盘或者网络上恢复它。

除了资源缓存之外,还有一些页面相关的资源,也是占据内存的,可以考虑清理掉
Activity Task 中,多余的 Activity,只保留 Root Activity 。

其实核心思想,就是根据 onTrimMemory()
回调的一些信息,来释放我们持有的可被恢复,不那么重要的内存资源,以提高系统性能,已经保证当前
App 的进程不那么容易被系统回收。

奥门威尼斯网址 6

3. 常见的内存泄漏问题

3.1 单例造成的泄漏

将Context对象保存在单例模式中,instance对象本身持有一个Context对象的引用,活动即时被销毁也不能被回收,因为静态变量一直持有它的引用

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

可以改为

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {

        //    使用Application的Context(也可以用自定义的Application)
        this.context = context.getApplicationContext();        
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

3.2 非静态内部类的静态实例造成的泄漏

静态的sResource在创建时会间接持有一个MainActivity实例的引用,导致MainActivity无法被回收

public class MainActivity extends Activity {
    private static TestResource sResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (sResource == null) {
            sResource = new TestResource();
        }
        // ...
    }

    // 非静态内部类
    class TestResource {
        // ...
    }
}

将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例

如果用到Context就使用Application的Context

但是Dialog不能使用Application和Service的Context

3.3 Handler 造成的内存泄漏问题

当创建匿名对象时,该对象会间接持有外部类实例的一个引用,mHandler对象本身会持有MainActivity的引用,导致MainActivity销毁后无法即时被回收

public class MainActivity extends Activity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }

    private void loadData() {
        // ...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

在Activity中避免使用非静态内部类,比如将Handler声明为静态的,这样Handler的存活时间就与Activity无关了

同时引入弱引用的方式引入Activity,避免将Activity作为Context传入

使用前判空

public class MainActivity extends Activity {
    private static class MyHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;

        private MyHandler(MainActivity activity){
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }

    private void loadData() {
        // ...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

3.4 集合类泄漏

  • 如果集合类是全局的变量(类中的静态属性,全局性的map等既有静态引用或final一直指向它)
  • 没有相应的删除机制
  • 很可能导致集合所占用的内存只增不减

4. 避免内存泄漏的方法

  1. 尽量不要让静态变量引用Activity
  2. 使用WeakReference弱引用,会保证GC时会被回收
  3. 使用静态内部类来代替内部类,静态内部类不持有外部类的引用
  4. 静态内部类使用弱引用来引用外部类
  5. 在声明周期结束的时候释放资源

5. 减少内存使用

  1. 使用更轻量的数据结构(SpareArray代替HashMap)

    • Google自己定义的类占用内存更小
  2. 避免在onDraw方法中创建对象

    • onDraw()方法被频繁调用,在其中创建对象会导致临时对象过多,发生内存抖动
  3. 对象池(Message.obtain())

    • 当一定要在onDraw中创建对象,推荐使用对象池
    • 相当于对象缓冲,在创建时查找是否已经存在对象,没有在创建
  4. LRUCache

    • 大大减少内存使用
  5. Bitmap内存复用,压缩(inSampleSize,inBitmap)

  6. StringBuilder

    • 代替String,尤其是进行拼接操作时

发表评论

电子邮件地址不会被公开。 必填项已用*标注