一个Null值引发的神奇Bug,安卓SharedPreferences探索

现象

最近项目中出现一个诡异的Bug,测试同学发现,在完成某项业务流程后,登录状态被清空了。接到问题后,我们第一时间进行复现,均未能成功。

分析定位

开发中,我们使用SDK提供的SharedPreferences(下文中简称为Shared)进行数据的持久化,而在出现Bug的代码中,没有任何清除该登录标志位的操作。于是我提出猜想,会不会是某一次操作Shared时出现问题,导致所有数据被清空。但由于一直未能在测试机上复现,所以迟迟没有定位到原因。直到最近,我们使用出现Bug的同型号手机,在进行一项“查看”操作后,将其复现。

根据以上信息,我们定位到“查看”功能代码,发现在操作Shared写入数据时,会有null作为key的情况。应用进程被杀后再次进入时,就会出现登录信息被清空的情况。关于在测试机上无法复现的问题。经过验证,发现这个问题只在系统5.0版本以下出现。看来5.0之后应该是做了处理。

探索

Bug是处理完了,但我们一向提倡要知其所以然。于是我写了一个Demo,看看到底发生了什么。界面很简单,只有两个按钮:

界面

Activity代码如下:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //创建一个名为fenglx的SharedPreferences,模式为MODE_PRIVATE
        SharedPreferences sharedPreferences = getSharedPreferences("fenglx" , MODE_PRIVATE);
        final SharedPreferences.Editor editor= sharedPreferences.edit();
        //按钮1
        findViewById(R.id.write_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //分别存入3个正常Key-value的测试值
                editor.putString("key1","value1");
                editor.putString("key2","value2");
                editor.putString("key3","value3");
                editor.commit();
            }
        });
        //按钮2
        findViewById(R.id.write_null_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //存入Key值为null的测试值
                editor.putString(null ,"value4");
                editor.commit();
            }
        });
    }
}

调用getSharedPreferences()后,会在/data/data/包名/shared_prefs目录下创建一个xml,用于持久化数据。通过adb命令,可以查看这些xml文件,以便观察不同操作下的数据变化。
Demo应用运行后,第一步先在Shared中存入3个测试数据。接下来,用adb操作打开名为fenglx.xml的文件,命令如下:

命令行截图

可以看到,我之前存储的三条数据都在文件中。然后我继续写入Key为null的数据,再次进行查看:
命令行截图

新数据被成功保存,但是没有key值,同时在Shared中能够获取到数据。但是,当我kill掉应用进程重新进入时,Shared中就取不到任何数据了。接着,我又增加了一种情况。在Kill进程,重新进入应用后,再次向Shared写入数据,发现xml中原来的数据被新数据覆盖了。讲的比较乱,为方便理解,用以下表格表示,在存入key为null后,发生的变化:

不退出应用 Kill进程重新进入 Kill进程,写入新数据
xml中 数据都在 数据都在 只有新数据
代码读取 数据都在 读取不到 只有新数据

根据以上情况,我得出这样的结论。在程序中,如果每次Shared读取,都去解析xml,显然耗时费力。通过源码可知,Shared在运行时,存储的数据会放在Map中。由此可见,应用启动时,程序会将xml解析加载到内存,映射成Map。而之后的读写,都是对内存上Map对象的操作。只有数据需要更新时,才会操作xml。
出现Shared数据丢失,很可能就是xml没有成功加载到内存,之后的操作又抹掉了xml中的原有数据。从而引发了像“登录状态被清除”的Bug。

控制台异常

十分凑巧,控制台的一段错误信息帮我定位到了读取xml的源码,一言不合就上源码,查看源码的方式有很多,我习惯使用grepcode在线查看,它有着强大的搜索功能。
接下来,我通过源码来验证之前的想法,先以4.4.4源码为例:

551    public static final HashMap More readThisMapXml(XmlPullParser parser, String endTag, String[] name)
552    throws XmlPullParserException, java.io.IOException
553    {
554        HashMap map = new HashMap();
555
556        int eventType = parser.getEventType();
557        do {
558            if (eventType == parser.START_TAG) {
559                Object val = readThisValueXml(parser, name);
560                if (name[0] != null) {//!!!关键代码!!!
561                    //System.out.println("Adding to map: " + name + " -> " + val);
562                    map.put(name[0], val);
563                } else {
564                    throw new XmlPullParserException(
565                        "Map value without name attribute: " + parser.getName());
566                }
567            } else if (eventType == parser.END_TAG) {
568                if (parser.getName().equals(endTag)) {
569                    return map;
570                }
571                throw new XmlPullParserException(
572                    "Expected " + endTag + " end tag at: " + parser.getName());
573            }
574            eventType = parser.next();
575        } while (eventType != parser.END_DOCUMENT);
576
577        throw new XmlPullParserException(
578            "Document ended before " + endTag + " end tag");
579    }

关键代码部分,对Key进行了判空处理,name[0] == null时,直接抛出了XmlPullParserException异常。
那么5.0是否进行容错处理呢,接下来是5.0源码:

774     public static final HashMap<String, ?> More ...readThisMapXml(XmlPullParser parser, String endTag,
775             String[] name, ReadMapCallback callback)
776             throws XmlPullParserException, java.io.IOException
777     {
778         HashMap<String, Object> map = new HashMap<String, Object>();
779 
780         int eventType = parser.getEventType();
781         do {
782             if (eventType == parser.START_TAG) {
783                 Object val = readThisValueXml(parser, name, callback);
784                 map.put(name[0], val);
785             } else if (eventType == parser.END_TAG) {
786                 if (parser.getName().equals(endTag)) {
787                     return map;
788                 }
789                 throw new XmlPullParserException(
790                     "Expected " + endTag + " end tag at: " + parser.getName());
791             }
792             eventType = parser.next();
793         } while (eventType != parser.END_DOCUMENT);
794 
795         throw new XmlPullParserException(
796             "Document ended before " + endTag + " end tag");
797     }

真相大白,5.0源码中取消了if(name[0] != null)这段判空逻辑。所以,Key为null时,不会影响数据加载到内存。

问题总结

总结一下,两个版本源码唯一的差别在于,解析xml时,4.4.4版本对Key值进行了判空,如果存在null值,数据则不能顺利加载到内存。继而引发一个更严重的问题,原有数据无法加载到内存,新的数据存储操作会基于全新Map,写入xml时便会导致原有数据被抹去。数据的丢失是灾难性的。所以Google在5.0以后,修复了这个问题。
SharedPreferences是十分常用的数据持久化方式,开发人员应该避免使用null作为Key,即便这样做合法。在这个案例中,由于我们的疏忽,忽略了代码的健壮性。希望大家在开发时,注意这个问题,避免“因小失大”。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 前几天整理了Java面试题集合,今天再来整理下Android相关的面试题集合.如果你希望能得到最新的消息,可以关注...
    Boyko阅读 3,621评论 8 135
  • 夜深人静时,是一个心中满怀心事的人最难熬的时分,淡淡月光,总能勾起心中无数的回忆。 独自站在自家小院内,抬头仰望...
    目见阅读 291评论 0 4