概要:
内存泄漏在现在许多安卓程序员编码过程中是必然会遇到的问题,而它往往在程序员不经意间发生。其实这是跟一个程序员的编码习惯是息息相关的,优秀的程序员在开始编码的过程中就会不断的去优化自己的代码,时刻注意自己写的代码会不会发送内存泄漏,而一些普通的程序员仅仅是完成上级交给的任务,对自己的代码没有更高的要求。什么是内存泄漏?
Android的Java语言有个最大的优点,是托管内存环境,对象在创建或消除时不用特别小心。这点尽管不错,但也有些潜在的问题不易被发现。划分到Android运行时的内存堆,是根据声明类型和利于垃圾清理操作的角度来分配的,每一区域都有其预设的内存空间。
当一个程序所需的总存储空间接近上限,垃圾清理就会启动,删除掉没用的数据,一般情况下不用特别注意垃圾清理的执行。
但是大量的清理动作不断地重复,很快地消耗掉帧像周期,花费在垃圾清理上的时间越多,播放或发送录音等事情的时间就越少。
工程师们制造的内存泄露,是垃圾清理运行的常见因素,内存泄露是不能被继续使用的空间,但是垃圾收集器却无法辨别出来,结果他们就一直存在于堆中,占用有效空间,永远无法被删除,随着内存不断泄露,堆中的可用空间就不断变小,这意味着为了执行常用的程序,垃圾清理需要启动的次数越来越多。
内存泄漏表示的是不再用到的对象因为被错误引用而无法进行回收
搜索跟修复泄露是个很棘手的问题,有些泄露很容易就会产生,例如对没有使用的对象的循环引用。不过有些也很
复杂,例如,在类别载入器安装未完成就强制执行,不管怎样,一个程序想要运行得又快又好,就需留意可能存在的
内存泄露。你的代码将允许在各种各样的设备上,又互相结合,不是所有的数据都占用同样的内存,不过,还在有一
个简单的工具,可以查看Android SDK中潜在的漏洞。
自己的理解:
内存不在GC掌控之内了。
当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。
内存泄漏是造成应用程序OOM的主要原因之一!我们知道Android系统为每个应用程序分配的内存有限(16M,32M,64M,8M根据不同手机确定),而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过这个系统分配的内存限额这就造成了内存溢出而导致应用Crash。
内存泄漏与内存溢出的区别?
所以内存泄漏最终积累下来会导致内存溢出。
Android中常见的内存泄漏总结:
单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
public class CommUtil {
private static CommUtil instance;
private Context context;
private CommUtil(Context context){
this.context = context;
}
public static CommUtil getInstance(Context context){
if(instance == null){
instance = new CommUtil(context);
}
return instance;
}
}
这是一个最简单的单例模式,创建单例的时候需要去获取一个context,而这个context往往很多人直接获取的是调用的activity的contxet,这里其实存在着很大的问题,试想如果MainActivty如果因为某种原因销毁掉了,但是实际上是销毁掉了吗?不是的,大家可 以去试试用Mat工具去看下(不知道怎么用的后续文章会讲述),其实MainActivity还是存在于堆内存的,因为他被CommUtil持有着引用,所以GC是不会去回收此内存的。
最终这个故事告诉我们能用Application的context就用Application的,CommonUtil生命周期跟MainActivity不一致,而是Application进程同生同死。
2.Handler造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//处理消息
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadData();
}
private void loadData(){
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用
我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理 消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及 时回收,引发内存泄漏。
正确做法:创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息。
正确做法如下:
public class MainActivity extends AppCompatActivity {
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);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
3.非静态内部类创建静态实例造成的内存泄漏
有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:
public class MainActivity extends AppCompatActivity {
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 {
//...
}
}
这里非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收.
正确做法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext。
4.线程造成的内存泄漏
对于线程肯定很多人都写过这样的代码:
//——————test1
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(10000);
return null;
}
}.execute();
//——————test2
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(10000);
}
}).start();
上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成,那么将导致Activity的内存资源无法回收,造成内存泄漏。正确的做法还是使用静态内部类的方式,如下:
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();
5.资源未关闭造成的内存泄漏
对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
6.观察者模式设置监听不清理造成的内存泄漏
有时候我们在写自定义控件的时候可能会写这样的代码:
MyView.java:
public class MyView extends View{
public MyView(Context context){
super(context);
init();
}
public interface MyListener{
public void myListenerCallback();
}
private void init(){
ListenerCollector collector = new ListenerCollector();
collector.setsListener(this,myListener);
}
private MyListener myListener = new MyListener() {
@Override
public void myListenerCallback() {
System.out.print("有被调用");
}
};
}
ListenerCollector.java:
public class ListenerCollector {
static private WeakHashMap<View,MyView.MyListener> listenerCollector = new WeakHashMap<>();
public void setsListener(View view, MyView.MyListener listener){
listenerCollector.put(view,listener);}
}
这里的设置监听很容易出现内存泄露,问题就出在static,例如现在去不断的旋转屏幕,MyView在界面上就会不断的执行,不断执行就会不断去调用init()方法,不断去调用new ListenerCollector(),虽然不断的调用但是仍然只会存在一个listenerCollector,会将之前旋转屏幕的所有view的listener监听都放在listenerCollector里面了。这样虽然view销毁了,但是WeakHashMap中的相对应的listener不会销毁,这样就导致了无法释放listener,从而导致内存泄漏。
正确的做法:
public class ListenerCollector {
static private WeakHashMap<View,MyView.MyListener> sListener = new WeakHashMap<>();
public void setsListener(View view, MyView.MyListener listener){ sListener.put(view,listener);}
public static void clearListeners(){
//移除所有监听。
sListener.clear();
};
}
并在MainActivity中的onStop方法中清除:
@Override
protected void onStop() {
super.onStop();
ListenerCollector.clearListeners();
}
至此内存泄漏的情况大体介绍完毕,内存泄漏是每个程序员应该去注意的问题,这样才能开发出一个高性能的App.
后面将会继续带来性能优化的其他篇章,有意见可以评论。