安卓内存分析(2)——常见内存泄漏场景二

时间:2023-02-10 12:03:06

问题背景

在前面我们已经梳理了部分常见的内存泄漏场景,参考 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 造成内存泄露四种内存泄漏场景的介绍,持续更新,有兴趣的同学可以进一步深入研究。