美文网首页安卓rom源码分析Android知识Android开发经验谈
一个Null值引发的神奇Bug,安卓SharedPreferen

一个Null值引发的神奇Bug,安卓SharedPreferen

作者: 让我来试试 | 来源:发表于2017-08-03 11:40 被阅读3511次

    现象

    最近项目中出现一个诡异的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,即便这样做合法。在这个案例中,由于我们的疏忽,忽略了代码的健壮性。希望大家在开发时,注意这个问题,避免“因小失大”。

    相关文章

      网友评论

      • 梦想编织者灬小楠:这个问题好像之前也遇到过。除此之外,SP储存应该尽量避免为null的value,4.X版本会保存在SP的xml文件里,5.0以上版本则不会保存这个键值对...
        让我来试试:@梦想编织者灬小楠 你说的很对:+1:我们更提倡关注内部过程。
      • 钱八斤:记住了
        让我来试试:@钱八斤 :smile:
      • 26798758d52a:去年还是什么时候看到过一个人写的文章,跟你的是一模一样的,都是在SharedPreference中key为null,导致Bug。
        让我来试试:@刘义_ca5e 是啊,这个问题有一定隐蔽性,之前对Shared 也没深入了解过。

      本文标题:一个Null值引发的神奇Bug,安卓SharedPreferen

      本文链接:https://www.haomeiwen.com/subject/dygelxtx.html