sequence

This week I read an article in Android Blog Weekly, Please don’t abuse Share Preference. I happened to encounter a bug about SharedPreference this week, which took me some time to find out, so I wrote an article here to record the bug.

1. The bug again

First of all, let me talk about the origin of this bug and simulate the bug scene through a demo.

There is a XX cloud disk module in my product, and part of the login information is recorded in SharedPrefrence. One day, one of the test students brought me a mobile phone and told me that after logging in, the previous login information would be lost if the app was restarted after the killing process. At that time, I was confused, and then used to observe the phenomenon, the stranger thing is: After logging in, I found that the login information was indeed saved in SharedPreference, and it was still there after I killed the process, but when I opened the app again, I found that some of the data in SharedPreference had been cleaned (note that it was not completely cleaned, but partially).

From the phenomenon, people feel confused. At the same time, there is no such strange phenomenon on some mobile phones, which makes me feel that the ROM of this mobile phone is not bad. But as a developer, I can’t make such a leap of faith, so I wrote a demo to test it and found none of the above problems. Then you can be sure that this is my own app logic has a bug! But I searched the code globally and couldn’t find any logic to delete the SharedPreference value, so the trail was broken again.

Confused, I had to turn to the SharedPreference file itself, when I noticed a strange phenomenon that my SharedPreference had a key value of NULL. The key is null? As a developer, I’m still very sensitive to null Pointers, so I wondered if the null key was causing this. Here’s a demo that shows the truth:

public class MainActivity extends FragmentActivity implements View.OnClickListener { Button saveOne, saveTwo, getOne, getTwo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); saveOne = (Button) findViewById(R.id.saveone); saveTwo = (Button) findViewById(R.id.savetwo); getOne = (Button) findViewById(R.id.getOne); getTwo = (Button) findViewById(R.id.getTwo); saveOne.setOnClickListener(this); saveTwo.setOnClickListener(this); getOne.setOnClickListener(this); getTwo.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.saveone : putString("one", "saveone"); break; case R.id.savetwo : putString(null, "savetwo"); break; case R.id.getOne : Toast.makeText(this, getString("one"), Toast.LENGTH_LONG) .show(); break; case R.id.getTwo : Toast.makeText(this, getString(null), Toast.LENGTH_LONG).show(); break; } } public void putString(String key, String value) { SharedPreferences sharedPreferences = getSharedPreferences("test", MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(key, value); editor.commit(); } public String getString(String key) { SharedPreferences sharedPreferences = getSharedPreferences("test", MODE_PRIVATE); return sharedPreferences.getString(key, "null"); }}Copy the code

Store a normal key and get the value:

Store a value with key null and get:

Now I kill the process and restart the demo, and something weird happens:

After killing the process, I went to check the values and found that they were still there

But I can’t get any values

When I click “store a null key in preference” and then look at the SharedPreference data, I find that all the data is missing:

It turns out that the data was wiped after killing the process because a null key was stored.

2 analysis the bug

Now that the problem has been located, it can be easily solved by finding the null key and assigning it to it. However, as a developer who has high requirements for himself, it is necessary to study the whole process of the bug. First of all, I would like to analyze the cause of this bug from the aspect of phenomena. When I failed to get the value after I killed the process to enter the APP for the second time, I posted the following log on logcat:

10-29 22:54:37. 099, 10964-11037 /? W/SharedPreferencesImpl: getSharedPreferences org.xmlpull.v1.XmlPullParserException: Map value without name attribute: string at com.android.internal.util.XmlUtils.readThisMapXml(XmlUtils.java:568) at com.android.internal.util.XmlUtils.readThisValueXml(XmlUtils.java:821) at com.android.internal.util.XmlUtils.readValueXml(XmlUtils.java:755) at com.android.internal.util.XmlUtils.readMapXml(XmlUtils.java:494) at android.app.SharedPreferencesImpl.loadFromDiskLocked(SharedPreferencesImpl.java:113) at android.app.SharedPreferencesImpl.access$000(SharedPreferencesImpl.java:48) at android.app.SharedPreferencesImpl$1.run(SharedPreferencesImpl.java:87)Copy the code

Also, this exception will only be printed if I miss the value the first time I get it, and will not be printed no matter how many times I miss it. A Map value with no attribute is obtained, roughly literally. According to the article I mentioned at the beginning, each value of SharedPreference is read one by one in the SharedPreference file and stored in the memory at the first get, and then every GET operation is taken out from the memory.

Based on this inference, I venture to assume that the reason for null is because we failed to parse the XML file for the first time (throwing an exception), so that the map we pulled out of memory is null, so that we will value null every time. The later values are “partially erased” because we wrote the memory values directly to disk, overwriting the original values. As for some phones that do not have this problem, I guess android has implemented fault tolerance for this problem since 5.0, that is, when a value is found without a key, it can also write a value to memory, but the corresponding key is null.

3. Test your conjecture

In order to verify the conjecture before, it is necessary to go deep into android source code research. Here we compare 4.4 code with 5.1 code. First let’s take a look at the source code for 4.4 based on where the exception is thrown:

public static final HashMap readThisMapXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { HashMap map = new HashMap(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { Object val = readThisValueXml(parser, name); if (name[0] ! = null) { map.put(name[0], val); } else { throw new XmlPullParserException( "Map value without name attribute: " + parser.getName()); } } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { return map; } throw new XmlPullParserException( "Expected " + endTag + " end tag at: " + parser.getName()); } eventType = parser.next(); } while (eventType ! = parser.END_DOCUMENT); throw new XmlPullParserException( "Document ended before " + endTag + " end tag"); }Copy the code

When name is null, an exception is thrown, which is exactly what I saw on logcat. Now let’s look at the 5.1 code:

public static final HashMap readThisMapXml(XmlPullParser parser, String endTag, String[] name, ReadMapCallback callback) throws XmlPullParserException, java.io.IOException { HashMap map = new HashMap(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { Object val = readThisValueXml(parser, name, callback); map.put(name[0], val); } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { return map; } throw new XmlPullParserException( "Expected " + endTag + " end tag at: " + parser.getName()); } eventType = parser.next(); } while (eventType ! = parser.END_DOCUMENT); throw new XmlPullParserException( "Document ended before " + endTag + " end tag"); }Copy the code

The obvious difference is that the branch whose name is null is not processed. Instead, the value is read and written to the map. So why do you “delete” some of the values when you start the APP again after killing the process? Let’s move on to the source code:

SharedPreferencesImpl.java

private MemoryCommitResult commitToMemory() { MemoryCommitResult mcr = new MemoryCommitResult(); synchronized (SharedPreferencesImpl.this) { if (mDiskWritesInFlight > 0) { mMap = new HashMap(mMap); } mcr.mapToWriteToDisk = mMap; mDiskWritesInFlight++; Omit the following code....Copy the code

The 4.4 code is almost identical to the 5.1 code in this area, so the reason 4.4 will delete the value is that it has not been read from disk into memory. When a value is copied, it is “lost”, resulting in no old value being written to the new file, which looks like some value has been deleted, but should actually be overwritten by null values.

4 summarizes

In general, the root cause of this problem is that the 4.x SDK allows you to write a value with a null key into the SharedPreferences without taking it out, and not only cannot take it out, but also causes the disastrous effect of “deleting” data in the process of writing. This was extremely unreasonable, so Google fixed this logic in a subsequent SDK. However, as an application layer developers, but also need to review such low-level mistakes from their own. After all, the consequences of this mistake are very serious, but it is colorless, tasteless and difficult to find. In the future, we should try our best to avoid such mistake happening again.

I believe that through such a phenomenon from the analysis of the problem to tracking, and then to the SDK source comparison between the differences between versions, for their level of improvement or a lot of benefits, I hope to be able to analyze some more in-depth problems in the future. Of course, the last thing we want to say is that we should start from ourselves, try to make fewer low-level mistakes and bugs, and be a high-level engineer rather than a code farmer with debugging all day long.