内存泄漏以及各种不同的优化方法

来源:互联网 发布:数控车床编程自学视频 编辑:程序博客网 时间:2024/06/05 14:36

搞开发的人都知道,内存泄漏与内存溢出一直都是一个经久不衰的话题,而且是每一个开发人员必须要面对而且要去解决的问题,那么今天我们就一起来探讨一些常发性的内存泄漏问题,以及我们应该怎样做才能更好的解决这些内存泄漏的问题.
一、首先,我们需要了解一下相关概念
①内存泄漏:memory leak,是指程序在申请内存后,无法释放已申请的内存空间。
②内存溢出:out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。
☛内存泄漏最终会导致内存溢出的产生。☚
二、相关的内存泄漏
◆以发生方式分类的话,内存泄漏可以分为:常发性内存泄漏、偶发性内存泄漏、一次性内存泄漏、隐式内存泄漏。
◆按照不同情况下发生的内存泄漏种类来说的话,有以下几种具体的情况:
①集合类泄漏:集合类如果仅仅有添加元素的方法,而没有响应的删除机制,导致内存被占用就会产生内存泄漏。
错误的案例:

private void collection() {    ArrayList<Object> list = new ArrayList();    Object o;    for (int i = 1; i < 100; i++) {        o = new Object();        list.add(o);    }    o = null;//释放引用}

分析:我们循环申请Object对象,并将所申请的对象放入一个 Arraylist 中,如果我们仅仅释放引用本身,那么 Arraylist 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Arraylist 后,还必须从 Arraylist 中删除,最简单的方法就是将 Arraylist 对象设置为 null。
②正确的代码

private void collection() {    ArrayList<Object> list = new ArrayList();    Object o;    for (int i = 1; i < 100; i++) {        o = new Object();        list.add(o);    }    o = null;    list = null;//把引用的对象从ArrayList当中删除}

②单例造成的泄漏:由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。
错误的案例:

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

错误分析:
1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,
所以这将没有任何问题。

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

正确的代码:

public class AppManager {
private static AppManager instance;
private Context context;

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

③非静态内部类创建静态实例造成的内存泄漏:非静态内部类持有了静态实例,因为静态实例的声明周期是与整个应用的生命周期相对应的,所以当非静态内部类对应的Activity关闭的时候,因为非静态内部类还持有静态实例而导致内存泄漏。
错误案例:

public class MainActivity extends Activity {//静态实例private static TestResource mManager = null;@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    if(mManager == null){        mManager = new TestResource();//持有静态实例    }}//非静态内部类class TestResource {//...}}

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

将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,
请按照上面推荐的使用Application 的 Context。

解决方法:将内部类抽取出来封装成一个单例。

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

错误案例:

public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    Runnable run1 = new MyRunnable();    Runnable run2 = new Runnable() {        @Override        public void run() {        注意:如果在这个位置进行一个一分钟的耗时操作,那么MainActivity在一分钟内是无法退出的,所以,不能胡乱使用匿名内部类进行这样的操作。        }    };}private class MyRunnable implements Runnable {    @Override    public void run() {    }}}

错误分析:run1和run2的区别是,run2使用了匿名内部类。我们来看看运行时这两个引用的内存:
可以看到,run1没什么特别的。
但run2这个匿名类的实现对象里面多了一个引用:
this$0这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被run2持有,
✔☛如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。

⑤Handler使用时造成的内存泄漏:Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。

错误案例:

public class MainActivity extends Activity {private final Handler mLeakyHandler = new Handler() {    @Override    public void handleMessage(Message msg) {        // ...    }};@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    mLeakyHandler.postDelayed(new Runnable() {        @Override        public void run() {        }    }, 1000 * 60 * 10);    finish();}

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

正确的代码:将Handler改成静态的即可

private static class MyHandler extends Handler {private final WeakReference<MainActivity> mActivity;public MyHandler(MainActivity activity) {  mActivity = new WeakReference<MainActivity>(activity);}

好理解的就不举例子了
⑥尽量避免使用static成员变量:如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。
⑦资源未关闭造成的内存泄漏:应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。比如:BraodcastReceiver(广播)、ContentObserver(内容观察者)、File(文件)、Cursor(游标)、Stream(流)、Bitmap(图)
⑧一些不良代码造成的内存压力:有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。
例如:
1、Bitmap 没调用 recycle()方法,对于 Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。
2、构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用 ViewHolder。
⑨池(Pool)的使用:对象池和线程池
☛对象池:格式Message message = Message.obtain();
将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象所造成的开销。
☛线程池:线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
⑩减少不必要的全局变量:尽量避免static成员变量引用资源耗费过多的实例,比如Context。
因为Context的引用超过它本身的生命周期,会导致Context泄漏。所以尽量使用Application这种Context类型。 你可以通过调用Context.getApplicationContext()或 Activity.getApplication()轻松得到Application对象。
⑪避免内部Getters/Setters的使用:在Android中,虚方法调用的代价比直接字段访问高昂许多。通常根据面向对象语言的实践,在公共接口中使用Getters和Setters是有道理的,但在一个字段经常被访问的类中宜采用直接访问。
⑫避免使用浮点数:通常的经验是,在Android设备中,浮点数会比整型慢两倍。
⑬避免使用枚举:枚举变量非常方便,但不幸的是它会牺牲执行的速度和并大幅增加文件体积。
⑭能使用实体类尽量不去使用接口:
假设你有一个HashMap对象,你可以将它声明为HashMap或者Map:

Map map1 = new HashMap();
HashMap map2 = new HashMap();
哪个更好呢?
按照传统的观点Map会更好些,因为这样你可以改变他的具体实现类,只要这个类继承自Map接口。传统的观点对于传统的程序是正确的,但是它并不适合嵌入式系统。调用一个接口的引用会比调用实体类的引用多花费一倍的时间。如果HashMap完全适合你的程序,那么使用Map就没有什么价值。如果有些地方你不能确定,先避免使用Map,剩下的交给IDE提供的重构功能好了。(当然公共API是一个例外:一个好的API常常会牺牲一些性能)
⑮for循环使用时要注意的几个点:
a.访问成员变量比访问本地变量慢得多
例:for(int i =0; i < this.mCount; i++) {}
b.不要在for的第二个条件当中调用任何方法
例:for(int i =0; i < this.getCount(); i++) {}
c. a 和 b中的错误最好改成:
int count = this.mCount; / int count = this.getCount();
for(int i =0; i < count; i++) {}
d.for-each语法
在java1.5中引入的for-each语法。编译器会将对数组的引用和数组的长度保存到本地变量中,这对访问数组元素非常好。 但是编译器还会在每次循环中产生一个额外的对本地变量的存储操作(如下面例子中的变量a),这样会比普通循环多出4个字节,速度要稍微慢一些:

for (Foo a : mArray) {
sum += a.mSplat;
}

⑯对于内存,UI资源的优化也是很重要的,所以对布局进行优化也是必不可少的内存优化操作:
a)ViewStub 是一个不可见的,大小为0的View,最佳用途就是实现View的延迟加载,避免资源浪费,在需要的时候才加载View。
也可以减少视图层级。
b)布局重用 可以通过这个标签直接加载外部的xml到当前结构中,是复用UI资源的常用标签
c)减少视图层级

⑰WebView的优化
a)WebView缓存
开启WebView的缓存功能可以减少对服务器资源的请求,一般使用默认缓存策略就可以了。
//设置 缓存模式
webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
// 开启 DOM storage API 功能
webView.getSettings().setDomStorageEnabled(true);

b)资源文件本地存储
资源等文件(不需要更新)本地存储,在需要的时候直接从本地获取。哪些资源需要我们去存储在本地呢,当然是一些不会被更新的资源,例如图片文件,js文件,css文件,替换的方法也很简单,重写WebView的方法即可。

{  try {  if (url.endsWith("icon.png")) {      InputStream is = appRm.getInputStream(R.drawable.icon);      WebResourceResponse response = new WebResourceResponse("image/png",        "utf-8", is);      return response;  } else if (url.endsWith("jquery.min.js")) {      InputStream is = appRm.getInputStream(R.raw.jquery_min_js);      WebResourceResponse response = new WebResourceResponse("text/javascript",        "utf-8", is);      return response;  }  } catch (IOException e) {         e.printStackTrace();  }  return super.shouldInterceptRequest(view, url);}  }

c)先加载网页后加载图片
首先阻塞网络图片的加载
mContentWV.getSettings().setBlockNetworkImage(true);
然后在网页加载完毕时加载图片

  @Override  public void onPageFinished(WebView view, String url) {            super.onPageFinished(view, url);            hideLoading();            if (!isError) {            view.setVisibility(View.VISIBLE);                mContentWV.getSettings().setBlockNetworkImage(false);                if (!mContentWV.getSettings().getLoadsImagesAutomatically()) {                    mContentWV.getSettings().setLoadsImagesAutomatically(true);                }            }        }

d)加快HTML网页装载完成的速度
默 认情况html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css 或js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加 载。
故在WebView初始化时设置如下代码:

public void int () {if(Build.VERSION.SDK_INT >= 19) {    webView.getSettings().setLoadsImagesAutomatically(true);} else {    webView.getSettings().setLoadsImagesAutomatically(false);}}

同时在WebView的WebViewClient实例中的onPageFinished()方法添加如下代码:

 @Override public void onPageFinished(WebView view, String url) {       if(!webView.getSettings().getLoadsImagesAutomatically()) {webView.getSettings().setLoadsImagesAutomatically(true);}}

e)自定义出错界面
f)是否已滚动到页面底部
g)远程网页需访问本地资源
h)ViewPager里非首屏WebView点击事件不响应
i)WebView硬件加速导致页面渲染闪烁
j)避免addJavaScriptInterface带来的安全问题
k)关于web页面如何通知WebView

参考的网址:
内存泄漏和内存溢出的介绍
http://blog.csdn.net/buutterfly/article/details/6617375