性能优化之内存泄露(Memory Leak)&OOM&ANR分析

1 内存泄露

1.1 什么是Java中的内存泄漏

当一个对象已经不需要使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用,从而导致了对象不能被GC回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。
  在Java 中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:①这些对象是可达的,即在有向图中存在通路可以与gc roots相连(直接或间接地引用到gc roots);②这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC回收,它却占用内存。结合GC垃圾回收机制:性能优化之JVM/DVM和垃圾回收机制

1.2 Android 中内存泄漏的原因

如果长生命周期的对象持有短生命周期对象的引用,造成不再会被使用的对象的内存不能被回收,就很可能会出现内存泄露,这就是Java中内存泄漏的发生的原因。

1.3 图例解析

在C++中,内存泄漏的范围更大一些,有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。
  通过分析:对于C++,程序员需要自己管理边和顶点;而**对于Java,程序员只需要管理边,不需要管理顶点的释放。**通过这种方式,Java提高了编程的效率。
这里写图片描述

1.4 内存泄漏带来的影响

(1)应用卡顿:泄漏的内存影响了GC的内存分配,过多的内存泄漏会影响应用的执行效率。
(2)内存溢出(OOM):过多的内存泄漏,最终会导致 Dalvik分配的内存,出现OOM。

1.5 一个不太注意到的内存泄漏案例

(1)长生命周期的对象持有短生命周期对象的引用的内存泄漏例子

public class Simple {
	Object object;
	public void method() {
		object = new Object();
		//...其他代码
	}
}

这里的object实例,我们期望它只作用于method()方法中,且其他对象不会再用到它。但是,当method()方法执行完成后,object对象所分配的内存不会马上被认为是可以被释放的对象,只有在Simple类创建的对象被释放后才会被释放,严格的说,这就是一种内存泄露。
(2)解决方法一:将object作为method1()方法中的局部变量(局部变量是属于方法中的变量,生命周期随方法而结束。 )

public class Simple {
	public void method() {
		Object object = new Object();
		//...其他代码
	}
}

(3)解决方法二:给对象赋予了空值null,之后不再调用

public class Simple {
	Object object;
	public void method1(){
		object = new Object();
		//...其他代码
		object = null;
	}
}

1.6 小结

在内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用),这是所有语言都有可能会出现的内存泄漏方式。编程时如果不小心,我们很容易发生这种情况,如果不太严重,可能就只是短暂的内存泄露。

2 引起内存泄漏的情景分析

2.1 静态变量引起的内存泄漏

在java中静态变量的生命周期是在类加载时开始,类卸载时结束。换句话说,在android中其生命周期是在进程启动时开始,进程死亡时结束。所以在程序的运行期间,如果进程没有被杀死,静态变量就会一直存在,不会被回收掉。如果静态变量强引用了某个Activity中变量,那么这个Activity就同样也不会被释放,即便是该Activity执行了onDestroy(不要将执行onDestroy和被回收划等号)。
  这类问题的解决方案为:
  ①寻找与该静态变量生命周期差不多的替代对象
  ②若找不到,将强引用方式改成弱引用。比较典型的例子如下:

2.1.1 单例引起的Context内存泄漏
public class Singleton {
    private Context context;
    private volatile static Singleton instance = null;
    private Singleton(Context context) {
	    this.context = context;
    }
    public static Singleton getInstance(Context context) {
        // 两层判空,第一层是为了避免不必要的同步
        // 第二层是为了在null的情况下创建实例
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(context);
                }
            }
        }
        return instance;
    }
}

(1)分析:当调用getInstance时,如果传入的context是Activity的context。只要这个单例没有被释放,这个Activity也不会被释放。
(2)解决方案:传入Application的context,因为Application的context的生命周期比Activity长,可以理解为Application的context与单例的生命周期一样长,传入它是最合适的。

private SingleTon(Context context) { 
    this.context = context.getApplicationContext(); 
}

2.2 非静态内部类引起的内存泄漏

非静态内部类会持有外部类的引用,如果这个非静态的内部类的生命周期比它的外部类的生命周期长,那么当销毁外部类的时候,它无法被回收,就会造成内存泄漏(静态内部类却不会)。这类问题的解决方案为:
(1)将内部类变成静态内部类
(2)如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用
(3)在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务

2.2.1 外部类的静态变量持有非静态内部类
public class MainActivity extends AppCompatActivity { 
    private static Test test; 

    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 
        if (test == null) { 
            test = new Test(); 
        } 
    } 

    private class Test { 
	    //非静态内部类Test默认持有外部类MainActivity的引用
    } 
}

(1)分析:这个其实和单例的原理是一样的,由于静态对象test的生命周期和整个应用的生命周期一致,而非静态内部类Test持有外部类MainActivity的引用,导致 MainActivity退出的时候不能被回收,从而造成内存泄漏。
(2)解决方案:①把test改成非静态,这样test的生命周期和MainActivity是一样的了,就避免了内存泄漏。②把Test改成静态内部类,让test不持MainActivity的引用,不过一般没有这种操作。

2.3 关于Handler引起的内存泄漏

2.3.1 Handler或Runnable作为非静态内部类

Handler和runnable都有定时器的功能,当它们作为非静态内部类的时候,同样会持有外部类的引用,如果它们的内部有延迟操作,在延迟操作还没有发生的时候,销毁了外部类,那么外部类对象无法回收,从而造成内存泄漏。假设Activity的代码如下:

public class MainActivity extends AppCompatActivity {  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  

        new Handler().postDelayed(new Runnable() {  
            @Override  
            public void run() {  

            }  
        }, 10 * 1000);  
    }  
}

(1)分析:Handler和Runnable作为匿名内部类,都会持有MainActivity的引用,而它们内部有一个10秒钟的定时器,如果在打开MainActivity的10秒内关闭了MainActivity,那么由于Handler和Runnable的生命周期比MainActivity长,会导致 MainActivity无法被回收,从而造成内存泄漏。
(2)解决方案1:一般套路就是把 Handler 和 Runnable 定义为静态内部类,这样它们就不再持有MainActivity的引用了,从而避免了内存泄漏。

public class MainActivity extends AppCompatActivity {  
    private Handler handler;  
    private Runnable runnable;  

    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  

        handler = new TestHandler();  
        runnable = new TestRunnable();  
        handler.postDelayed(runnable, 10 * 1000);  
    }  

    private static class TestHandler extends Handler {  
    }  
    private static class TestRunnable implements Runnable {  
        @Override  
        public void run() {  
            Log.d(TAG, "run: ");  
        }  
    }  
    private static final String TAG = "MainActivity";  
}

(3)解决方案2:再在onDestory调用handler的removeCallbacks方法来移除Message,这样不但能避免内存泄漏,而且在退出Activity时取消了定时器,保证10秒以后也不会执行run方法。

@Override  
protected void onDestroy() {  
    super.onDestroy();  
    handler.removeCallbacks(runnable);  
}
2.3.2 Handler或Runnable持有Context对象
public class MainActivity extends AppCompatActivity { 
	private MyHandler handler;
    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 

        handler = new MyHandler(this); 
        handler.sendEmptyMessageDelayed(0, 10*1000)); 
    } 

    private static class MyHandler extends Handler {
	    Context context;
	    MyHandler(Context context) {
	        this.context = context;
	    }
	    @Override
	    public void handleMessage(Message msg) {
	        if (context != null) {
	            mImageView.setImageBitmap(mBitmap);
	        }
	    }
	}
}

(1)分析:由于在Handler中持有Context对象,而这个Context对象是通过TestHandler的构造方法传入的,它是一个MainActivity对象。也就是说,虽然MyHandler作为静态内部类不会持有外部类MainActivity的引用,但是我们在调用它的构造方法时传入了MainActivity的对象,从而handler对象持有了MainActivity的引用,如果用户在10秒内关闭了Activity,而这个handler10秒后执行,它持有Activity的引用,就导致该Activity无法被回收。
(2)解决方案:使用弱引用的方式来引用Context来避免内存泄漏

public class MainActivity extends AppCompatActivity { 
    private MyHandler handler;
   
    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 

        handler = new MyHandler(this); 
        handler.sendEmptyMessage(1);
    } 

    private class MyHandler extends Handler {
	    WeakReference<MainActivity> mActivity;
	    MyHandler(MainActivity activity) {
	        mActivity= new WeakReference<MainActivity>(activity);
	    }
	    
	    @Override
	    public void handleMessage(Message msg) {
		   final MainActivity activity = mActivity.get();
		   if (ActivityUtils.isActivityFinished(activity)) {
				return;
		   }
	        
		   mImageView.setImageBitmap(mBitmap);
	    }
	}
} 
2.3.3 关于Handler Leak的总结理解

(1)Handler的一个常用例子

Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case HyyConstants.REFRESH_LIST:
                    Toast.makeText(getApplicationContext(), "Refresh list", Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;
            }
        }
    };

(2)分析:在应用程序线程的MessageQueue中排队的Message对象还保留他们的目标Handler。如果Handler是一个内部类(注:无论是匿名还是非匿名,匿名是比较常见用法),它的外部类将被保留(至于为什么,请参考Java嵌套类相关说明)。为了避免泄漏外部类,声明一个Handler子类为静态内部类(注:这样就避免了Handler对象对外部类实例的自动引用),其内部持有一个对外部类对象的WeakReference。

(3)再分析一下这几个泄漏问题:
①排队中的Message对象对Handler的持有导致泄漏;
②Handler对象对外部类(如Activity或Service)实例的强引用持有。

(4)解决方案:
①针对第1个原因,在使用Handler的组件生命周期结束前清除掉MessageQueue中的发送给Handler的Message对象(例如在Activity或Service的onDestroy()中调用Handler的remove*方法)。
②针对第2个原因,Handler的实现类采用静态内部类的方式,避免对外部类的强引用,在其内部声明一个WeakReference引用到外部类的实例。

(5)关于Handler的remove()方法(可以参考源码或文档)
①removeCallbacks(Runnable r) ——清除r匹配上的Message。
②removeCallbacks(Runnable r, Object token) ——清除r匹配且匹配Message.obj的Message,token为空时,只匹配r。
③removeCallbacksAndMessages(Object token) ——清除token匹配上的Message。
④removeMessages(int what) ——按what来匹配
⑤removeMessages(int what, Object object) ——按what来匹配
⑥handler.removeCallbacksAndMessages(null);我——清除以该Handler为target的所有Message(包括Callback),

(6)最佳方案

public class MyActivity extendsActivity {
    private MyHandler mHandler;
    @Override
    protectedvoid onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler = newMyHandler(this);
    }
 
    @Override
    protected void onDestroy() {
        mHandler.removeCallbacksAndMessages(null);   // Remove all Runnable and Message.
        super.onDestroy();
    }
 
   private static class MyHandler extends Handler {
        private WeakReference<MyActivity> mActivity;    // WeakReference to the outer class's instance.
 
        public MyHandler(MyActivity activity) {
            mActivity= new WeakReference<MyActivity>(activity);
        }
 
        @Override
        public void handleMessage(Message msg) {
            final MyActivity activity = mActivity.get();
            if (ActivityUtils.isActivityFinished(activity)) {
                return;
            }
            
            if(outer != null) {
                // Do something with outer as your wish.
            }
        }
    }
}
 /**
     * 直接判断Activity是否已经销毁
     *
     * @param activity
     * @return true=Activity已经销毁,false=没有销毁
     */
    public static boolean isActivityFinished(Activity activity) {
        return activity == null || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) || activity.isFinishing();
    }

(7)参考链接
关于HandlerLeak的一点理解

2.3 资源未关闭引起的内存泄漏

当使用了BraodcastReceiver、Cursor、Bitmap、自定义属性attr等资源时,当不需要使用时,需要及时释放掉,若没有释放,则会引起内存泄漏

2.4 无限循环动画

在Activity中播放属性动画中的一类无限循环动画,没有在ondestory中停止动画,Activity会被动画持有而无法释放

2.5 容器使用时的内存泄露

Vector vector = new Vector(10); 
for (int i = 1; i < 100; i++) { 
    Object obj = new Object(); 
    vector.add(obj); 
    obj = null;  
}

在这个例子中,我们循环申请Object对象,并将所申请的对象放入Vector中,仅仅释放引用本身,那么Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。而此处的内存泄露可能是短暂的,因为在整个method()方法执行完成后,那些对象还是可以被回收(但我们也应该避免)。
  因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null。

//...对vector的操作
vector = null;

3 内存溢出(OOM)

3.1 OOM产生的原因

当应用的heap资源超过了Dalvik虚拟机分配的内存(64M/48M/24M)就会内存溢出。

3.2 什么情况导致OOM

  • 内存泄露多了就容易导致OOM。
  • 大图的处理。压缩图片,平时开发就要注意对象的频繁创建和回收。
  • 可以适当的检测:ActivityManager.getMemoryClass()可以用来查询当前应用的HeapSize阀值。可以通过命名adb shellgetProp | grep dalvik.vm.heapxxxlimit查看。

3.3 如何避免OOM

3.3.1 减小对象的内存占用

(1)使用更加轻量级的数据结构。考虑适当的情况下替代HashMap等传统数据结构而使用安卓专门为手机研发的数据结构类ArrayMap/SparseArray、SparseLongMap/SparseIntMap更加高效。HashMap.put(string,Object);Object o = map.get(string),会导致一些没必要的自动装箱和拆箱。
(2)减少Bitmap对象的内存占用。资源图片经过压缩。使用inSampleSize图片压缩比例进行图片压缩,可以避免大图加载造成OOM; decodeformat:图片的解码格式选择,ARGB_8888/RGB_565/ARGB_4444/ALPHA_8,还可以使用WebP。
(3)适当的避免在android中使用Enum枚举,替代使用普通的static常量。(一般还是提倡多用枚举—软件的架构设计方面;如果碰到这个枚举需要大量使用的时候就应该更加倾向于解决性能问题。)

3.3.2 内存对象的重复利用

(1)ListView/GridView源码可以看到重用的情况ConvertView的复用。RecyclerView中Recycler源码。
(2)Bitmap的复用:Listview等要显示大量图片,需要使用LRU缓存机制来复用图片。
(3)避免在onDraw方法里面执行对象的创建,要复用,避免内存抖动。
(4)字符串操作大量数据使用合适的类:单线程在字符串缓冲区下操作大量数据用StringBuilder;多线程在字符串缓冲区下操作大量数据用StringBuffer;操作少量的数据用String;。

3.3.3 避免对象的内存泄露

参考-- 2 引起内存泄漏的情景分析

3.3.4 使用一些内存的优化策略

4 ANR定位和修正

4.1 定位

(1)利用loop()中打印的日志,检测应用中的UI卡顿;Android UI性能优化 检测应用中的UI卡顿
(2)通过Bugly统计的ANR信息跟踪;
(3)可以通过查看/data/anr/traces.txt查看ANR信息。

4.2 根本原因

主线程卡顿,导致应用在5秒时间未响应用户的输入事件。

4.3 ANR错误出现的场景

(1)Activity的onCreate和onResume回调中尽量避免耗时的操作
(2)主线程当中执行IO/网络操作,容易阻塞;执行了耗时的计算----自定义控件的时候onDraw方法里面经常这么做(在onDraw里面创建对象容易导致内存抖动—绘制动作会大量不断调用,产生大量垃圾对象导致GC很频繁就造成了内存抖动)。
(3)BroadCastReceiver没有在10秒内完成处理,onReceived代码中要尽量减少耗时的操作,建议使用IntentService处理。
(4)Service执行了耗时的操作,因为service也是在主线程当中执行的,所以耗时操作应该在service里面开启子线程来做。
(5)使用Thread或者HandlerThread时,使用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)或者java.lang.Thread.setPriority (int priority)设置优先级为后台优先级,这样可以让其他的多线程并发消耗CPU的时间会减少,有利于主线程的处理。

6 使用场景

6.1 我们所讨论内存泄露,主要是讨论“堆内存”,他存放的就是:引用指向的对象实体。

有时候确实会有一种情况:当需要的时候可以访问,当不需要的时候可以被回收也可以被暂时保存以备重复使用。
  例如:ListView或者GridView、RecyclerView加载大量数据或者图片的时候,图片非常占用内存,一定要管理好内存,不然很容易内存溢出。滑出去的图片就回收,节省内存。看ListView的源码----回收对象,还会重用ConvertView。如果用户反复滑动或者下面还有同样的图片,就会造成多次重复IO(很耗时),那么需要缓存—平衡好内存大小和IO,算法和一些特殊的java类。

算法:lrucache(最近最少使用先回收)
特殊的java类利于回收,StrongReference,SoftReference,WeakReference,PhatomReference

这里写图片描述
  软引用比LRU算法更加任性,回收量是比较大的,你无法控制回收哪些对象。比如使用场景:默认头像、默认图标、ListView或者GridView、REcyclerView要使用内存缓存+外部缓存(SD卡)
  综述:开发时,为了防止内存溢出,处理一些比较占用内存大并且生命周期长的对象的时候,可以尽量使用软引用(SoftReference)和弱引用(WeakReference)。

7 参考链接

Android 中内存泄漏的原因分析及解决方案

全方位带你彻底搞懂Android内存泄露 | 案例分析

手把手教你在Android Studio 3.0上分析内存泄漏

Android 内存泄漏总结

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值