一、问题背景
帮助朋友做一个项目中的蓝牙模块,需求是进入功能界面,开启蓝牙连接,离开界面后断开蓝牙连接,已被给后续的模块使用蓝牙资源。
常规逻辑:判断蓝牙权限->扫描蓝牙设备->配对连接->数据传输->关闭蓝牙连接. 每次进入、离开均会连接和断开连接蓝牙。
问题现象:对于同一个蓝牙界面,第一次进入能够显示搜索到的蓝牙设备。当退出后,再次进入不能够显示蓝牙设备,也就无法后续的操作。
二、问题分析
好了,至此,问题的现象和项目中的步骤已经明确。废话不多说先来几段硬核的代码,看看我们的实现主流程。
初始化:涉及到一个搜索的dialog,里面显示搜索到的devices,还有控制蓝牙连接&数据解析的类。
扫描设备:
蓝牙设备的监听通过广播完成(有兴趣的小伙伴可以自行了解蓝牙相关知识,这里只做简单介绍)
资源释放工作:
当前我是把蓝牙操作界面所涉及都放在这个基类:BluetoothActivityCompat中。
当我拿到问题时候,简单看了下流程,然后关键点加了一些日志,分别是initview, onStartScan, onFoundDevice(),了解是否检测到了蓝牙设备,已经管理蓝牙设备的list里面的数据情况。
通过日志分析,生命周期方法正常执行,并且管理蓝牙的集合数据正常,设备可以扫描到蓝牙设备,只是第一次在mSearchingDialog中显示了已经扫描到的蓝牙设备,后续扫描到的设备不显示,至此问题定性完成:UI显示没有及时更新导致,蓝牙功能正常。
发现了问题的根因,接下来修改:首先
我们可以看到原先的思路是,调用mBluetoothAdapter.notifyDataSetChanged()对数据进行操作,因为涉及到多出的数据清除,添加。所以我修改了数据刷新的方式,并将adpter初始化中的数据源修改
adapter中添加setData()方法当运行后惊喜的发现,依然不行。之后是网上百度环节,百度给出的场景多为:adapter中的数据源被修改了地址的指针,姑且称作数据源为mData, 一般为mData = list;修改了mData的内存地址,然而adapter中的数据依然指向原有的数据集地址,好似黑夜中看到了一丝丝的火光,恨不得立马燃爆我的内心。马上开箱去检查我的代码。
然后做出修改:
这样子好像大功告成,刷新了数据并且避免了数据源指向新的对象, 堪称完美。但是打脸的时刻在下一秒来临,一顿 running,依旧如故。至此我陷入了无穷的思考,然后把思绪迁移到对象的释放上来,会不会是由于dialog没有销毁,或者是adapter没有销毁导致的呢?然后在destroy中添加了释放的代码,并且在initview中添加了对象的日志打印;
经过日志分析后发现并无异样,正常的释放和初始化,并且开始adapter=null,这一切看似都没有问题。那么我怀疑是不是这个activity的启动模式是singletask/singletop呢,导致了重新进去并没有销毁把之前的界面展示出来。赶紧查看mainifest.xml啥也没发现,一看是默认的standard。在我后续的测试中发现了,多测试几次出现了crash,原因是mBlueetoothAdapter不能为null,一看代码,映射到onStartScan() 中
按照常规分析,此activity是standard模式,生命周期中执行了onDestroy(),应该每次进入都会产生一个新的实例,对应java的内存栈的话,应该是隔离的,这里的crash是在initview()执行后,按照道理不会产生crash,日志为证:
如果说crash 的话只有一个原因,activity的实例依然存在java内存中, 我并没有进行内存分析,先按照我的思路寻找内存泄露的点。一顿查阅代码后发现,此activity中引用了一个单例的蓝牙管理类
确实通过listener的方式持有了activity的引用,导致释放不掉,再次进入也就会产生操作的还是上一个activity 的对象的问题,就会出现进入展示了一个新的dialog,但是没有展示数据,因为那些对象全都是上一个的。
修改方案:每一个都是standard模式,没有必要单例,所以改成非单例模式。也可以通过WeakReference来完成优化。此处我选择第一种。至此问题解决,再次运行完美通过。
三、总结
1.遇到问题需要先找到根因,再指定解决方案,不能急躁,不然越着急越是搞不定。
2.adapter 刷新数据失效的原因,数据源变为新的对象,指向新的地址。 至于有助于理解java内存中的对象的引用机制。
3.内存泄露的现象,分析机制,已经如何解决。即使之前我可能看过很多博客,但是没有亲自遇到修改相关问题,理解更加深入了一步。
好了,今天周五,祝愿各位看官每天都能像周五一样迎接周末。