基于磁盘文件的跨进程通信机制详解
文件共享 IPC(进程间通信)的原理非常简单:一个进程将数据序列化后写入文件,另一个进程从同一个文件读取并反序列化。
它不依赖 Android 框架特有的 Binder 机制,因此数据量不受 1MB 限制,非常适合传输大文件(视频、图片、大 JSON)。
媒介:磁盘上的持久化文件,或基于共享内存的临时文件(如 Ashmem)。
无主动通知:文件本身不会主动告诉读取方"数据已就绪",需要额外引入轮询、文件观察者、广播等通知机制。
必须处理并发:多进程同时读写需要文件锁、原子写入等保护手段。
fun writeDataToFile(targetDir: File, fileName: String, data: ByteArray) {
val tempFile = File(targetDir, "$fileName.tmp")
val targetFile = File(targetDir, fileName)
// 流式写入(即使 data 不大,也习惯用流)
FileOutputStream(tempFile).use { it.write(data) }
// 原子替换,防止读到不完整文件
tempFile.renameTo(targetFile)
}
fun readDataFromFile(targetDir: File, fileName: String): ByteArray? {
val targetFile = File(targetDir, fileName)
return if (targetFile.exists()) targetFile.readBytes() else null
}
对于上百 MB 甚至 GB 级别的文件,绝对不能一次性加载到内存,必须使用缓冲区流式搬运。
suspend fun copyStreamToFile(
inputStream: InputStream,
targetDir: File,
fileName: String,
bufferSize: Int = 8 * 1024
) = withContext(Dispatchers.IO) {
val tempFile = File(targetDir, "$fileName.tmp")
val targetFile = File(targetDir, fileName)
try {
FileOutputStream(tempFile).use { output ->
inputStream.use { input ->
val buffer = ByteArray(bufferSize)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
}
}
}
tempFile.renameTo(targetFile) // 原子替换
} catch (e: Exception) {
tempFile.delete()
throw e
}
}
renameTo在同文件系统下是原子操作,瞬间完成替换当需要跨进程共享一个巨大的对象列表时,绝不能把所有对象转成内存 List,再一次性序列化成巨大的 JSON 字符串,那会直接 OOM。
应该一边获取数据,一边直接写文件,内存中永远只保留一条数据的大小。
suspend fun writeBooksAsJson(
booksFlow: Flow, // 一条一条发射数据
targetDir: File,
fileName: String
) = withContext(Dispatchers.IO) {
val tempFile = File(targetDir, "$fileName.tmp")
val targetFile = File(targetDir, fileName)
tempFile.bufferedWriter().use { writer ->
val jsonWriter = JsonWriter(writer)
jsonWriter.beginArray()
booksFlow.collect { book ->
// 每收到一本书,立刻写入,不堆积在内存
gson.toJson(book, Book::class.java, jsonWriter)
}
jsonWriter.endArray()
}
tempFile.renameTo(targetFile)
}
Flow<Book>不是列表,而是一条一条 异步发射的数据流emit(book)发一条 → collect收一条 → 处理一条 → 释放内存,再等下一条collect会自动挂起等待,直到所有数据发射完毕才继续执行后续代码suspend fun readBooksFromJson(
targetDir: File,
fileName: String
): Flow<Book> = flow {
val targetFile = File(targetDir, fileName)
val reader = targetFile.bufferedReader()
val jsonReader = JsonReader(reader)
jsonReader.beginArray()
while (jsonReader.hasNext()) {
var title = ""
var author = ""
jsonReader.beginObject()
while (jsonReader.hasNext()) {
when (jsonReader.nextName()) {
"title" -> title = jsonReader.nextString()
"author" -> author = jsonReader.nextString()
else -> jsonReader.skipValue()
}
}
jsonReader.endObject()
emit(Book(title, author)) // 读出一条,发射一条
}
jsonReader.endArray()
jsonReader.close()
}
设计上规定只有进程 A 写入,其他进程只读。最简单安全。
val raf = RandomAccessFile(file, "rw")
val lock = raf.channel.lock() // 阻塞,直到获取到锁
try {
// 执行写操作
} finally {
lock.release()
raf.close()
}
channel.lock()没有超时参数,会无限等待,因此不适合在主线程或长时间 IO 操作中直接使用。
绝对禁忌:网络下载、大文件、视频等长时间 IO,必须改用 tmp + rename原子写入方案。
temp.tmp,多个目标文件并发写入时会互相覆盖fileName参数tempFile.renameTo(targetFile)在同一个文件系统下会瞬间完成:旧文件被删除(或覆盖),新文件名直接指向临时文件的数据。整个过程不可分割,读取方不会看到中间状态。
| 位置 | 路径示例 | 同应用多进程 | 不同应用 | 用户可见 | 需要权限 |
|---|---|---|---|---|---|
| 内部私有存储 | /data/data/包名/files |
不可见 | 否 | ||
| 应用专属外部存储 | /sdcard/Android/data/包名/files/ |
理论可见,路径深 | 否 | ||
| 外部公有存储 | /sdcard/ |
可见,可删除 | 需存储权限 | ||
| ContentProvider+内部存储 | content://... |
不可见 | 否 |
context.getExternalFilesDir(type),无需权限,安全便捷FileProvider或自定义 ContentProvider暴露 URI,文件仍保存在内部私有目录,安全且用户不可见| 方式 | 实现 | 适用场景 |
|---|---|---|
| 轮询 | 定时检查文件是否存在/修改时间 | 简单但耗电,不推荐 |
| FileObserver | 在接收进程注册,监听文件事件 | 高效,但仅限同 UID |
| 广播 | 写完后发送全局/本地广播,携带文件路径 | 跨进程,简单易用 |
| ContentObserver | 配合 ContentProvider | 标准跨应用通知 |
| Messenger / AIDL 回调 | 写入方主动回调接收方 | 高性能,双向通信 |
MutableList<T>是接口,只能用来声明变量类型,不能直接实例化mutableListOf()是一个工厂函数,等价于 ArrayList(),是创建可变列表的最简洁方式val list: MutableList<String> = mutableListOf(),符合"面向接口编程"原则renameTo确保读到的一定是完整数据getExternalFilesDir,跨应用必须用 ContentProvider