Android内存泄漏定位、分析、解决全方案

47次阅读

共计 6117 个字符,预计需要花费 16 分钟才能阅读完成。

原文链接
更多教程

为什么会发生内存泄漏

内存空间使用完毕之后未回收, 会导致内存泄漏。有人会问:Java 不是有垃圾自动回收机制么?不幸的是,在 Java 中仍存在很多容易导致内存泄漏的逻辑(logical leak)。虽然垃圾回收器会帮我们干掉大部分无用的内存空间,但是对于还保持着引用,但逻辑上已经不会再用到的对象,垃圾回收器不会回收它们。
例如

忘记释放分配的内存的。(Cursor 忘记关闭等)。
应用不再需要这个对象,未释放该对象的所有引用。
强引用持有的对象,垃圾回收器是无法在内存中回收这个对象。
持有对象生命周期过长,导致无法回收。

Java 判断无效对象的原理
Android 内存回收管理策略图:
图中的每个圆节点代表对象的内存资源,箭头代表可达路径。当圆节点与 GC Roots 存在可达路径时,表示当前资源正被引用,虚拟机是无法对其进行回收的(如图中的黄色节点)。反过来,如果圆节点与 GC Roots 不存在可达路径,则意味着这块对象的内存资源不再被程序引用,系统虚拟机可以在 GC 过程中将其回收掉。
从定义上讲,Android(Java)平台的内存泄漏是指没有用的对象资源任与 GC-Root 保持可达路径,导致系统无法进行回收。
内存泄漏带来的危害

用户对单次的内存泄漏并没有什么感知,但当泄漏积累到内存都被消耗完,就会导致卡顿,崩溃。
内存泄露是内存溢出 OOM 的重要原因之一,会导致 Crash

Android 中常见的可能发生内存泄漏的地方

1. 在 Android 开发中,最容易引发的内存泄漏问题的是 Context。
比如 Activity 的 Context,就包含大量的内存引用,一旦泄漏了 Context,也意味泄漏它指向的所有对象。
造成 Activity 泄漏的常见原因:
Static Activities
在类中定义了静态 Activity 变量,把当前运行的 Activity 实例赋值于这个静态变量。如果这个静态变量在 Activity 生命周期结束后没有清空,就导致内存泄漏。因为 static 变量是贯穿这个应用的生命周期的,所以被泄漏的 Activity 就会一直存在于应用的进程中,不会被垃圾回收器回收。
static Activity activity; // 这种代码要避免
单例中保存 Activity
在单例模式中,如果 Activity 经常被用到,那么在内存中保存一个 Activity 实例是很实用的。但是由于单例的生命周期是应用程序的生命周期,这样会强制延长 Activity 的生命周期,这是相当危险而且不必要的,无论如何都不能在单例子中保存类似 Activity 的对象。举例:
public class Singleton {
private static Singleton instance;
private Context mContext;
private Singleton(Context context){
this.mContext = context;
}

public static Singleton getInstance(Context context){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton(context);
}
}
}
return instance;
}
}
在调用 Singleton 的 getInstance()方法时传入了 Activity。那么当 instance 没有释放时,这个 Activity 会一直存在。因此造成内存泄露。解决方法:
可以将 new Singleton(context)改为 new Singleton(context.getApplicationContext())即可,这样便和传入的 Activity 没关系了。
Static Views
同理,静态的 View 也是不建议的
Inner Classes
内部类的优势可以提高可读性和封装性,而且可以访问外部类,不幸的是,导致内存泄漏的原因,就是内部类持有外部类实例的强引用。例如在内部类中持有 Activity 对象
解决方法:
1. 将内部类变成静态内部类;2. 如果有强引用 Activity 中的属性,则将该属性的引用方式改为弱引用;3. 在业务允许的情况下,当 Activity 执行 onDestory 时,结束这些耗时任务;
例如:发生内存泄漏的代码:
public class LeakAct extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.aty_leak);
test();
}
// 这儿发生泄漏
public void test() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
解决方法:
public class LeakAct extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.aty_leak);
test();
}
// 加上 static,变成静态匿名内部类
public static void test() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
Anonymous Classes
匿名类也维护了外部类的引用。当你在匿名类中执行耗时任务,如果用户退出,会导致匿名类持有的 Activity 实例就不会被垃圾回收器回收,直到异步任务结束。

原文链接
更多教程

Handler
handler 中,Runnable 内部类会持有外部类的隐式引用,被传递到 Handler 的消息队列 MessageQueue 中,在 Message 消息没有被处理之前,Activity 实例不会被销毁了,于是导致内存泄漏。解决方法:
1. 可以把 Handler 类放在单独的类文件中,或者使用静态内部类便可以避免泄露;2. 如果想在 Handler 内部去调用所在的 Activity, 那么可以在 handler 内部使用弱引用的方式去指向所在 Activity. 使用 Static + WeakReference 的方式来达到断开 Handler 与 Activity 之间存在引用关系的目的.3. 在界面销毁是,释放 handler 资源
@Override
protected void doOnDestroy() {
super.doOnDestroy();
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
mHandler = null;
mRenderCallback = null;
}
同样还有其他匿名类实例,如 TimerTask、Threads 等,执行耗时任务持有 Activity 的引用,都可能导致内存泄漏。
线程产生内存泄露的主要原因在于线程生命周期的不可控。如果我们的线程是 Activity 的内部类,所以 MyThread 中保存了 Activity 的一个引用,当 MyThread 的 run 函数没有结束时,MyThread 是不会被销毁的,因此它所引用的老的 Activity 也不会被销毁,因此就出现了内存泄露的问题。
要解决 Activity 的长期持有造成的内存泄漏,可以通过以下方法:

传入 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。
如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,主动结束执行的任务,并释放 Activity 资源。
将线程的内部类,改为静态内部类。

因为非静态内部类会自动持有一个所属类的实例,如果所属类的实例已经结束生命周期,但内部类的方法仍在执行,就会 hold 其主体(引用)。也就使主体不能被释放,亦即内存泄露。静态类编译后和非内部类是一样的,有自己独立的类名。不会悄悄引用所属类的实例,所以就不容易泄露。

如果需要引用 Acitivity,使用弱引用。
谨慎对 context 使用 static 关键字。

2.Bitmap 没调用 recycle()
Bitmap 对象在不使用时, 我们应该先调用 recycle()释放内存,然后才设置为 null.
3. 集合中对象没清理造成的内存泄露
我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是 static 的话,那情况就更严重了。解决方案:
在 Activity 退出之前,将集合里的东西 clear,然后置为 null,再退出程序。
4. 注册没取消造成的内存泄露
这种 Android 的内存泄露比纯 Java 的内存泄漏还要严重,因为其他一些 Android 程序可能引用系统的 Android 程序的对象(比如注册机制)。即使 Android 程序已经结束了,但是别的应用程序仍然还有对 Android 程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。解决方案:
1. 使用 ApplicationContext 代替 ActivityContext;2. 在 Activity 执行 onDestory 时,调用反注册;
5. 资源对象没关闭造成的内存泄露
资源性对象比如(Cursor,File 文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。而不是等待 GC 来处理。
6. 占用内存较多的对象 (图片过大) 造成内存溢出
因为 Bitmap 占用的内存实在是太多了,特别是分辨率大的图片,如果要显示多张那问题就更显著了。Android 分配给 Bitmap 的大小只有 8M. 解决方法:
1. 等比例缩小图片
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;// 图片宽高都为原来的二分之一,即图片为原来的四分之一
2. 对图片采用软引用,及时地进行 recycle()操作
// 软引用
SoftReference<Bitmap> bitmap = new SoftReference<Bitmap>(pBitmap);
// 回收操作
if(bitmap != null) {
if(bitmap.get() != null && !bitmap.get().isRecycled()){
bitmap.get().recycle();
bitmap = null;
}
}
7.WebView 内存泄露(影响较大)
解决方案:
用新的进程起含有 WebView 的 Activity, 并且在该 Activity 的 onDestory() 最后加上 System.exit(0); 杀死当前进程。
检测内存泄漏的方法

1. 使用 静态代码分析工具 -Lint 检查内存泄漏
Lint 是 Android Studio 自带的工具,使用姿势很简单 Analyze -> Inspect Code 然后选择想要扫面的区域即可

对可能引起泄漏的编码,Lint 都会进行温馨提示:

2.LeakCanary 工具

Square 公司出品的内存分析工具,官方地址如下:https://github.com/square/lea…LeakCanary 需要在项目代码中集成,不过代码也非常简单,如下的官方示例:
在你的 build.gradle:
dependencies {
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:1.6.3’
releaseImplementation ‘com.squareup.leakcanary:leakcanary-android-no-op:1.6.3’
// Optional, if you use support library fragments:
debugImplementation ‘com.squareup.leakcanary:leakcanary-support-fragment:1.6.3’
}
在 Application 类:
public class ExampleApplication extends Application {

@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code…
}
}
当内存泄漏发生时,LeakCanary 会弹窗提示并生成对应的堆存储信息记录
-3.Android Monitor
开 Android Studio,编译代码,在模拟器或者真机上运行 App,然后点击
,在 Android Monitor 下点击 Monitor 对应的 Tab,进入如下界面

在 Memory 一栏中,可以观察不同时间 App 内存的动态使用情况,点击
可以手动触发 GC,点击
可以进入 HPROF Viewer 界面,查看 Java 的 Heap,如下图
Reference Tree 代表指向该实例的引用,可以从这里面查看内存泄漏的原因,Shallow Size 指的是该对象本身占用内存的大小,Retained Size 代表该对象被释放后,垃圾回收器能回收的内存总和。
扩展知识

四种引用类型的介绍

强引用(StrongReference):JVM 宁可抛出 OOM,也不会让 GC 回收具有强引用的对象;
软引用(SoftReference):只有在内存空间不足时,才会被回的对象;
弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
虚引用(PhantomReference):任何时候都可以被 GC 回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为 GC 回收 Object 的标志。

原文链接
更多教程

正文完
 0