一个null引发的SharedPreferences惨案

今日科技快讯

昨天,Google在中国举办的2016开发者大会在北京国家会议中心举行,由于我下周会去参加上海的那一场Google开发者大会,因此也就没有及时去网上了解一下大会上具体讲了哪些内容。但是,昨天有一条重磅新闻被刷屏了,那就是Google终于提供了一个中国版的Android开发者网站,而且网站中的绝大部分内容都和原版的Android开发者网站是同步的。我进行了一些测试,除了全网搜索功能不能使用,以及网页中关联的Youtube视频不能播放之外,其他功能基本都是可以正常使用的,这意味着以后我们查阅文档都不需要再翻墙了,同时这也被视为是Google回归中国的第一步。

中国版的Android开发者官网地址是:

https://developer.android.google.cn

作者简介

本篇来自 tracyxia 的投稿,分析了他使用SharedPreferences时遇到的一个奇怪的问题,希望能帮助那些还不了解的朋友。最后祝大家周末愉快!

tracyxia 的博客地址:

http://blog.csdn.net/tracyxia716

序言

本周在安卓博客周刊里看到了一篇:

请不要滥用SharedPreference

http://weishu.me/2016/10/13/sharedpreference-advices

感觉颇有收获。而我恰好在这周碰到了一个关于SharedPreference的bug,让我费了一番功夫才找出,所以在这里就写一篇文章来记录一下这个bug。

Bug再现

首先让我来讲下这个bug的由来,并通过一个demo来模拟下bug现场。

在我所做的产品中有个XX云盘模块,其中有部分登陆信息是记录在SharedPrefrence中的。突然有一天,测试的同学拿来一部手机跟我说,这个手机登陆以后,杀掉进程再重新启动app就会丢失掉之前的登陆信息。当时我就一脸懵逼,再拿来观察一下现象,更加奇怪的事情是:登陆以后我发现登陆信息确实保存在了SharedPreference中,杀掉进程以后也还在,但是重新打开app以后,发现SharedPreference里的数据有部分被清空了(注意不是全部清空,而是部分)。

从现象上来看让人感到一头雾水,同时有的手机上又没有这种奇怪的现象发生,让我一时间都觉得是不是这个手机的rom比较烂所导致的。但是作为一个开发人员当然不能如此妄下定论,于是我写了一个demo来测试,发现没有上述的问题。那么可以肯定的是,这是我自己的app的逻辑存在bug!但是我全局找了一遍代码,并没有发现有任何去删除SharedPreference值的逻辑,于是这条线索又断了。

一头雾水的我只能转而研究SharedPreference文件本身,此时我发现了一个很怪异的现象,我的SharedPreference里有个key为null的值。key为null?作为一个开发人员,对于空指针还是有着十足的敏感,于是我就怀疑到了是不是这个为null的key引起的呢?下面我通过一个demo演示就真相大白了:


存储一个正常的key并获取值:

存储一个key为null的值并获取:

以上我们的存储与获取都是正常的,并没有什么好说的,现在我杀掉进程再重新启动这个demo,那么诡异的现象就发生了:

杀掉进程以后去查看值发现都还在

但是都获取不到值了

是不是很怪异,明明值还在,但是就是获取不到真实的值,但是更加怪异的事情还在后面,当我点击了”存储一个为null的key进去”的时候再去观察SharedPreference数据发现数据都没了: 

真相大白,原来杀掉进程以后数据被清空了是因为存储了一个为null的key引起的。

分析Bug

既然问题已经定位了,就很容易解决了,将那个为null的key找出来赋值进去即可。但是作为一个对自己有高要求的研发人员,还是很有必要研究清楚整个bug的来龙去脉的。

首先,从现象上来分析下这个bug产生的原因,在我杀掉进程第二次进入app没有获取到值的时候,在logcat上打下了如下一段日志:


同时,此异常只有在我第一次获取值没有获取到的时候才打印出来,后面无论有多少次未获取到都不会打印。大概从字面上的意思上来说,就是去获取了一个没有attribute的一个Map值产生的。通过我开头提到的文章了解到,SharedPreference每次取值的时候都是在第一次get的时候就将SharedPreference文件里的值一一读取并存入内存中,而后我们的每次get操作都是从内存当中取出的。

基于这个推断,我大胆的推测:之所以为null,是因为我们在此第一次去解析xml文件失败(抛异常了),所以导致了我们内存中取出来的map里为null,以至于我们后面每次取值都为null。而后写入的值则是因为我们将内存的值直接写入磁盘,覆盖了原有的值,所以导致了我们的数据”部分被删除”。至于有的手机没有该问题,我猜测的是Android在5.0开始对于这类问题进行了容错处理,就是当发现有的value没有key的时候也能将值写入内存,只不过对应的key为null而已。

验证猜想

为了验证之前的猜想,就必须深入到安卓系统源码研究了。这里我们拿4.4的代码与5.1的代码来进行对比。首先我们根据抛异常的位置到4.4的源码里来一窥究竟:


真相豁然开朗,当name为null的时候抛出了异常,与我之前在logcat上看到的一模一样。那么接着我们再看看5.1的代码这段是怎么处理的:


我们可以看到明显的区别是,这里没有对name为null的分支进行处理,而是一视同仁的去读取值写入了map中。

那么,至于为什么在杀掉进程以后再次开启APP又会”删除”掉部分值呢?我们接着分析源码:

SharedPreferencesImpl.Java


在这一块上4.4的代码与5.1的代码几乎一致,所以,导致4.4会删除值的原因就在于之前并没有从磁盘中读取到值到内存中。在对值进行copy的时候就”丢失”,从而导致写入进去的新文件里没有以前的旧值,由此现象上来看好像是某些值被删除了,但实际上确切的说应该是被空值所覆盖。

总结

总的来说,这个问题的根源在于4.X的sdk里允许了往SharedPreferences里写入key为null的值而不允许取出来,并且不仅不能取出来,甚至还在写的过程当中引发了”删除”数据这样灾难性的后果。这样是及其不合理的,所以google在后续的SDK里也对这一块的逻辑修复了。不过,作为一个应用层开发人员,也是需要从自身检讨一下这样的低级错误。毕竟这个错误的后果十分严重,但又无色无味难以发现,以后需要尽可能的避免这样的错误再次发生。

相信通过这样一次从现象入手分析问题到追踪,再到SDK源码比对各版本间的差异,对于自身的水平提高还是有很多的益处的,希望以后还能够这样来分析一些更加深入的问题。当然,最后想说的是,我们还是从自身做起,尽量少犯低级错误,少出bug,做一个高水平的工程师,而不是整天debug的码农。

更多

每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。

如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。

欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:

好的,下面是使用 Kotlin 和 SharedPreferences 编写一个简单的注册应用程序的基本步骤: 1. 创建一个新的 Kotlin Android 项目,并在 app 的 build.gradle 文件中添加以下依赖项: ```groovy implementation "androidx.appcompat:appcompat:1.3.0" implementation "androidx.core:core-ktx:1.6.0" ``` 2. 在 res/layout 目录中创建一个名为 activity_register.xml 的新布局文件,并在其中添加以下控件: ```xml <EditText android:id="@+id/usernameEditText" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Username" android:inputType="text" /> <EditText android:id="@+id/passwordEditText" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Password" android:inputType="textPassword" /> <Button android:id="@+id/registerButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Register" /> ``` 3. 创建一个新的 Kotlin 类,用于表示用户信息。例如: ```kotlin data class User(val username: String, val password: String) ``` 4. 在 MainActivity 类中,实例化 EditText 和 Button 控件,并在 Button 的点击事件中保存用户信息到 SharedPreferences。例如: ```kotlin class MainActivity : AppCompatActivity() { private lateinit var usernameEditText: EditText private lateinit var passwordEditText: EditText private lateinit var registerButton: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_register) usernameEditText = findViewById(R.id.usernameEditText) passwordEditText = findViewById(R.id.passwordEditText) registerButton = findViewById(R.id.registerButton) registerButton.setOnClickListener { val username = usernameEditText.text.toString() val password = passwordEditText.text.toString() val user = User(username, password) saveUser(user) Toast.makeText(this, "Registration successful", Toast.LENGTH_SHORT).show() } } private fun saveUser(user: User) { val sharedPreferences = getSharedPreferences("users", Context.MODE_PRIVATE) val editor = sharedPreferences.edit() editor.putString(user.username, user.password) editor.apply() } } ``` 5. 运行应用程序并测试。 这样,一个简单的注册应用程序就完成了。当用户在 EditText 中输入用户名和密码,并单击 Register 按钮时,应用程序将使用 SharedPreferences 保存用户信息。您可以使用 SharedPreferences 的 getString() 方法检索用户密码,以验证用户身份。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值