IBigerBiger的成长之路

Android性能优化(四)-内存优化

内存划分机制

java的内存区域如何划分有两种方式

第一种是从抽象的JVM角度看的,主要分为堆(Heap),栈(Stacks)方法区(MethodArea),运行时常量池(RuntimeConstant Pool),本地方法栈(NativeMethod Stacks),PC Register(PC寄存器)。

如下所示



另外一种则是从操作系统上的进程的角度来看的,分为堆(Heap),栈(Stacks),数据段(data segment),代码段(code segment)

可以看到两种划分方式都将堆(Heap)栈(Stacks)单独提出来了,而我们关于内存优化其实与这两者有着比较大的关系,我们首先了解下什么是堆,什么是栈,堆栈内存有什么区别?

  • 堆(Heap) Heap内存的分配也叫做动态内存分配,java中运行环境用来分配给对象和JRE类的内存都在堆内存,C/C++有时候可以用malloc或者new来申请分配一个内存。在C/C++可能需要自己负责释放(java里面直接依赖GC机制)。

  • 栈(Stacks) Stack内存是相对于线程Thread而言的, 在执行函数(方法)时,函数一些内部变量的存储都可以放在栈上面创建,函数执行结束的时候这些存储单元就会自动被释放掉。栈内存包括分配的运算速度很快,因为内置在处理器的里面的。当然容量有限。它保存线程中方法中短期存在的变量值和对Heap中对象的引用等.

区别:堆是不连续的内存区域,堆空间比较灵活也特别大。 栈式一块连续的内存区域,大小是有操作系统觉决定的。堆管理很麻烦,频繁地new/remove会造成大量的内存碎片,这样就会慢慢导致效率低下。对于栈的话,他先进后出,进出完全不会产生碎片,运行效率高且稳定。

堆内存中存储的为对象实例,实际上是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在栈中)。对象实例在堆中分配好以后,需要在栈中保存一个4字节的堆内存地址,用来定位该对象实例在堆中的位置,便于找到该对象实例。

栈内存中存储的为基本数据类型, 指令代码,常量,对象的引用地址

Android系统并不会对堆(Heap)中空闲内存区域做碎片整理。系统仅仅会在新的内存分配之前判断Heap的尾端剩余空间是否足够,如果空间不够会触发gc操作,从而腾出更多空闲的内存空间。在Android的高级系统版本里面针对Heap空间有一个Generational Heap Memory的模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。例如,刚分配到Young Generation区域的对象通常更容易被销毁回收,同时在Young Generation区域的gc操作速度会比Old Generation区域的gc操作速度更快。如下图所示:



每一个Generation的内存区域都有固定的大小,随着新的对象陆续被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发GC的操作,以便腾出空间来存放其他新的对象。

我们通常说的内存泄露,是针对堆内存的. 因为栈内存在函数出栈的时候就销毁了。当内存占用达到Dalvik Heap的阈值时候就会发生我们比较熟悉的OOM(OutOfMemory)现象。

首先我们来了解下关于垃圾回收机制

垃圾回收(GC)

Java垃圾回收器

在C,C++或其他程序设计语言中,资源或内存都必须由程序员自行声明产生和回收,否则其中的资源将消耗,造成资源的浪费甚至崩溃。但手工回收内存往往是一项复杂而艰巨的工作。
于是,Java技术提供了一个系统级的线程,即垃圾收集器线程(Garbage Collection Thread),来跟踪每一块分配出去的内存空间,当Java 虚拟机(Java Virtual Machine)处于空闲循环时,垃圾收集器线程会自动检查每一快分配出去的内存空间,然后自动回收每一快可以回收的无用的内存块。

作用

1.清除不用的对象来释放内存:
采用一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。
2.消除堆内存空间的碎片:
由于创建对象和垃圾收集器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。

优点

1.减轻编程的负担,提高效率:
使程序员从手工回收内存空间的繁重工作中解脱了出来,因为在没有垃圾收集机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾收集机制可大大缩短时间。
2.它保护程序的完整性:
因此垃圾收集是Java语言安全性策略的一个重要部份。

缺点

1.占用资源时间:
Java虚拟机必须追踪运行程序中有用的对象, 而且最终释放没用的对象。这一个过程需要花费处理器的时间。
2.不可预知:
垃圾收集器线程虽然是作为低优先级的线程运行,但在系统可用内存量过低的时候,它可能会突发地执行来挽救内存资源。当然其执行与否也是不可预知的。
3.不确定性:
不能保证一个无用的对象一定会被垃圾收集器收集,也不能保证垃圾收集器在一段Java语言代码中一定会执行。
同样也没有办法预知在一组均符合垃圾收集器收集标准的对象中,哪一个会被首先收集。
4.不可操作
垃圾收集器不可以被强制执行,但程序员可以通过调用System. gc方法来建议执行垃圾收集器。

垃圾回收算法

1.引用计数(Reference Counting)
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
2.标记-清除(Mark-Sweep)
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
3.复制(Copying)
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
4.标记-整理(Mark-Compact)
此算法结合了 “标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象 “压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
5.增量收集(Incremental Collecting)
实施垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。
6.分代(Generational Collecting)
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

发生内存泄露怎么办?我们可以通过以下工具来检测是否发生内存泄露,并且定位内存泄露的地方

检测工具

MemoryMonitor

AndroidStudio 中Memory控件台(显示器)提供了一个内存监视器。我们可以通过它方便地查看应用程序的性能和内存使用情况,从而也就可以找到需要释放对象,查找内存泄漏等。

主要功能有:

  • 显示可用和已分配的Java存储器的随时间变化的曲线图。
  • 显示垃圾回收(GC)随着时间的推移事件。
  • 启动垃圾收集事件。
  • 快速测试应用程序缓慢是否会涉及到过多的垃圾收集事件。
  • 快速测试应用程序崩溃是否可能与运行内存

MemoryMonitor界面如图所示



Memory Monitor是Android Monitors中的一种,Monitors主要包括四种,Memory Monitor ,CPU Monitor ,NetWork Monitor, GPU Monitor ,由于我们这篇文章是与内存优化相关,所以只对 Memory Monitor进行介绍

可以看到图中有我标出了几个图标,这几个图标功能如下

  • 启动与关闭Memory监测按钮

  • GC按钮 ,可以手动GC,回收程序垃圾

  • 内存快照(Dump Java Heap) ,点击可以生成一个文件(包名+日期+“.hprof”),可以记录摸一个时间点内,程序内存的情况

  • start(stop) allocation tracking按钮先点击一次,可以看到Memory Recorder开始转动,对APP进行相应的操作。在合适的时间再点一次,结束记录。

接下来用一个内存泄漏的小例子说明MemoryMonitor的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MemoryMonitorManager {
private static MemoryMonitorManager instance;
private Context mContext;
private MemoryMonitorManager(Context context) {
this.mContext = context;
}
public static MemoryMonitorManager getInstance(Context context) {
if (instance == null) {
instance = new MemoryMonitorManager(context);
}
return instance;
}
}
1
2
3
4
5
6
7
8
9
10
public class MemoryMonitorActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MemoryMonitorManager memoryMonitorManager = MemoryMonitorManager.getInstance(this);
}
}

当我们运行后,旋转一下手机,会发现MemoryMonitor的显示如下



我们会发现内存上升了,这是反常的,因为不做特殊处理的情况下,横竖屏切换是Activity的销毁与重新创建的流程,那么这我们这端代码里面肯定就存在内存泄露的情况。

接下来就是寻找哪里发生了内存泄露了,点击Dump Java Heap,生成快照文件,Android Studio 自动弹出HPROF Viewer来分析它。

显示如下



可以看到我箭头指的位置,点击打开,点击上面的绿色按钮可以直接把内存泄露可能的类找出来,如下



从HPROF Viewer我们可以得到如下信息

  • 1.在Analyzer Tasks区域中,直接告诉你Leaked Activities,MemoryMonitorActivity包含其中
  • 2.可以看见两个MemoryMonitorActivity的实例,点击一个看他的引用树情况
  • 3.一个Activity应该只有一个实例,但是从A区域来看 total count的值为2,heap count的值也为2,说明有一个是多余的。
  • 4.可以看见两个MemoryMonitorActivity的实例,点击一个看他的引用树情况

多方面的证据表明MemoryMonitorActivity发生了内存泄露,至于怎么解决,应该不用我够多介绍了。

MAT

MAT工具全称为Memory Analyzer Tool,MAT是一款详细分析Java堆内存的工具,该工具非常强大,而使用该工具,我们需要hprof文件。这个文件我们在前面的Memory Monitor有提到过,但是该文件不能直接被MAT使用,需要进行一步转化,可以使用hprof-conv命令来转化,但是我们也可以选择用AndroidStudio可以直接转化。

如下



接下来将导出的文件放到MAT中分析即可,MAT一般选择用Histogram分析

如下所示



它按类名将所有的实例对象列出来,可以点击表头进行排序,在表的第一行可以输入正则表达式来匹配结果,如下



可以看到这里列出了我们需要的实例对象,可以看到这里存在两个MemoryMonitorActivity的实例,在某一项上右键打开菜单选择 list objects ->with incoming refs 将列出该类的实例,快速找出某个实例没被释放的原因,可以右健 Path to GC Roots–>exclue all phantom/weak/soft etc. reference,如下



从这里可以看到MemoryMonitorManager一直引用着这个MemoryMonitorActivity实例,从而不能释放,用这个方法可以快速找到某个对象的GC Root,一个存在GC Root的对象是不会被GC回收掉的。

当然MAT还可以将两个hprof文件进行对比,从而发现内存泄露的原因,如下



内存优化

栈存放基本数据类型变量和对象的引用,为了可以更加灵活的控制对象的生命周期与避免内存泄露的情况,这里介绍一下关于对象引用类型

  • 强引用(strong reference)

    如:Object object=new Object(),object就是一个强引用了。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

  • 软引用(SoftReference)

    只有内存不够时才回收,常用于缓存;当内存达到一个阀值,GC就会去回收它。

  • 弱引用(WeakReference)

    弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

  • 虚引用(PhantomReference)

    “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

这四种级别由高到低依次为:强引用>软引用>弱引用>虚引用。

减少内存占用

这个其实在上一篇文章中介绍了各种优化的选择方案,可以有效的减少对于内存占用

除此之外还有其他优化的点如下:

1.对常量使用static final修饰符

将一个方法或类声明为final不会带来性能的提升,但是会帮助编译器优化代码。举例说,如果编译器知道一个getter方法不会被重载,那么编译器会对其采用内联调用。

你也可以将本地变量声明为final,同样,这也不会带来性能的提升。使用“final”只能使本地变量看起来更清晰些(但是也有些时候这是必须的,比如在使用匿名内部类的时候)。

2.静态方法代替虚拟方法

如果不需要访问某对象的字段,将方法设置为静态,调用会加速15%到20%。这也是一种好的做法,因为你可以从方法声明中看出调用该方法不需要更新此对象的状态。

3.避免内部Getters/Setters

源生语言像C++中,通常做法是用Getters(i=getCount())代替直接字段访问(i=mCount)。这是C++中一个好的习惯,因为编译器会内联这些访问,并且如果需要约束或者调试这些域的访问,你可以在任何时间添加代码。而在Android中,这不是一个好的做法。虚方法调用的代价比直接字段访问高昂许多。通常根据面向对象语言的实践,在公共接口中使用Getters和Setters是有道理的,但在一个字段经常被访问的类中宜采用直接访问。

内存重复利用

1.复用系统自带资源

Android系统本身内置了很多的资源,例如字符串/颜色/图片/动画/样式以及简单布局等等,这些资源都可以在应用程序中直接引用。这样做不仅仅可以减少应用程序的自身负重,减小APK的大小,另外还可以一定程度上减少内存的开销,复用性更好。但是也有必要留意Android系统的版本差异性,对那些不同系统版本上表现存在很大差异,不符合需求的情况,还是需要应用程序自身内置进去。

2.列表控件复用问题

使用ListView与GridView需要注意复用问题,RecycleView则已经将复用集成进去了

3.池(PooL)

线程池:
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

对象池:
对象池使用的基本思路是:将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象所造成的开销。 并非所有对象都适合拿来池化――因为维护对象池也要造成一定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。但是对于生成时开销可观的对象,池化技术就是提高性能的有效策略了。

4.Bitmap缓存

Bitmap缓存分为两种:
一种是内存缓存,一种是硬盘缓存。

内存缓存(LruCache):
以牺牲宝贵的应用内存为代价,内存缓存提供了快速的Bitmap访问方式。系统提供的LruCache类是非常适合用作缓存Bitmap任务的,它将最近被引用到的对象存储在一个强引用的LinkedHashMap中,并且在缓存超过了指定大小之后将最近不常使用的对象释放掉。

硬盘缓存(DiskLruCache):
一个内存缓存对加速访问最近浏览过的Bitmap非常有帮助,但是你不能局限于内存中的可用图片。GridView这样有着更大的数据集的组件可以很轻易消耗掉内存缓存。你的应用有可能在执行其他任务(如打电话)的时候被打断,并且在后台的任务有可能被杀死或者缓存被释放。一旦用户重新聚焦(resume)到你的应用,你得再次处理每一张图片。

资源回收

1.Thread(线程)回收

线程中涉及的任何东西GC都不能回收(Anything reachable by a thread cannot be GC’d ),所以线程很容易造成内存泄露。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Thread t = new Thread() {
public void run() {
while (true) {
try {
Thread.sleep(1000);
System.out.println("thread is running...");
} catch (InterruptedException e) {}
}
}
};
t.start();
t = null;
System.gc();

如上在线程t中每间隔一秒输出一段话,然后将线程设置为null并且调用System.gc方法,最后的结果是线程并不会被回收,它会一直的运行下去。

因为运行中的线程是称之为垃圾回收根(GC Roots)对象的一种,不会被垃圾回收。当垃圾回收器判断一个对象是否可达,总是使用垃圾回收根对象作为参考点。

2.Cursor(游标)回收

Cursor是Android查询数据后得到的一个管理数据集合的类,在使用结束以后。应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor close掉,因为在源代码中我们发现,如果等到垃圾回收器来回收时,会给用户以错误提示。

3.Receiver(接收器)回收

调用registerReceiver()后未调用unregisterReceiver()也会产生内存问题,当我们Activity中使用了registerReceiver()方法注册了BroadcastReceiver,一定要在Activity的生命周期内调用unregisterReceiver()方法取消注册也就是说registerReceiver()和unregisterReceiver()方法一定要成对出现

4.注意监听器的注销

在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的listener,需要记得及时remove这个listener。(记得之前使用EventBus,没有在onDestroy注销,从而导致了内存问题)

5.集合中对象回收
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

6.Stream/File(流/文件)回收:

主要针对各种流,文件资源等等如:
InputStream/OutputStream,SQLiteOpenHelper,SQLiteDatabase,Cursor,文件,I/O,Bitmap图片等操作等都应该记得及时关闭。

常见的内存泄露案例

1.单例造成的内存泄露

这个其实在前面关于内存泄露相关分析工具中也有相关介绍了

单例的静态特性导致其生命周期同应用一样长。

泄露代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MemoryMonitorManager {
private static MemoryMonitorManager instance;
private Context mContext;
private MemoryMonitorManager(Context context) {
this.mContext = context;
}
public static MemoryMonitorManager getInstance(Context context) {
if (instance == null) {
instance = new MemoryMonitorManager(context);
}
return instance;
}
}

解决方案

(1)将该属性的引用方式改为弱引用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MemoryMonitorManager {
private static MemoryMonitorManager instance;
private WeakReference<Context> mContext;
private MemoryMonitorManager(Context context) {
this.mContext = new WeakReference<Context>(context);
}
public static MemoryMonitorManager getInstance(Context context) {
if (instance == null) {
instance = new MemoryMonitorManager(context);
}
return instance;
}
}

(2)使用ApplicationContext代替Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MemoryMonitorManager {
private static MemoryMonitorManager instance;
private Context mContext;
private MemoryMonitorManager(Context context) {
this.mContext = context.getApplicationContext();
}
public static MemoryMonitorManager getInstance(Context context) {
if (instance == null) {
instance = new MemoryMonitorManager(context);
}
return instance;
}
}
2.InnerClass匿名内部类

在Java中,非静态内部类和匿名类 都会潜在的引用它们所属的外部类,但是,静态内部类却不会。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。

如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
innerClass();
}
//泄漏处
public void innerClass() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}

解决方案

将内部类变成静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static public void innerClass() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}

另外如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用,在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务。

3.Activity Context的不正确使用

在Android应用程序中通常可以使用两种Context对象:Activity和Application。当类或方法需要Context对象的时候常见的做法是使用第一个作为Context参数。这样就意味着View对象对整个Activity保持引用,因此也就保持对Activty的所有的引用。

假设一个场景,当应用程序有个比较大的Bitmap类型的图片,每次旋转是都重新加载图片所用的时间较多。为了提高屏幕旋转是Activity的创建速度,最简单的方法时将这个Bitmap对象使用Static修饰。 当一个Drawable绑定在View上,实际上这个View对象就会成为这份Drawable的一个Callback成员变量。而静态变量的生命周期要长于Activity。导致了当旋转屏幕时,Activity无法被回收,而造成内存泄露。

如下代码

1
2
3
4
5
6
7
8
9
10
11
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView textView = new TextView(this);
if (sBackground == null) {
sBackground = getDrawable(R.drawable.xxx);
}
textView.setBackgroundDrawable(sBackground);
setContentView(textView);
}

解决方案

使用ApplicationContext代替ActivityContext,因为ApplicationContext会随着应用程序的存在而存在,而不依赖于activity的生命周期。

1
2
3
4
5
6
7
8
9
10
11
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView textView = new TextView(this);
if (sBackground == null) {
sBackground = getApplicationContext().getDrawable(R.drawable.xxx);
}
textView.setBackgroundDrawable(sBackground);
setContentView(textView);
}

对Context的引用不要超过它本身的生命周期,慎重的对Context使用“static”关键字。Context里如果有线程,一定要在onDestroy()里及时停掉。

4.Handler引起的内存泄漏

当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用。而Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,这个后台线程在任务执行完毕(例如图片下载完毕)之后,通过消息机制通知Handler,然后Handler把图片更新到界面。然而,如果用户在网络请求过程中关闭了Activity,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露),直到网络请求结束(例如图片下载完毕)。另外,如果你执行了Handler的postDelayed()方法,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。

代码如下

1
2
3
4
5
6
7
8
9
10
private Handler handler = new Handler()
{
public void handleMessage(android.os.Message msg)
     {
if (msg.what == 1)
        {
...
}
}
};

解决方案:

(1).通过程序逻辑来进行保护

在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。

如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。

(2).将Handler声明为静态类。

静态类不持有外部类的对象,所以你的Activity可以随意被回收。由于Handler不再持有外部类对象的引用,导致程序不允许你在Handler中操作Activity中的对象了。所以你需要在Handler中增加一个对Activity的弱引用(WeakReference)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static class handler extends Handler
{
WeakReference<Activity> mWeakReference;
public MyHandler(Activity activity)
{
mWeakReference=new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg)
{
final Activity activity=mWeakReference.get();
if(activity!=null)
{
if (msg.what == 1)
{
noteBookAdapter.notifyDataSetChanged();
}
}
}
}
5.WebView造成内存泄露

Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在着很大的差异。更严重的是标准的WebView也存在内存泄露的问题。

解决方案:

为WebView开启另外一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。