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用ViewAnimator写一个简单的控件轮播效果
Android用ViewAnimator写一个简单的控件轮播效果