Android控件轮播效果的延迟启动和内存泄漏

Posted wodongx123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android控件轮播效果的延迟启动和内存泄漏相关的知识,希望对你有一定的参考价值。

前言

之前由于在项目中需要使用轮播的功能,所以学习了一下ViewAnimator的用法和轮播的一些思路。

见这篇:android用ViewAnimator写一个简单的控件轮播效果
https://blog.csdn.net/qq_41872247/article/details/117089455

但是在编码的过程中,意外的发现,CountDownTimer这个类,他支持重复执行,也自带Handler可以在主线程执行回调,但是他却不支持延迟启动的功能。

而当我使用别的方式延迟启动的时候,就出现了会发生内存泄漏的场景,分享一下这次经验教训。

1. ViewAnimator的延迟启动

先贴一下原先的代码,代码很简单,一共也就三个方法

public class MainActivity extends AppCompatActivity {

    private static final long INTERVAL = 2000;
    private static final long FUTURE = INTERVAL * 2;

    private int listIndex = 0;
    ViewAnimator viewAnimator;
   String[] list = new String[]{"我是第一个TextView", "我是第二个TextView", "我是第三个TextView", 
            "我是第四个TextView", "我是第五个TextView", "我是第六个TextView", 
            "我是第七个TextView", "我是第八个TextView", "我是第九个TextView", 
            "我是第十个TextView"};


    CountDownTimer timer;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewAnimator = findViewById(R.id.va);

        initView();
        initTimer();
    }

    private void initView() {
        // 只加载两个TextView轮流播放
        TextView textView = new TextView(this);
        textView.setText(list[0]);
        TextView textView1 = new TextView(this);
        viewAnimator.addView(textView);
        viewAnimator.addView(textView1);

        viewAnimator.setInAnimation(this, R.anim.anim_in);
        viewAnimator.setOutAnimation(this, R.anim.anim_out);
    }

    private void initTimer() {
        timer = new CountDownTimer(FUTURE, INTERVAL) {
            @Override
            public void onTick(long millisUntilFinished) {
                viewAnimator.showNext();
                // 获取下个控件
                int index = viewAnimator.getDisplayedChild();
                TextView textView = (TextView) viewAnimator.getChildAt(
                        index > viewAnimator.getChildCount()? 0 : index);
                // 更新下个控件的内容
                textView.setText(list[listIndex++]);
                // 更新数据的下标
                if (listIndex >= list.length)
                    listIndex = 0;
            }

            @Override
            public void onFinish() {
                timer.start();
            }

        };

        timer.start();
    }

    @Override
    protected void onStop() {
        super.onStop();
        if(timer != null){
            timer.cancel();
            timer = null;
        }
    }
}

这个代码可以正常的执行轮播功能,没有什么问题,唯一的缺陷就是,当页面一启动的时候,轮播就立刻开始了,没有延迟,而一般情况下我们的需求是要等一个间隔时间,才开始进行第一次轮播。

于是我尝试延迟启动。

private void initTimer() {
    timer = new CountDownTimer(FUTURE, INTERVAL) {
        @Override
        public void onTick(long millisUntilFinished) {
            viewAnimator.showNext();
            // 获取下个控件
            int index = viewAnimator.getDisplayedChild();
            TextView textView = (TextView) viewAnimator.getChildAt(
                    index > viewAnimator.getChildCount()? 0 : index);
            // 更新下个控件的内容
            textView.setText(list[listIndex++]);
            // 更新数据的下标
            if (listIndex >= list.length)
                listIndex = 0;
        }

        @Override
        public void onFinish() {
            timer.start();
        }

    };

    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.currentThread().sleep(INTERVAL);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            timer.start();
        }
    });
}

这样做确实可以正常的延迟启动,但是我们忽略了一些应用场景。

2. 内存泄漏

假设我们的业务逻辑页面,刚打开这个页面后,不等第一次轮播开始,立刻关闭,会发生什么事?

onStop中,由于timer尚未start,所以cancel无效。

而匿名的线程已经启动,无法停止。而匿名的线程可以调用timer这个MainActivity的成员变量,就代表了timer持有MainActivity的引用。

MainActivity已经调用过了onDestroy,按道理说他的对象应该被回收,但是由于他的对象还被匿名的线程所持有,所以GC无法回收MainActivity。

第一个问题:匿名内部类持有外部类,且内部类的生命周期大于外部类,造成了内存泄漏。

在onStop中,我们将timer设为null,希望可以回收生成的CountDownTimer对象,但是线程在等待时间之后,会执行timer.start();

第二个问题:调用为空的对象,造成了空指针异常。

这两个问题,看似不大,而第二个问题也可以通过判空来处理。但是如果我的第一次轮播间隔不是代码中的2s,而是20s,甚至1min呢?再加上一个Activity本身所携带的各种各样的数据,那这个内存泄漏涉及的范围,是不是就特别广,特别大和特别久了。

3. 处理内存泄漏

这次内存泄漏的根本原因是:匿名内部类持有外部类的引用 + 内部类的生命周期大于外部类的生命周期。

那怎么处理呢,Thread有个特点就是,他的生命周期在运行之后除了Interrupt以外,没办法主动停止只能等他运行结束。用线程池也是一样的,停止也是会执行完当前内容才停止。

那我们要做的事就很简单了,就是让这个延时功能,可以在运行前的任意时刻强行停止,怎么实现这个功能呢?答案是用Handler的postDelay,关于Handler的机制这里不多说明,简单来说就是注册后的Runnable可以通过removeCallback注销。

而且由于我们的CountDownTimer也是封装的handler,直接用handler也不需要CountDownTimer了。

Runnable timerTask;

Handler handler = new Handler();

private void initTimer() {
   if (timerTask != null){
       handler.removeCallbacks(timerTask);
   }

   timerTask = new Runnable() {
       @Override
       public void run() {
           viewAnimator.showNext();
           // 获取下个控件
           int index = viewAnimator.getDisplayedChild();
            TextView textView = (TextView) viewAnimator.getChildAt(
                    index > viewAnimator.getChildCount()? 0 : index);
           // 更新下个控件的内容
           textView.setText(list[listIndex++]);
           // 更新数据的下标
           if (listIndex >= list.length)
               listIndex = 0;

           handler.postDelayed(timerTask, INTERVAL);
       }
   };

   handler.postDelayed(timerTask, INTERVAL);
}

@Override
protected void onStart() {
    super.onStart();
    initTimer();
}

@Override
protected void onStop() {
    super.onStop();
    if (timerTask != null){
        handler.removeCallbacks(timerTask);
        timerTask = null;
    }
}

以上是关于Android控件轮播效果的延迟启动和内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

Android控件轮播效果的延迟启动和内存泄漏

Android用ViewAnimator写一个简单的控件轮播效果

Android自己定义控件之轮播图控件

Android用ViewAnimator写一个简单的控件轮播效果

Android用ViewAnimator写一个简单的控件轮播效果

Android-----------广告图片轮播控件