一、问题背景
当一个App中存在多个进程时例如存在 主进程,辅进程两个进程,两个进程都会去向A文件中写入数据。但是我们业务中希望每次仅允许有一个进程向A文件写入内容。即当主进程写入时,辅进程要等待主进程写完之后才可以写入,防止出现并发修改导致数据异常的问题。
在实际的场景上,例如在我们的项目中未使用MMKV之前,KV存储是自行实现的多进程并发的SP。
二、实现方案
1、方案1:仅一个进程负责写
将所有的写入操作调整到同一个进程中,这样就相当于规避了多进程并发问题。
我们可以通过提供一个ContantProvider或者Service来是实现这个功能。
以下是使用ContentProvider的方式:
FileProvider
java
public class FileProvider extends ContentProvider {
private static final String AUTHORITY = "com.example.fileprovider";
private static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/file");
// 文件锁,确保单进程写入
private static final Object fileLock = new Object();
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
return null; // 不提供查询功能
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null; // 不提供插入功能
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0; // 不提供删除功能
}
@Override
public int update(Uri uri, ContentValues values,
String selection, String[] selectionArgs) {
return 0; // 不提供更新功能
}
// 自定义方法:写入文件
@Override
public Bundle call(String method, String arg, Bundle extras) {
if ("writeToFile".equals(method)) {
String content = extras.getString("content");
synchronized (fileLock) {
writeToFile(content);
}
Bundle result = new Bundle();
result.putBoolean("success", true);
return result;
}
return super.call(method, arg, extras);
}
// 实际写入文件的逻辑
private void writeToFile(String content) {
File file = new File(getContext().getFilesDir(), "A.txt");
try (FileOutputStream fos = new FileOutputStream(file, true)) {
fos.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
注册
ini
<provider
android:name=".FileProvider"
android:authorities="com.example.fileprovider"
android:exported="true" />
写入逻辑
java
private void writeToFileViaProvider(String content) {
Uri uri = Uri.parse("content://com.example.fileprovider/file");
ContentResolver resolver = getContentResolver();
Bundle extras = new Bundle();
extras.putString("content", content);
try {
Bundle result = resolver.call(uri, "writeToFile", null, extras);
if (result != null && result.getBoolean("success")) {
Log.d("FileProvider", "Write successful");
}
} catch (Exception e) {
Log.e("FileProvider", "Failed to write file", e);
}
}
使用Service + Binder的方式,代码比较简单,这里就不写了。
2、方案2:通过文件锁的方式
文件锁主要是利用FileChannel、FileLock来控制多进程并发。
关于 Channel
Channel 经常翻译为通道,类似 IO 中的流,用于读取和写入。不用像BIO那样,读数据和写数据需要不同的数据通道。
java
public interface Channel extends Closeable {
/**
* Tells whether or not this channel is open.
*
* @return <tt>true</tt> if, and only if, this channel is open
*/
public boolean isOpen();
/**
* Closes this channel.
*/
public void close() throws IOException;
}
我们常用的Channel有:
- FileChannel:文件通道,用于文件的读和写。
- DatagramChannel:用于UDP连接的接收和发送。
- SocketChannel:把它理解为TCP连接通道,简单理解就是TCP客户端。
- ServerSocketChannel:TCP对应的服务端,用于监听某个端口进来的请求。
FileChannel
FileChannel 是 Java NIO (New I/O) 中的一个类,用于对文件进行高效的读写操作。它提供了比传统 FileInputStream
和 FileOutputStream
更灵活和高效的文件操作方式。
所有的NIO操作始于通道,通道是数据来源或数据写入的目的地。其与 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
FileChannel的获取方式:
通过FileInputStream/FileOutputStream
java
// 通过 FileInputStream/FileOutputStream (只读或只写)
FileInputStream fis = new FileInputStream("file.txt");
FileChannel readChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("file.txt");
FileChannel writeChannel = fos.getChannel();
通过RandomAccessFile
java
// 通过 RandomAccessFile
RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");
FileChannel channel = raf.getChannel();
通过FileChannel.open()
java
FileChannel channel = FileChannel.open(Paths.get("file.txt"), StandardOpenOption.READ);
在我们示例代码中选择了使用 FileOutputStream
来获取FileChannel。
FileLock
FileLock
表示文件或文件区域的锁,用于控制多个进程或线程对同一文件的并发访问。
锁的类型
- 共享锁 (Shared Lock) :多个进程可同时持有,用于读操作
- 排他锁 (Exclusive Lock) :一次只能由一个进程持有,用于写操作
通过文件锁的方式控制多进程并发的 示例代码:
java
public class FileWriter {
private static final String FILE_PATH = "/path/to/your/file.txt";
public void writeToFile(String content) {
File file = new File(FILE_PATH);
try {
FileOutputStream fos = new FileOutputStream(file, true);
FileChannel channel = fos.getChannel())
// 获取独占锁
FileLock lock = channel.lock();
try {
// 写入文件
fos.write(content.getBytes());
} finally {
// 释放锁
lock.release();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、总结
以上简单介绍了一下两种控制多进程并发的方案。
其中使用ContentProvider或者Service的方式将所有的操作控制在同一个进程中的方案逻辑清晰,但是代码量比较多。尤其是使用Service的方式,虽然上面我们每个给出示例代码,但是可以想象没新增一个进程都需要写相关的代码,写起来就比较啰嗦了。
而ContentProvicer的方式,系统中也有很多相关的实现方案,例如更新媒体文件,更新联系人数据等。
使用文件锁的方式对于仅熟悉Android不熟悉Java的同学不容易想到,所以本篇也同时简单介绍了一下FileChannel以及FileLock。