Android 内存泄露

在很久很久之前,看过一篇关于内存泄露的文章,里面列举了比较全的应该注意的问题,后来找不到原文地址,今天翻了微博,找到了该文章,为了方便日后自己查看,将注意的问题提取出来。在Android开发中,我们的编码习惯可能会让我们编写出一些容易导致内存泄露的代码。所以我们应该要养成一个良好的编码习惯。

单例

平时,我们可能会这样写单例

public class Singleton{
    private static Singleton instance;
    private Context mContext;
    private Singleton(Context mContext){
        this.mContext = mContext;
    }
    public static Singleton getInstance(Context context){
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(context);
                }
            }
        }
        return instance;
    }
}


然后这样写会有隐患,原因是如果我们再Activity中或者其他地方使用Singleton.getInstance()时,我们会顺手写this或者mContext(这个变量也是指向this)作为参数传进去。我们的Singleton是单例,意味着被初始化后就应该一直存在内存中,以方便我们以后调用的时候不会再次创建Singleton对象,但是Singleton中的mContext变量一直都会持有Activity中的Context,导致Activity即使执行了onDestroy方法,也不能够将自己销毁,但是ApplicationContext就显然有所不同了,它一直伴随着我们的应用存在,所以就不用担心前面所说的导致Activity无法销毁的问题了。于是就诞生了正确的写法。

public class Singleton{
    private static Singleton instance;
    private Context mContext;
    private Singleton(Context mContext){
        this.mContext = mContext;
    }
    public static Singleton getInstance(Context context){
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(context.getApplicationContext());
                }
            }
        }
        return instance;
    }
}


匿名内部类

在Android开发中,很多地方会用到匿名内部类,比如事件的监听,Handler的消息处理等等,而一旦使用错误,也会导致内存泄露。

public class MainActivity extends Activity{   
    private Button btn;
    private Handler handler = new Handler(){
       @override
       public void handlerMessage(Message msg){   
       }
    };
    @override
    public void onCreate(Bundle bundle){
       super.onCreate(bundle);
       setContextView(R.layout.activity_main);
       btn=(Button)findViewById(R.id.btn);
       btn.setOnClickListner(View.OnClickListener(){
            @override
            public void onClick(View view){
                Message message = Message.obtain();
                handler.sendMessage(message);
            }
       });

    }
}


当我们执行了MainActivity的finish方法,被延迟的消息会在被处理之前存在于主线程消息队列中,而这个消息中又包含了Handler的引用,而Handler是一个匿名内部类的实例,其持有外面的MainActivity的引用,所以这导致了MainActivity无法回收,进而导致MainActivity持有的很多资源都无法回收,所以产生了内存泄露。然而一个静态的匿名内部类实例是不会持有外部类的引用的。所以正确的写法应该是这样的。

public class MainActivity extends Activity{
    private Button btn;
    private static class MyHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;
        public MyHandler(MainActivity activity) {
            mActivity = new WeakReference<MainActivity>(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mActivity.get();
            if (activity != null) {
                 // ...
            }
        }
    }
    private final MyHandler handler = new MyHandler(this);
    @override
    public void onCreate(Bundle bundle){
       super.onCreate(bundle);
       setContextView(R.layout.activity_foo_layout);
       btn=(Button)findViewById(R.id.btn);
       btn.setOnClickListner(View.OnClickListener(){
            @override
            public void onClick(View view){
                Message message = Message.obtain();
                handler.sendMessage(message);
            }
       });
    }
}


很多时候我们甚至会使用handler发送一个匿名Runnable对象,同样的也会导致内存泄露的问题。因此Runnable对象应该也要使用静态的匿名内部类。

private static class MyRunnable implements Runnable{
    private WeakReference<TextView> textViewWeakReference;
    public MyRunnable(TextView textView){
        textViewWeakReference = new WeakReference<TextView>(textView);
    }
    @override
    public void run(){
         final TextView textView = textViewWeakReference.get();
         if(textView != null){
              textView.setText("OK");
         }
    };
}


使用的时候这样用就可以了

handler.postDelayed(new MyRunnable(textView),1000 * 60 * 10);


Handler

在使用了Handler之后,记得在onDestroy里面调用

handler.removeCallbacksAndMessages(object token);


移除相关消息。

我们可以使用

handler.removeCallbacksAndMessages(null);


当参数为null时,可以清除掉所有跟次handler相关的Runnable和Message,我们在onDestroy中调用次方法也就不会发生内存泄漏了。

对以上几点做一个总结 
- 不要让生命周期长于Activity的对象持有到Activity的引用 
- 尽量使用Application的Context而不是Activity的Context 
- 尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用。如果使用静态内部类,将外部实例引用作为弱引用持有。 
- 垃圾回收不能解决内存泄露,了解Android中垃圾回收机制

Context

注意Context与ApplicationContext的区别 
- View.getContext,返回当前View对象的Context对象,通常是当前正在展示的Activity对象。 
- Activity.getApplicationContext,获取当前Activity所在的(应用)进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。 
- ContextWrapper.getBaseContext():用来获取一个ContextWrapper进行装饰之前的Context,可以使用这个方法,这个方法在实际开发中使用并不多,也不建议使用。 
- Activity.this 返回当前的Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以。

这里写图片描述 
大家注意看到有一些NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢? 
- 数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。 
- 数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。 
- 数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视) 
注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。

好了,这里我们看下表格,重点看Activity和Application,可以看到,和UI相关的方法基本都不建议或者不可使用Application,并且,前三个操作基本不可能在Application中出现。实际上,只要把握住一点,凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。 
关于Context更详细的内容见Android Context 上下文 你必须知道的一切

=====================================

android内存 相关文章:

Android性能优化-内存泄漏(上)

为什么要做性能优化?

  1. 手机性能越来越好,不用纠结这些细微的性能?

    • Android每一个应用都是运行的独立的Dalivk虚拟机,根据不同的手机分配的可用内存可能只有(32M、64M等),所谓的4GB、6GB运行内存其实对于我们的应用不是可以任意索取

    • 优秀的算法与效率低下的算法之间的运行效率要远远超过计算机硬件的的发展,虽然手机单核、双核到4核、8核的发展,但性能优化任然不可忽略

  2. 手机应用一般使用的周期比较短,用完就关了。不像服务器应用要长年累月运行,似乎影响不大?

    • 现在一般的用户都不会重启手机,可能一个月都不会重启。像微信这样的APP,每天都在使用。如果一旦发生内存泄漏,那么可能一点一点的累积,程序就会出现OOM。
  3. 等应用出现卡顿、发烫等,再来关注性能优化?

    • 似乎是没错的。现在一般我们也都是等出现问题了再来找原因。但是学好性能优化的目的不仅仅如此,我们在编码阶段就应该从源头来杜绝一些坑,这样的成本比后期再来寻找原因要少得多

    所以为了我们的应用的健壮性、有良好的用户体验。性能优化技术,需要我们用心去研究和应用。

什么是内存泄漏?

JVM内存管理


Java采用GC进行内存管理。深入的JVM内存管理知识,推荐《深入理解Java虚拟机》。关于内存泄漏我们要知道,JVM内存分配的几种策略。

  1. 静态的

    静态的存储区,内存在程序编译的时候就已经分配好了,这块内存在程序整个运行期间都一直存在,它主要存放静态数据、全局的static数据和一些常量。

  2. 栈式的

    在执行方法时,方法一些内部变量的存储都可以放在栈上面创建,方法执行结束的时候这些存储单元就会自动被注释掉。栈 内存包括分配的运算速度很快,因为内在在处理器里面。当然容量有限,并且栈式一块连续的内存区域,大小是由操作系统决定的,他先进后 出,进出完成不会产生碎片,运行效率高且稳定

  3. 堆式的

    也叫动态内存 。我们通常使用new 来申请分配一个内存。这里也是我们讨论内存泄漏优化的关键存储区。GC会根据内存的使用情况,对堆内存里的垃圾内存进行回收。堆内存是一块不连续的内存区域,如果频繁地new/remove会造成大量的内存碎片,GC频繁的回收,导致内存抖动,这也会消耗我们应用的性能

我们知道可以调用 System.gc();进行内存回收,但是GC不一定会执行。面对GC的机制,我们是否无能为力?其实我们可以通过声明一些引用标记来让GC更好对内存进行回收。

类型 回收时机 生命周期
StrongReference (强引用) 任何时候GC是不能回收他的,哪怕内存不足时,系统会直接抛出异常OutOfMemoryError,也不会去回收 进程终止
SoftReference (软引用) 当内存足够时不会回收这种引用类型的对象,只有当内存不够用时才会回收 内存不足,进行GC的时候
WeakReference (弱引用) GC一运行就会把给回收了 GC后终止
PhantomReference (虚引用) 如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收 任何时候都有可能

开发时,为了防止内存溢出,处理一些比较占用内存并且生命周期长的对象时,可以尽量使用软引用和弱引用。

Tip

成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体),因为他们属于类,类对象最终还是要被new出来的

局部变量的基本数据类型和引用存在栈中,应用的对象实体存储在堆中。因为它们属于方法当中的变量,生命周期会随着方法一起结束

内存泄漏的定义

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

内存泄漏与内存溢出的区别

  • 内存泄漏(Memory Leak)
    进程中某些对象已经没有使用的价值了,但是他们却还可以直接或间接地被引用到GC Root导致无法回收。当内存泄漏过多的时候,再加上应用本身占用的内存,日积月累最终就会导致内存溢出OOM

  • 内存溢出(OOM)
    当 应用的heap资源超过了Dalvik虚拟机分配的内存就会内存溢出

内存泄漏带来的影响

  • 应用卡顿
    泄漏的内存影响了GC的内存分配,过多的内存泄漏会影响应用的执行效率

  • 应用异常(OOM)
    过多的内存泄漏,最终会导致 Dalvik分配的内存,出现OOM

Android开发常见的内存泄漏

单例造成的内存泄漏

  1. 错误示例
  2. 当调用getInstance时,如果传入的context是Activity的context。只要这个单例没有被释放,那么这个
    Activity也不会被释放一直到进程退出才会释放。

    public class CommUtil {
        private static CommUtil instance;
        private Context context;
        private CommUtil(Context context){
        this.context = context;
        }
    
        public static CommUtil getInstance(Context mcontext){
        if(instance == null){
            instance = new CommUtil(mcontext);
        }
        return instance;
        }
  3. 解决方案

    能使用Application的Context就不要使用Activity的Content,Application的生命周期伴随着整个进程的周期

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

  4. 错误示例

 private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mManager == null){
            mManager = new TestResource();
        }

    }
    class TestResource {

    }
  1. 解决方案

将非静态内部类修改为静态内部类。(静态内部类不会隐式持有外部类)

Handler造成的内存泄漏

  1. 错误示例

mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。

 private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {

        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
  1. 解决方案

创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息

   private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

线程造成的内存泄漏

  1. 错误示例

异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(10000);
                return null;
            }
        }.execute();


        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(10000);
            }
        }).start();
  1. 解决方案

使用 静态内部类,避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任务AsyncTask::cancel(),避免任务在后台执行浪费资源

   static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        private WeakReference<Context> weakReference;

        public MyAsyncTask(Context context) {
            weakReference = new WeakReference<>(context);
        }

        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            MainActivity activity = (MainActivity) weakReference.get();
            if (activity != null) {
                //...
            }
        }
    }
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }
//——————
    new Thread(new MyRunnable()).start();
    new MyAsyncTask(this).execute();

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

  1. 错误示例

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

  1. 解决方案

在Activity销毁时及时关闭或者注销

使用了静态的Activity和View

  1. 错误示例
static view; 

    void setStaticView() { 
      view = findViewById(R.id.sv_button); 
    } 

    View svButton = findViewById(R.id.sv_button); 
    svButton.setOnClickListener(new View.OnClickListener() { 
      @Override public void onClick(View v) { 
        setStaticView(); 
        nextActivity(); 
      } 
    }); 


    static Activity activity; 

    void setStaticActivity() { 
      activity = this; 
    } 

    View saButton = findViewById(R.id.sa_button); 
    saButton.setOnClickListener(new View.OnClickListener() { 
      @Override public void onClick(View v) { 
        setStaticActivity(); 
        nextActivity(); 
      } 
    });
  1. 解决方案

    应该及时将静态的应用 置为null,而且一般不建议将View及Activity设置为静态

注册了系统的服务,但onDestory未注销

  1. 错误示例

    SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
    Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
    sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
  2. 解决方案

    //不需要用的时候记得移除监听
        sensorManager.unregisterListener(listener);

不需要用的监听未移除会发生内存泄露

  1. 错误示例

    //add监听,放到集合里面
        tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
            @Override
            public void onWindowFocusChanged(boolean b) {
                //监听view的加载,view加载出来的时候,计算他的宽高等。
            }
        });
  2. 解决方案

    //计算完后,一定要移除这个监听
                tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
Tip

tv.setOnClickListener();//监听执行完回收对象,不用考虑内存泄漏
tv.getViewTreeObserver().addOnWindowFocusChangeListene,add监听,放到集合里面,需要考虑内存泄漏

下一篇将介绍如何进行内存泄漏的分析

Android性能优化-内存泄漏(下)

如何进行内存泄漏的分析

使用Android Studio Monitors

AndroidMonitors是Android Studio自带的功能,我们可以通过里面的Memory模块来进行内存泄漏的分析,平时开发我们也可以通过该模块来观察内存的抖动情况。


这里我们首先知道,标注1是进行GC的操作,标注2是进行Dump操作,也就是可以生成我们瞬时的堆内存快照,我们主要也是通过分析堆内存的快照来进行内存泄漏分析。
一般我们先进行几次gc操作,待内存平稳后,执行dump操作。会生成一个phrof的内存快照


此时我们可以看到几个面板:

  1. ClassName:堆内存中存在的类
  2. Instaance:类存在的实例
  3. ReferenceTree:持有该类的引用

几个属性的含义:

  1. Depth:引用的层级
  2. Shallow Size:对象的大小(Byte)
  3. Dominating Size:释放该对象能节省的堆内存(Byte)

将快照转换为Mat能够导入的格式
在as的captures中可以右键选择export to standard .hprof 将快照转换为mat能够带入的文件格式


使用MAT

MAT是一款功能更强大的内存泄漏分析工具,在实际的内存分析中,我们可以结合Monitors进行内存泄漏分析。

下载地址http://www.eclipse.org/mat/


导入快照后,我们可以通过Histogram查看内存快照


在Histogram中,我们可以通过筛选过滤出我们项目中的包和类,这个操作实际中很有用。


选中具体的对象后,右键list objects--with incoming references可以查看对改对象持有的应用


我们可以看到,这个时候引用还是非常的,我们需要过滤一些无用的软引用之类的。通过右键-megre shortest path to GC roots-exclude all phantom/sofe/weak etc.refrences进行过滤,这个时候基本就能查出我们自己写的代码的引用


另外Mat还支持2个快照进行比对,这个功能也是非常有用的。
我们可以在Navigation History中选择 Histogram,然后右键选择Add to compare basket加入比较选项,将2个快照的Histogram加入后在compare basket栏中点击红色感叹号就可以执行快照的比对。


使用leakcanary

Square开源了一个内存泄露自动探测神器——LeakCanary,它是一个Android和Java的内存泄露检测库,可以大幅度减少了开发中遇到的OOM问题。

github https://github.com/square/leakcanary

通过官方的文档介绍,我们可以轻松在项目集成

加入依赖:

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }

Application 配置:

public class ExampleApplication extends Application {

  ......
  //在自己的Application中添加如下代码
public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (ExampleApplication) context
            .getApplicationContext();
    return application.refWatcher;
}

  //在自己的Application中添加如下代码
private RefWatcher refWatcher;

@Override
public void onCreate() {
    super.onCreate();
    ......
        //在自己的Application中添加如下代码
    refWatcher = LeakCanary.install(this);
    ......
}

.....
}

使用 RefWatcher 监控那些本该被回收的对象:

public abstract class BaseFragment extends Fragment {

  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}

最后如果有内存泄漏,会接收到相应的推送。


这样我们就能在编码的阶段,尽量的避免出现内存泄漏的情况。

如何对自己的项目进行内存泄漏分析

上面说了这么多,怎么来对我们自己的项目进行内存泄漏的分析呢?

一般我们都是在不知道项目中那里有存在内存泄漏的情况下,怎么来查找出那个地方出现了内存泄漏?

这里我们主要检查Activity及Fragment的内存泄漏情况。

使用Memory Usage查看Activity及Fragment的内存泄漏情况,首先先运行自己项目到MainActivity,观察 Menory Usage。


待gc内存稳定后,我们可以执行一些操作,如进入其他的Activity执行其他操作,然后 检测内存的抖动情况及gc稳定后,内存与初始内存的对比。

这里我使用开启不保留活动来模拟MainActivity的异常退出及恢复。继续看Menory Usage。



这个时候,我只有在MainActivity出现过, 理论上应当只有一个MainActivity的实例,这个地方就是一个值得怀疑的内存泄漏的点。这个时候我们就可以通过Mioniter和Mat进行内存分析


这个时候我们可以看到引用的的可怀疑对象,接着我们就进入源码分析。


果然这里有一个单例持有了MainActivity的使用。

分析内存泄漏是一个体力活,我们大概在项目中主要要记住。

  1. 使用leakcanary 在编码阶段进行检测

  2. 结合内存抖动及Memory Usage 检查Activity及Fragment的的泄漏情况

  3. 使用Monitor及Mat进行引用持有分析找出怀疑的对象

  4. 分析源代码,找到元凶




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值