问题背景
在前面我们已经梳理了部分常见的内存泄漏场景,参考 https://blog.51cto.com/baorant24/6045858 ,包括:单例导致的内存泄漏、静态变量导致的内存泄漏、非静态内部类导致的内存泄漏、未取消注册和回调导致的内存泄漏等,本文将继续对几种常见的内存泄漏场景进行补充。
问题分析
1、Timer 和 TimerTask 导致内存泄露
Timer 和 TimerTask 在 Android 中通常会被用来做一些计时或循环任务,比如实现无限轮播的 ViewPager,之前有个应用首页的轮播图就出现过这个问题,代码如下:
public class MainActivity extends AppCompatActivity {
private ViewPager mViewPager;
private PagerAdapter mAdapter;
private Timer mTimer;
private TimerTask mTimerTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
mTimer.schedule(mTimerTask, 3000, 3000);
}
private void init() {
mViewPager = (ViewPager) findViewById(R.id.view_pager);
mAdapter = new ViewPagerAdapter();
mViewPager.setAdapter(mAdapter);
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
loopViewpager();
}
});
}
};
}
private void loopViewpager() {
if (mAdapter.getCount() > 0) {
int curPos = mViewPager.getCurrentItem();
curPos = (++curPos) % mAdapter.getCount();
mViewPager.setCurrentItem(curPos);
}
}
/**
* 取消timer和mTimerTask
*/
private void stopLoopViewPager() {
if (mTimer != null) {
mTimer.cancel();
mTimer.purge();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 记得取消timer和mTimerTask
stopLoopViewPager();
}
}
代码分析: 当Activity 销毁的时,有可能 Timer 还在继续等待执行 TimerTask ,它持有 Activity 的引用不能被回收,这样就导致了内存泄漏,因此当我们 Activity 销毁的时候要立即 cancel 掉 Timer 和 TimerTask ;
2、集合中的对象未清理造成内存泄露
如果一个对象放入到 ArrayList 、 HashMap 等集合中,这个集合就会持有该对象 的引用; 当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而 此对象已经无用了),这个对象就造成了内存泄露;并且如果集合被静态引用的话,集合里面那 些没有用的对象更会造成内存泄露了;所以在使用集合时要及时将不用的对象从集合 remove ,或 者 clear 集合,以避免内存泄漏。下面是LeakCanary官方的一个demo,参考 https://square.github.io/leakcanary/fundamentals-fixing-a-memory-leak/ ,代码如下:
class ExampleApplication : Application() {
val leakedViews = mutableListOf<View>()
}
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
val textView = findViewById<View>(R.id.helper_text)
val app = application as ExampleApplication
// This creates a leak, What a Terrible Failure!
app.leakedViews.add(textView)
}
}
代码分析: ExampleApplication实例,Application实例是一个永远不会被垃圾收集的单例,生命周期和整个应用一致。该实例具有一个leakedViews字段,该数组具有一个元素,该元素引用一个TextView具有mContext字段的元素它引用了实例MainActivity,这样就导致了MainActivity一直无法被销毁了。
3、属性动画造成内存泄露
动画同样是一个耗时任务,比如在 Activity 中启动了属性动画(ObjectAnimator),但是在销毁 的时候,没有调用 cancle 方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去, 动画引用所在的控件,所在的控件引用 Activity ,这就造成 Activity 无法正常释放; 因此同样要 在 Activity 销毁的时候 cancel 掉属性动画,避免发生内存泄漏。代码如下:
@Overrideprotected
void onDestroy() {
super.onDestroy();
// 取消动画
mAnimator.cancel();
}
4、WebView 造成内存泄露
代码中, WebView 在加载网页后会长期占用内存而不能被释放,因此我们在 Activity 销毁后要调用它的 destory() 方法来销毁它以释放内存 另外在查阅 WebView 内存泄露相关资料时看到这种情况: Webview 下面的 Callback 持有 Activity 引用,造成 Webview 内存无法释放,即使是调用了Webview.destory() 等方法都无法解决问题( Android5.1 之后) 最终的解决方案是:在销毁 WebView 之前需要先将 WebView 从 父容器中移除,然后在销毁 WebView,代码如下:
@Override
void onDestroy() {
super.onDestroy();
// 先从父控件中移除
((ViewGroup)mWebView.getParent()).removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
}
问题总结
继前一篇介绍了单例导致的内存泄漏、静态变量导致的内存泄漏、非静态内部类导致的内存泄漏、未取消注册和回调导致的内存泄漏等四种常见的内存泄漏场景后(参考https://blog.51cto.com/baorant24/6045858 ),本文增加了Timer 和 TimerTask 导致内存泄露、集合中的对象未清理造成内存泄露、属性动画造成内存泄露、WebView 造成内存泄露四种内存泄漏场景的介绍,持续更新,有兴趣的同学可以进一步深入研究。