android 内存泄漏,以及检测方法

摘要:
我们知道Android系统为每个应用程序分配的内存是有限的。当应用程序中存在许多内存泄漏时,应用程序所需的内存将不可避免地超过系统分配的内存配额,这将导致内存溢出和应用程序崩溃。在处理消息之前,此引用关系将一直保持,这会阻止MainActivity被垃圾收集器回收,从而导致内存泄漏。

1、为什么会产生内存泄漏

当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

2、内存泄漏对程序的影响

内存泄漏是造成应用程序OOM的主要原因之一。我们知道Android系统为每个应用程序分配的内存是有限的,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过系统分配的内存限额,这就造成了内存溢出从而导致应用Crash。

3、如何检查和分析内存泄漏

因为内存泄漏是在堆内存中,所以对我们来说并不是可见的。通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。

1、MAT是一款强大的内存分析工具,功能繁多而复杂。

2、LeakCanary则是由Square开源的一款轻量级的第三方内存泄漏检测工具,当检测到程序中产生内存泄漏时,它将以最直观的方式告诉我们哪里产生了内存泄漏和导致谁泄漏了而不能被回收。(LeakCanary使用教程https://www.liaohuqiu.net/cn/posts/leak-canary-read-me/)

4、常见的内存泄漏及解决方法

1、单例造成的内存泄漏

由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。

示例:防止单例导致内存泄漏的实例  

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;   } }

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。
2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用

程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

应该改成如下:

public class AppManager {
  private static AppManager instance;
  private Context context;
  private AppManager() {
    this.context = MyApplication.getContext();// 使用Application 的context
  }
  public static AppManager getInstance() {
    if (instance == null) {
      instance = new AppManager();
    }
    return instance;
  }
}

2、非静态内部类创建静态实例造成的内存泄漏

例如,有时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现如下写法:

public class MainActivity extends AppCompatActivity {
 
    private static TestResource mResource = null;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null){
            mResource = new TestResource();
        }
        //...
    }
 
    class TestResource {
    //...
    }
}

这样在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据。虽然这样避免了资源的重复创建,但是这种写法却会造成内存泄漏。因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。

解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。

3、Handler造成的内存泄漏

示例:创建匿名内部类的静态对象public class MainActivity extends AppCompatActivity {
 
    private final Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                // ...
                handler.sendEmptyMessage(0x123);
            }
        });
    }
}

1、从Android的角度

当Android应用程序启动时,该应用程序的主线程会自动创建一个Looper对象和与之关联的MessageQueue。当主线程中实例化一个Handler对象后,
它就会自动与主线程Looper的MessageQueue关联起来。所有发送到MessageQueue的Messag都会持有Handler的引用,所以Looper会据此回调
Handle的handleMessage()方法来处理消息。只要MessageQueue中有未处理的Message,Looper就会不断的从中取出并交给Handler处理。另外,
主线程的Looper对象会伴随该应用程序的整个生命周期。

2、 Java角度
在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。

对上述的示例进行分析,当MainActivity结束时,未处理的消息持有handler的引用,而handler又持有它所属的外部类也就是MainActivity的引用。
这条引用关系会一直保持直到消息得到处理,这样阻止了MainActivity被垃圾回收器回收,从而造成了内存泄漏。
解决方法:将Handler类独立出来或者使用静态内部类,这样便可以避免内存泄漏。

5、资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源
将不会被回收,从而造成内存泄漏。

1)比如在Activity中register了一个BraodcastReceiver,但在Activity结束后没有unregister该BraodcastReceiver。

2)资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的
缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。

3)对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。
4)Bitmap对象不在使用时调用recycle()释放内存。

6、使用ListView时造成的内存泄漏

初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象缓存起来。当向上滚动ListView时,
原先位于最上面的Item的View对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参
convertView就是被缓存起来的Item的View对象(初始化时缓存中没有View对象则convertView是null)。

构造Adapter时,没有使用缓存的convertView。
解决方法:在构造Adapter时,使用缓存的convertView。

7、集合容器中的内存泄露

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

8、WebView造成的泄露

当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。

解决方法:为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内

存的完整释放。

如何避免内存泄漏?

1、在涉及使用Context时,对于生命周期比Activity长的对象应该使用Application的Context。凡是使用Context优先考虑Application的Context,当然

它并不是万能的,对于有些地方则必须使用Activity的Context。对于Application,Service,Activity三者的Context的应用场景如下:

android 内存泄漏,以及检测方法第1张

其中,NO1表示Application和Service可以启动一个Activity,不过需要创建一个新的task任务队列。而对于Dialog而言,只有在Activity中才能创建。
除此之外三者都可以使用。

2、对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏。
3、对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null。
4、保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。
5、对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
1)将内部类改为静态内部类
2)静态内部类中使用弱引用来引用外部类的成员变量

免责声明:文章转载自《android 内存泄漏,以及检测方法》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇webpack打包将配置文件单独抽离不压缩打包Android-Native-Server 启动和注册详细分析下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

java中子类继承父类程序执行顺序问题

Java中,new一个类的对象,类里面的静态代码块、非静态代码、无参构造方法、有参构造方法、类的一般方法等部分,它们的执行顺序相对来说比较简单,用程序也很容易验证。比如新建一个测试父类。 publicclassFatherTest { privateString name; FatherTest(){ System.out.println("--父类的无参...

iOS—静态方法(类方法)和实例方法

1.实例方法/动态方法     a).标识符:-     b).调用方式:(实例对象    函数)     c).实例方法在堆栈上。 2.静态方法/类方法     a).标识符:+     b).调用方式:(类    函数)     c).静态方法在堆上分配内存。 3.静态方法和实例方法的区分      a).静态方法常驻内存,实例方法不是,所以静态方法效...

由浅入深TheradLocal

线程并发:在多线程并发的场景下 传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量 线程隔离:每个线程的变量都是独立的,不会相互影响 常用方法 方法声明 描述 ThreadLocal() 创建ThreadLocal对象 public void set(T value) 设置当前线程绑定的局部变量 public...

多个类用@feignclient标注同一个服务,出错问题:Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.Caused by: org.springframework.beans.factory.support.Bea..

如果标注了两个或以上类 @FeignClient 标注同一个 服务名称 调用方会主配置类启动会报错   测试类报错   java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.context.cache.D...

prometheus学习笔记(2)-利用java client写入数据

继续学习prometheus,上一节演示了用http方式使用curl向pushgateway发送数据,本节将研究如何利用client jar包,以java代码的方式写入数据。 一、依赖的jar包 1 <dependency> 2 <groupId>io.prometheus</groupId> 3...

Android 桌面组件【widget】初探

转自:http://www.cnblogs.com/TerryBlog/archive/2010/07/29/1788319.html 本来打算晚上继续 Api Demos 系列的,不过今天下午的时候无聊去玩了一下桌面组件 App Widget 觉得挺不错的一个东西,对它很是感兴趣,玩了一下碰到很多问题,一直在解决问题到了晚上10点。只能怪自己理解不深刻,...