浅谈Android开发中内存泄露与优化与框架模式之MVC与MVP

时间:2022-01-08 21:16:57

》浅谈Android开发中内存泄露与优化

内存泄露是在Android开发中尤其要重视的一个问题,对开发人员开说,这是一个很容易犯也很常见的错误。优化内存泄露的问题,主要从两方面着手,一是开发人员避免写出有内存泄露的代码,二是通过一些诸如MAT的内存分析工具来找出潜在的内存泄露并解决它。

其实平时遇到的最多的情况,就是对Activity或Context保持一个长生命周期的引用。下面主要来分析一下造成内存泄露的各种原因。

一、静态变量导致的内存泄露

要不怎么说static关键字要慎用呢?来看看下面这段代码,Context对象为静态的,那么Activity就无法正常销毁,会常驻内存。

  1. public class MemoryActivity extends Activity{  
  2.     public static Context mContext;  
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         mContext = this;  
  8.     }  
  9. }  

这是一个很隐晦的内存泄漏的情况,在开发过程中,Context能使用ApplicationContext得尽量使用ApplicationContext,因为这个Context的生存周期和你的应用的生存周期一样长,而不是取决于activity的生存周期。除此之外,在Activity里面创建了静态的View,这就意味着该View持有一个对当前这个Activity的引用,那么Activity也是无法正常销毁的。

二、引用没释放导致的内存泄露

1、注册服务没取消导致的内存泄露

假如我们在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。但是如果在释放LockScreen对象的时候没有取消之前注册的PhoneStateListener对象,那么则会导致LockScreen无法被垃圾回收。而锁屏界面又不断的创建和销毁,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory。类似的,BraodcastReceiver,ContentObserver,FileObserver在Activity onDeatory或者某类声明周期结束之后一定要unregister掉,否则这个Activity类会被system强引用,不会被内存回收。

2、集合中的对象没有及时清理导致的内存泄露

当该集合为静态的时候,那么在集合里面对象越来越多的时候,最好要及时清理不需要用到的对象。

三、单例模式导致的内存泄露

单例模式的特点就是它的生命周期和Application一样,那么如果某个Activity实例被一个单例所持有,也就是说在单例里面引用了它,那么就会造成Activity对象无法正常回收释放。

四、资源对象未关闭导致的内存泄露

资源性对象(如Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。例如程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。类似的,Bitmap在不需要之后,应该调用recycle回收,再置为null。

五、属性动画导致的内存泄露

例如下面的代码,由于该属性动画为循环动画,如果在Activity销毁时,没有取消动画,那么虽然我们看不见动画在执行,实际上动画仍然一直播放下去,这个时候Button会被动画所持有,而Button又持有对应的Activity对象,那么就会造成Activity无法正常释放。

  1. public class MemoryActivity extends Activity{  
  2.     @Override  
  3.     protected void onCreate(Bundle savedInstanceState) {  
  4.         super.onCreate(savedInstanceState);  
  5.         setContentView(R.layout.activity_main);  
  6.         Button button = (Button) findViewById(R.id.btn_end);  
  7.         ObjectAnimator animator = ObjectAnimator.ofFloat(button, "", 0,180);  
  8.         animator.setDuration(2000);  
  9.         animator.setRepeatCount(-1);  
  10.         animator.start(); // 没有调用cancle()  
  11.     }  
  12. }  

六、Adapter未使用缓存的convertView导致的内存泄露

ListView提供每一个item所需要的view对象,初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的Item的View对象会被回收,然后被用来构造新出现的最下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。由此可以看出,如果我们不去使用 convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。正确的写法如下

  1. public View getView(int position, ViewconvertView, ViewGroup parent) {  
  2.         View view = null;  
  3.         if (convertView != null) { // 不应该直接new  
  4.                 view = convertView;   
  5.                 ...   
  6.         } else {   
  7.                 view = new Xxx(...);  
  8.                 ...   
  9.         }   
  10.         return view;   
  11. }  

七、Handler内部类内存泄露

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

  内存泄露检测工具MAT的使用请参考:http://jingyan.baidu.com/article/ae97a646b4eea8bbfc461d5a.html

  这里需要强调一点,有一些内存泄露通过Mat是查不出来的,比如native的代码,MAT对C/C++是无能为力的。

》框架模式之MVC与MVP 

MVC (Model-View-Controller):顾名思义,M是指逻辑模型,V是指视图模型,C则是控制器。一个逻辑模型我们可以才用多种视图模型,例如进度条显示,我们可以采用圆环、直线、容器式的试图显示,当然,一种视图模型也可以对于多种逻辑模型。

MVC的作用:

1、将M层和V层的实现代码分离,从而使同一个程序可以在同一View下,有不同的表现形式;

2、C层则是确保M层和V层的同步,一旦M层有改变,那么C层应该控制V去同步更新;

3、MVC是一个框架模式,它强制性的使应用程序的输入、处理和输出分开。

4、使用MVC应用程序被分成三个核心部件:模型、视图、控制器,它们各自处理自己的任务。

MVC的好处:

1、MVC的价值,在于各模块的松耦合、复用,提高开发效率,在Android中,三个模块分离想对明显,界面框架和ContentProvider模块得到充分复用;

2、从用户的角度出发,用户可以根据自己的需求,选择自己合适的浏览数据的方式;

3、从开发者的角度,MVC把应用程序的逻辑层与界面是完全分开的,故开发者一般不需要管UI,主要把精力放在逻辑层上。

举个例子,在Android中: 

1、视图层(View):一般在layout下,采用XML文件进行界面的描述,可以通过R.layout.***引入。当然,视图的显示绝不仅仅这些,还可以通过HTML5+CSS3+JS,利用Android自带的WebView来显示,另外,诸如PhoneGap的跨平台开发框架,也是针对不同平台的WebView做了扩展和封装,使WebView这个组件变成可访问设备本地API,所以本质上还是用WebView;

2、控制层(Controller):控制器起到不同层面间的组织作用,用于控制应用程序的流程,处理事件并作出响应。Android的控制层的重任通常落在了众多的Acitvity的肩上,这句话的另一层含义则是尽量不要在Acitivity中写一些复杂逻辑的代码,这部分复杂的业务逻辑,交由Model层去处理,另外,耗时的操作尽量在子线程中执行,使得主线程能快速处理UI事件和Broadcast消息,UI需要更新通过Handler、Messenger等方式通知主线程更新。众所周知,Android主线程阻塞时间不能超过5秒,否则将会发生ANR(Application Not Responding)异常。

3、模型层(Model):对数据库的操作、网络请求、格式转换、文件下载、大数据处理等的耗时操作都在Model里面处理。

  其实,MVC并没有严格的定义,但是Android中,MVC三个层分离明显,界面框架和ContentProvider模块得到充分复用,开发者并不需要在这方面话太多的心思去思考。

  MVP(Model-View-Presenter):和MVC类似,Presenter就是Model和View交互的中间纽带,负责处理与用户交互的逻辑。

android-MVP模式:

View不直接与Model交互,而是通过与Presenter交互来与Model间接交互。 
Presenter与View的交互是通过接口来进行的。 
通常View与Presenter是一对一的,但复杂的View可能绑定多个Presenter来处理逻辑。

android-MVC模式:

View可以与Model直接交互。 
Controller是基于行为的,并且可以被多个View共享,并且决定显示哪个View。