一篇技术好文之Android性能优化内存泄漏无处可藏(图文)

默认标题_官方公众号首图_2018.04.29 (1).png

每次来公司面试的人,一般都会问最基本的两个问题,一个是自定义View的绘制流程及事件分发,第二个就是性能优化内存泄漏如何处理?第一个问题基本上都能说个大概,第二个问题其实很多工作好几年的都不一定能回答的比较让人满意。这里整理下基本的内存泄漏及解决办法。使用的是LeakCannary来进行检测。

你能从本文了解到如下知识:1. 什么是内存泄漏 2. 内存泄漏的分类及影响 3.常见的内存泄漏及解决办法 4.文章总结

[toc]

什么是内存泄漏?

内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。再形象点比喻就像家里的水龙头没有拧紧,漏水了。

内存泄漏的分类及影响?

分类:常发性内存泄漏,偶发性内存泄漏,一次性内存泄漏,隐式内存泄漏。
危害:内存泄漏造成的影响其实是内存泄漏的堆积,这将会消耗系统所有的内存。所以一个内存泄漏危害并不大,因为不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

常见的内存泄漏及解决办法:

1. 单例造成的内存泄漏:

第一种情况:

public class LoginActivity extends Activity {
    public static LoginActivity instance;
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        ……
        instance = this;
    }
}

在其他地方引用LoginActivity.instance会造成检测如下的:

image

这种情况我们可以通过使用弱引用的方法来优化,修改如下:

public class LoginActivity extends Activity {
    public static WeakReference<LoginActivity> instance;
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        ……
        instance = new WeakReference<LoginActivity>(this);
    }
}

单例造成的内存泄漏第二种情况(在网上找到的实例及图片):

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

检测结果如下:


image

解决办法,使用Application的context替代Activity的context,修改后的diam如下:

public class LoginManager {
  private static LoginManager mInstance;
  private Context mContext;
  private LoginManager(Context context) {
    this.mContext = context.getApplicationContext();
  }
  public static LoginManager getInstance(Context context) {
    if (mInstance == null) {
      synchronized (LoginManager.class) {
        if (mInstance == null) {
          mInstance = new LoginManager(context);
        }
      }
    }
    return mInstance;
  }
  public void dealData() {
  }
}

2. 接口实现引用造成的内存泄漏。

不知道这样实现代码的多不多?

public class MyApplication extends LitePalApplication{
    ……
     UnReadMsgListener unReadMsgListener;
     public void setUnReadMsgListener(UnReadMsgListener unReadMsgListener){
          this.unReadMsgListener = unReadMsgListener;//在其他页面进行接口实现
     } 
     ……
}

造成的内存泄漏分析图如下:

image

原因分析:在其他页面进行setUnReadMsgListener操作,MyApplication将明显持有对此接口的引用,此接口被Activity实现,所以MyApplication一直持有Activity的引用。
在尽量不修改原代码的情况下,解决办法如下:

UnReadMsgListener unReadMsgListener;
    WeakReference<UnReadMsgListener> mListenerWeakReference;
    public void setUnReadMsgListener(UnReadMsgListener unReadMsgListener){
        mListenerWeakReference = new WeakReference<UnReadMsgListener>(unReadMsgListener);
        this.unReadMsgListener = mListenerWeakReference.get();
    }

3. 使用ViewVideo造成的内存泄漏(MediaPlayer.mSubtitleController):

有时候为了快速开发,经常会在xml中使用VideoView去快速集成播放一个视频,这样做就会内存泄漏。检测结果如下:

image

从LeakCanary分析结果得出,是由于VideoView持有对Activity的Context的引用造成的。因为我们将VideoView写在XMl中,所以默认是应用当前页面的Context的。

解决办法:
第一种:将VideoView在代码中实现:

VideoView mVideoView = new VideoView(MyApplication.getContext());
//添加到父容器
……

第二种:重写当前Activity页面的attachBaseContext方法:

  @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(new ContextWrapper(newBase)
        {
            @Override
            public Object getSystemService(String name)
            {
                if (Context.AUDIO_SERVICE.equals(name))
                    return getApplicationContext().getSystemService(name);
                return super.getSystemService(name);
            }
        });
    }

4. MediaPlayer源码存在的内存泄漏问题:

这个问题是紧接上一个内存泄漏,上面的处理方式基本能解决VideoView给我们带来的内存泄漏问题。这里我们来深入了解下为什么使用VideoView会造成内存泄漏。看MediaPlayer的源码我们可以得知:在系统的MediaPlayer的release过程中就mSubtitleController 资源未做处理,幸运的是在reset中进行此资源的处理,所以我们在使用MeidPlayer播放视频后进行资源释放时再release时进行下MediaPlayer的reset操作。下面我们看下MediaPlayer的源码:

    //MediaPlayer系统源码
    ……
  public void release() {
        baseRelease();
        stayAwake(false);
        updateSurfaceScreenOn();
        mOnPreparedListener = null;
        mOnBufferingUpdateListener = null;
        mOnCompletionListener = null;
        mOnSeekCompleteListener = null;
        mOnErrorListener = null;
        mOnInfoListener = null;
        mOnVideoSizeChangedListener = null;
        mOnTimedTextListener = null;
        if (mTimeProvider != null) {
            mTimeProvider.close();
            mTimeProvider = null;
        }
        mOnSubtitleDataListener = null;
        _release();
    }
    ……
    public void reset() {
        mSelectedSubtitleTrackIndex = -1;
        synchronized(mOpenSubtitleSources) {
            for (final InputStream is: mOpenSubtitleSources) {
                try {
                    is.close();
                } catch (IOException e) {
                }
            }
            mOpenSubtitleSources.clear();
        }
        if (mSubtitleController != null) {//这里有对mSubtitleController进行处理操作
            mSubtitleController.reset();
        }
        if (mTimeProvider != null) {
            mTimeProvider.close();
            mTimeProvider = null;
        }

        stayAwake(false);
        _reset();
        // make sure none of the listeners get called anymore
        if (mEventHandler != null) {
            mEventHandler.removeCallbacksAndMessages(null);
        }

        synchronized (mIndexTrackPairs) {
            mIndexTrackPairs.clear();
            mInbandTrackIndices.clear();
        };
    }

如果上面的描述不够详细,你可以参考stackoverflow
解决办法上面有提到过,如下:

image

没错,我就是截图过来滴。

image

5. Handler使用造成的内存泄漏(MessageQueue.mMessage)

Handler 的使用造成的内存泄漏问题应该说是最为常见了,我们看一下下面代码:

 public class BaseActivity extends AppCompatActivity {
     ......
     private Handler baseHandler = new Handler();
      @Override
    protected void onResume() {
         baseHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    heartBeat();
                    if (!isPause) {
                        baseHandler.postDelayed(this, 60 * 1000);
                    }
                }
            }, 100);
    }
 }

检测到泄漏结果如下:


image

原因分析:由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

解决办法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,最后当我们Activity销毁后,Looper线程的消息队列中可能会存在待处理的消息,所以我们在Activity的OnDestroy中移除消息队列 MessageQueue 中的消息。修改后代码如下:

public class BaseActivity extends AppCompatActivity {
     ......
      private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;
    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }
    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {//这里切记要判空
        // ...
      }
    }
  }
  private final MyHandler baseHandler= new MyHandler(this);
      @Override
    protected void onResume() {
         baseHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    heartBeat();
                    if (!isPause) {
                        baseHandler.postDelayed(this, 60 * 1000);
                    }
                }
            }, 100);
    }
     @Override
    protected void onDestroy() {
         if (baseHandler != null) {
            baseHandler.removeCallbacks(null);
            baseHandler = null;
        }
    }
 }

当然这里简单说一下软引用和弱引用的使用:记住两点即可:
第一点:如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
第二点:可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

6. 匿名内部类造成的内存泄漏

在异步操作过程中,我们经常会这样做:

public class WelcomeActivity extends Activity {
    ......
    public void sel(){
         new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(10000);
                    //do something ... 里面持有对当前activity的引用
                }
            }).start();
    }
}

检测到的内存泄漏结果如下:


image

原因分析:在Activity结束时,若线程内依旧还有任务未完成,则会发生内存泄漏。上面的Runnable是一个内部类,因此对当前的Activity存在一个隐式引用(文章开头有提到,威胁最大的一种引用)。

解决思路:不使用匿名内部类,通过静态内部类来实现,使用弱应用来持有Activity的引用。

解决后的代码:

public class WelcomeActivity extends Activity {
......
public void sel(){
new Thread(new splashhandler()).start();
}
static class splashhandler implements Runnable {
public void run() {
SystemClock.sleep(10000);
WelcomeActivity welcomeActivity = welcomeActivityWeakReference.get();
if(welcomeActivity != null) //注意判空
//do something ... 里面持有对当前activity的引用
}
}

匿名内部类被异步线程所持有的时候,我们一定要特别小心,如果么有进行任何处理措施,极容易出现内存泄漏的情况。下面我们再分析一种使用AsyncTask过程中造成的内存泄漏处理情况:

public class MainActivity extends Activity {
    public void sel(){
        new AsyncTask<void, void="">() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(10 * 1000);
                //do something ... 里面持有对当前activity的引用
                return null;
            }
        }.execute();
    }
}

原因分析和上面是一样的,Activity结束了,异步任务还未处理完。

解决办法:使用软引用,并在Activity的onDestroy里调用AsyncTask.cancel()方法。

public class MainActivity extends Activity {
    private WeakReference<context> weakReference;
    AsyncTask asyncTask;
    public void sel(){
        asyncTask = new AsyncTask<>() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(10 * 1000);
                //do something ... 里面持有对当前activity的引用
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                super.onPostExecute(aVoid);
                MainActivity activity = (MainActivity) weakReference.get();
                if (activity != null) {
                    //...
                }
            }
        };
        asyncTask.execute();
    }
     @Override
    protected void onDestroy() {
        asyncTask.cancel();
    }
}

关于异步线程内存泄漏的原理,推荐看下这篇文章深入分析 ThreadLocal 内存泄漏问题

7. 集合的内存泄露问题

通常我们会添加一些对象的引用到集合中,当我们不需要用到该集合对象时,我们需要及时将该集合清空掉,如果不清空,将导致这个集合会越来越大。如果集合是静态的话,那情况将会更严重,因为声明为static的生命周期和整个app进程的生命周期一致。

下面代码

public class MyApplication extends LitePalApplication {
     private static Map<String, Activity> destoryMap = new HashMap<>();
    public void registerActivity(String activityName,Activity act) {
        if (allActivities == null) {
             destoryMap.put(activityName, activity);
        }
    }

    public void unregisterActivity(String activityName) {
        destoryMap.remove(activityName);
    }
}

检测结果如下图:


image

HashMap的value对应的Activity对象未释放,这里解决办法我们可以使用上面多次提到过的使用弱应用去处理HashMap的Value值,当然这是在最小修改的前提下进行。如果需要对Activity进行管理,这里建议不要使用HashMap,可以使用HashSet去做,同样最好存放的是弱应用对象,而且集合列表最好不要使用static修饰。修改后的代码如下(使用HashMap或者HashSet存储对象时,最好覆盖hashCode()和equal()方法):

public class MyApplication extends LitePalApplication {
    ……
    private Set<WeakReference<Activity>> destoryMap ;
     public void registerActivity(Activity act) {
        if (allActivities == null) {
            allActivities = new HashSet<WeakReference<Activity>>();
        }
        allActivities.add(new WeakReference<Activity>(act));
    }

    public void unregisterActivity(Activity act) {
        if (allActivities != null) {
            allActivities.remove(new WeakReference<>(act));
        }
    }
}

当然,我们在使用集合时,应该注意不要使用staic去修饰。其次就是使用完集合之后需要将其致空。如果是如下写法也会出现内存泄漏:

public void sel(){
    Vector vector = new Vector(10);
            for (int i = 0; i < 100; i++) {
                Object o = new Object();
                vector.add(o);
                o = null;
            }
}

我们将对象置空其集合还会持有对该对象的引用,为此我们应该在不使用Vector的时侯将vector 置null。这种情况比较常见的就是我们在Recyclerview的适配器中的运用,我们在当前活动页面销毁的时候应该将其对应的所有集合都清空。

8.资源对象没关闭造成的内存泄漏

在开发过程中我们经常会使用到BraodcastReceiver,ContentObserver,InputStream,Cursor,Stream,Bitmap等资源。切记在资源不再使用的时候将其释放,关闭掉。大多数频发的OOM出现绝大部分是因为图片资源未回收。在图片资源使用完后可以通过recycler方法来进行处理:

if(!mBitmap.isRecycled){
    mBitmap.recycle();
    mBitmap = null;
}

当然,广播的注销,内容观察者的注销,输入输出流的关闭,cursor的关闭这些就不一一列举了。只要在使用的时候多留意下这些都不是问题滴。

总结

对于内存泄漏问题,记住以下几点:
1、对于生命周期比Activity长的对象如果需要应该使用ApplicationContext,在需要使用Context参数的时候先考虑Application.Context.
2、在引用组件Activity,Fragment时,优先考虑使用弱引用。
3、在使用异步操作时注意Activity销毁时,需要清空任务列表,如果有使用集合,将集合清空并置空,释放相应的资源。
4、内部类持有外部类的引用尽量修改成静态内部类中使用弱引用持有外部类的引用。
5、 留意活动的生命周期,在使用单例,静态对象,全局性集合的时候应该特别注意置空。

文章中部分代码纯手打,可能有个别单词误差,如果有误差,还请各位看官理解,如果能留言指出就十分感谢了。


image

等等,最后:盗用大牛的一句话,技术无罪,我是aserbao,微信公众号aserbao,微博同名。随时欢迎撩(学习交流)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容