每次来公司面试的人,一般都会问最基本的两个问题,一个是自定义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会造成检测如下的:
这种情况我们可以通过使用弱引用的方法来优化,修改如下:
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;
}
检测结果如下:
解决办法,使用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;//在其他页面进行接口实现
}
……
}
造成的内存泄漏分析图如下:
原因分析:在其他页面进行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去快速集成播放一个视频,这样做就会内存泄漏。检测结果如下:
从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
解决办法上面有提到过,如下:
没错,我就是截图过来滴。
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);
}
}
检测到泄漏结果如下:
原因分析:由于 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();
}
}
检测到的内存泄漏结果如下:
原因分析:在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);
}
}
检测结果如下图:
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、 留意活动的生命周期,在使用单例,静态对象,全局性集合的时候应该特别注意置空。
文章中部分代码纯手打,可能有个别单词误差,如果有误差,还请各位看官理解,如果能留言指出就十分感谢了。
等等,最后:盗用大牛的一句话,技术无罪,我是aserbao,微信公众号aserbao,微博同名。随时欢迎撩(学习交流)。