Android内存管理(13)常见产生内存泄漏的原因

1.集合类泄漏

  集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多留一些心眼。

2.static 成员

  如果static成员比它外部的类生命周期长,如果在static成员时引用了外部类,那么外部类不会以被释放。

2.1 static成员易造成内存泄漏

  如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。
  这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,以为如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。
  这里修复的方法是:
    不要在类初始时初始化静态成员。可以考虑lazy初始化。 架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

2.2.单例造成的内存泄漏

  由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子,

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

  这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

  • 如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。
  • 如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

  正确的方式应该改为下面这种方式:

 1 public class AppManager {
 2         private static AppManager instance;
 3         private Context context;
 4         private AppManager(Context context) {
 5             this.context = context.getApplicationContext();// 使用Application 的context
 6         }
 7         public static AppManager getInstance(Context context) {
 8             if (instance == null) {
 9                 instance = new AppManager(context);
10             }
11             return instance;
12         }
13     }

  或者这样写,连 Context 都不用传进来了:
  在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context,

 1 ...
 2 
 3     context = getApplicationContext();
 4 
 5     ...
 6     /**
 7      * 获取全局的context
 8      * @return 返回全局context对象
 9      */
10     public static Context getContext(){
11         return context;
12     }
13 
14     public class AppManager {
15         private static AppManager instance;
16         private Context context;
17         private AppManager() {
18             this.context = MyApplication.getContext();// 使用Application 的context
19         }
20         public static AppManager getInstance() {
21             if (instance == null) {
22                 instance = new AppManager();
23             }
24             return instance;
25         }
26     }
27     

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

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

 1   public class MainActivity extends AppCompatActivity {
 2         private static TestResource mResource = null;
 3         @Override
 4         protected void onCreate(Bundle savedInstanceState) {
 5             super.onCreate(savedInstanceState);
 6             setContentView(R.layout.activity_main);
 7             if(mManager == null){
 8                 mManager = new TestResource();
 9             }
10 //...
11         }
12         class TestResource {
13 //...
14         }
15     }

  这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为:
  将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。当然,Application 的 context 不是万能的,所以也不能随便乱用。

3.匿名内部类易产生内存泄漏

  看下面2个情况:

3.1 Runable/Thread造成的内存泄漏

  android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露

 1   public class MainActivity extends Activity {
 2         ...
 3         Runnable ref1 = new MyRunable();
 4         Runnable ref2 = new Runnable() {
 5             @Override
 6             public void run() {
 7 
 8             }
 9         };
10         ...
11     }

  ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这两个引用的内存:
    可以看到,ref1没什么特别的。
    但ref2这个匿名类的实现对象里面多了一个引用:

  
  this$0这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。

3.2.Handler 造成的内存泄漏

  Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。
  由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。举个例子:

 1   public class SampleActivity extends Activity {
 2 
 3         private final Handler mLeakyHandler = new Handler() {
 4             @Override
 5             public void handleMessage(Message msg) {
 6 // ...
 7             }
 8         }
 9 
10         @Override
11         protected void onCreate(Bundle savedInstanceState) {
12             super.onCreate(savedInstanceState);
13 
14 // Post a message and delay its execution for 10 minutes.
15             mLeakyHandler.postDelayed(new Runnable() {
16                 @Override
17                 public void run() { /* ... */ }
18             }, 1000 * 60 * 10);
19 
20 // Go back to the previous Activity.
21             finish();
22         }
23     }

  在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。
  修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:

 1   public class SampleActivity extends Activity {
 2 
 3         /**
 4          * Instances of static inner classes do not hold an implicit
 5          * reference to their outer class.
 6          */
 7         private static class MyHandler extends Handler {
 8             private final WeakReference<SampleActivity> mActivity;
 9 
10             public MyHandler(SampleActivity activity) {
11                 mActivity = new WeakReference<SampleActivity>(activity);
12             }
13 
14             @Override
15             public void handleMessage(Message msg) {
16                 SampleActivity activity = mActivity.get();
17                 if (activity != null) {
18 // ...
19                 }
20             }
21         }
22 
23         private final MyHandler mHandler = new MyHandler(this);
24 
25         /**
26          * Instances of anonymous classes do not hold an implicit
27          * reference to their outer class when they are "static".
28          */
29         private static final Runnable sRunnable = new Runnable() {
30             @Override
31             public void run() { /* ... */ }
32         };
33 
34         @Override
35         protected void onCreate(Bundle savedInstanceState) {
36             super.onCreate(savedInstanceState);
37 
38 // Post a message and delay its execution for 10 minutes.
39             mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
40 
41 // Go back to the previous Activity.
42             finish();
43         }
44     }    

  综述,即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。

4.避免 override finalize()

  • finalize 方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是: 虚拟机调用GC的时间不确定 Finalize daemon线程被调度到的时间不确定
  • finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize 方法,再次被 GC 时也不会再执行了,原因是:
    含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该Object的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。
  • 含有Finalize方法的object需要至少经过两轮GC才有可能被释放。

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

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

6.一些不良代码造成的内存压力

  有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。
  比如: Bitmap 没调用 recycle()方法,对于 Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。 构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用 ViewHolder。

7.总结

  • 对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就不考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。
  • 尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。
  • 对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
  将内部类改为静态内部类
  静态内部类中使用弱引用来引用外部类的成员变量
  • Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable.
  • 在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。
  • 正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。
  • 保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。

 

posted @ 2016-04-04 00:17  f9q  阅读(489)  评论(0编辑  收藏  举报