As mentioned in Pawan's answer, AtomicFile
is recommended for writing file safely.
But you still need to be careful when using it, sometimes it's not that 'atomic'.
You can find AtomicFile
in android.util
, android.support.v4.util
and androidx.core.util
. But their implementation are DIFFERENT in different versions of API level
/ support library
/ androidx library
, so please pay attention to your dependency version before using it.
In general, you should use the latest version of androidx's AtomicFile. It's in androidx.core:core
, and dependenced by androidx.appcompat:appcompat
.
Let's see what actually happens when we try to write a file with AtomicFile
in lower versions.
API level: [17, 29] / support library: [22.1.0, 28.0.0] / androidx core: [1.0.0, 1.3.2]
public FileOutputStream startWrite() throws IOException {
// Step 1: rename base file to backup file, or delete it.
if (mBaseName.exists()) {
if (!mBackupName.exists()) {
if (!mBaseName.renameTo(mBackupName)) {
Log.w("AtomicFile", "Couldn't rename file " + mBaseName
+ " to backup file " + mBackupName);
}
} else {
mBaseName.delete();
}
}
// At this moment,the base file does not exist anyway.
// Step 2: open output stream to base file.
FileOutputStream str;
try {
str = new FileOutputStream(mBaseName);
} catch (FileNotFoundException e) {
// ...
}
return str;
}
As you can see, AtomicFile
prefers to back up the base file before writing it.
This can indeed provide an effective means of recovery when writing to the base file fails. But you can only say that the write operation is safely, not atomically.
Imagine what happens if the system loses power between steps 1 and 2, the base file will be lost, and only the backup file will remain.
Also, because the FileOutputStream
points to the base file, writing is not atomically either.
But google has improved the implementation of AtomicFile in high versions.
API level: 30+ / androidx core: 1.5.0+
public FileOutputStream startWrite() throws IOException {
// Step 1: recover backup file, which was used in old version.
if (mLegacyBackupName.exists()) {
rename(mLegacyBackupName, mBaseName);
}
// Step 2: open output stream to new file, not the base file.
try {
return new FileOutputStream(mNewName);
} catch (FileNotFoundException e) {
// ...
}
}
The improved AtomicFile takes another approach: when you write content to the output stream returned by startWrite()
, you actually write to a new file (a temporary file with suffix of .new
), and the new file will be rename to base file after finishWrite()
called.
Now, the content of the base file will only have two cases:
- The writing fails, and the content of base file remains unchanged.
- The writing is successful.
It's real atomic now.