先来看一下出现的问题,下面代码是将这个任务放入线程池中执行,获取到bitmap后再在UI线程做一些UI的更新。
一些特殊场景下走到mVideoView这一行会出现空指针,界面已经退出,此时用户感知不到
特殊场景:非wifi且是播放服务器视频,且使用createVideoThumbnail没有解析出视频第一帧
ThreadPool.addTask(new Runnable(){
@Override
public void run(){
final Bitmap bitmap = createVideoThumbPic();
if(mainHandler != null){
mainHandler.post(new Runnable(){
//其他逻辑省略
if(bitmap == null){
mVideoView.setVisibility(GONE);//这一行引起空指针
}
});
}
}
});
一开始有点纳闷,mVideoView难道是因为内存不足被回收了?然后又查看是不是哪里把mVideoView置为了null,果然在onDestroy里把mVideoView置为了null,一定是这里有问题,
界面虽然onDestroy退出了,但是线程里的任务还在执行,等到执行到mVideoView这一行的时候就出现了NullPointerException.
下面先复现一下问题,复现的办法最开始想的是:
1. 难道是添加这个任务的时候线程池中的核心数量满了?导致退出界面后线程池才开始从阻塞队列中调度这个任务?不太有可能,因为当前情境下这个线程池只有这一个任务
2. 再次review代码,发现原来有个耗时操作,就是从url去解析出图片的第一帧为Bitmap,用户可能在这个时候退出界面
用代码验证,这2种假设都是可以复现这个问题的。说第二种吧尝试在mVideoView执行之前让线程sleep 5秒,sleep的时候退出界面 ---> onDestroy中将mVideoView置为null->执行到那一行时自然会空指针
ThreadPool.addTask(new Runnable(){
@Override
public void run(){
final Bitmap bitmap = createVideoThumbPic();
Thread.sleep(5000);//睡眠5秒
if(mainHandler != null){
mainHandler.post(new Runnable(){
//其他逻辑省略
if(bitmap == null){
mVideoView.setVisibility(GONE);//这一行引起空指针
}
});
}}});
问题复现来了
02-24 14:43:21.849 25030-25030/? E/eup: java.lang.NullPointerException: Attempt to invoke virtual method 'void com.tenpay.sdk.view.QQVideoView.setVisibility(int)' on a null object reference
下面用MAT内存分析工具dump HPROF file进行分析
dump HPROF file的时机是用户进入界面后马上按back键(这个时候线程正在sleep中),且置bitmap为null,让代码sleep后走到mVideoView那一行,便可复现这个空指针异常
02-24 14:43:21.849 25030-25030/? E/eup: java.lang.NullPointerException: Attempt to invoke virtual method 'void com.tenpay.sdk.view.QQVideoView.setVisibility(int)' on a null object reference
看一下这个时候的activity引用情况,可见虽然onDestroy了但是还没有被回收
原因是new runnable这个匿名内部类会持有外部类GrapMchHbActivity这个外部类,而线程池中的阻塞队列持有这个内部类,根据可达性算法,GC的时候GrapMchHbActivity自然无法被回收
解决办法是
1 将onDestroy中的mVideoView = null这行去掉,实属多余
2 finish后不进行handle消息的处理
经测试,不再出现空指针
从这个crash可以学习到一些预防内存泄漏的教训
1. 对于内部类的使用要谨慎,因为会默认持有外部类的引用,在内部类的生命周期大于外部类的时候容易导致外部在该被回收的时候没有被GC回收
2.使用handler处理异步的时候,如果这个消息不再需要被处理,及时让Handler停止处理消息。