这里唯一的问题是我丢失了该文件的文件名。这似乎有点复杂,只是为了从驱动器获取文件路径。有没有更好的方法来做到这一点?
你似乎忽略了一个重要的问题。Linux中的文件不需要有名称。它们可以存在于内存中(例如
android.os.MemoryFile
),甚至可以在没有名称的目录中存在(如使用
O_TMPFILE
标志创建的文件)。但它们确实需要有一个文件描述符。
简要概述:文件描述符比简单文件更好,应该始终使用它们,除非在关闭它们后太过繁琐。如果您可以使用JNI,它们可以用于与
File
对象相同的事情,以及更多其他用途。它们由特殊的ContentProvider提供,并可以通过ContentResolver的
openFileDescriptor
方法访问(该方法接收与目标提供程序关联的Uri)。
也就是说,仅仅建议那些习惯于使用
File
对象的人将其替换为描述符听起来确实很奇怪。如果您想尝试,请阅读下面的详细说明。如果不想,请直接跳到答案底部的“简单”解决方案。
编辑:下面的答案是在Lollipop变得普遍之前编写的。现在有
一个方便的类,可直接访问Linux系统调用,使使用JNI处理文件描述符成为可选项。
描述符简介
文件描述符来自于 Linux 的 open
系统调用和对应的 C 库中的 open()
函数。您不需要访问文件即可操作其描述符。大多数访问检查将被简单地跳过,但某些关键信息,例如访问类型(读/写/读写等)被“硬编码”到描述符中,创建后无法更改。文件描述符由非负整数表示,从0开始。这些数字对每个进程都是本地的,没有任何持久性或系统范围的含义,它们只是为给定进程之间的文件句柄区分开来(0、1和2传统上引用stdin
,stdout
和stderr
)。
每个描述符由对存储在操作系统内核中的描述符表条目的引用来表示。该表的条目数量有每个进程和系统范围的限制,因此请快速关闭您的描述符,除非您希望突然失败尝试打开事物和创建新描述符。
描述符操作
在Linux中,C库函数和系统调用分为两种:一种是通过名称操作的(例如
readdir()
、
stat()
、
chdir()
、
chown()
、
open()
、
link()
),另一种是通过描述符操作的:
getdents
、
fstat()
、
fchdir()
、
fchown()
、
fchownat()
、
openat()
、
linkat()
等。只需阅读几个man手册并学习一些黑暗的JNI技巧,就可以轻松调用这些函数和系统调用。这将大大提高您软件的质量!(如果需要说明:我说的是“阅读”和“学习”,而不是一味地使用JNI。)
在Java中,有一个用于处理描述符的类:
java.io.FileDescriptor
。它
可以与FileXXXStream
类一起使用,从而间接地与所有框架IO类(包括内存映射和随机访问文件、通道和通道锁)一起使用。这是一个棘手的类。由于需要与某些专有操作系统兼容,这个跨平台类不会公开底层整数编号。甚至不能关闭!相反,您应该关闭相应的IO类,这些IO类(出于兼容性原因)彼此共享相同的底层描述符。
FileInputStream fileStream1 = new FileInputStream("notes.db");
FileInputStream fileStream2 = new FileInputStream(fileStream1.getFD());
WritableByteChannel aChannel = fileStream1.getChannel();
// pass fileStream1 and aChannel to some methods, written by clueless people
...
// surprise them (or get surprised by them)
fileStream2.close();
没有支持的方法可以从FileDescriptor
中获取整数值,但是你几乎可以安全地假设,在旧版本的操作系统上有一个私有整数descriptor
字段,可以通过反射访问。
使用描述符射击自己的脚
在Android框架中,有一个专门用于处理Linux文件描述符的类:android.os.ParcelFileDescriptor
。不幸的是,它几乎和FileDescriptor一样糟糕。为什么?有两个原因:
1)它有一个finalize()
方法。阅读它的javadoc以了解对性能的影响。如果您不想面对突然的IO错误,仍然需要关闭它。
2)由于它是可终结的,一旦类实例的引用超出范围,虚拟机将自动关闭它。这就是为什么一些框架类(尤其是MemoryFile
)具有finalize()
的错误之处,这是框架开发人员的失误:
public FileOutputStream giveMeAStream() {
ParcelFileDescriptor fd = ParcelFileDescriptor.open("myfile", MODE_READ_ONLY);
return new FileInputStream(fd.getDescriptor());
}
...
FileInputStream aStream = giveMeAStream();
幸运的是,有一种解决这种恐怖情况的方法:一个神奇的dup
系统调用:
public FileOutputStream giveMeAStream() {
ParcelFileDescriptor fd = ParcelFileDescriptor.open("myfile", MODE_READ_ONLY);
return new FileInputStream(fd.dup().getDescriptor());
}
...
FileInputStream aStream = giveMeAStream();
public FileOutputStream giveMeAStreamProperly() {
try (ParcelFileDescriptor fd = ParcelFileDescriptor.open("myfile", MODE_READ_ONLY)) {
return new FileInputStream(fd.dup().getDescriptor());
}
}
dup
系统调用可以克隆整数文件描述符,使相应的
FileDescriptor
与原始描述符独立。注意,跨进程传递描述符不需要手动复制:接收到的描述符与源进程无关。如果使用反射获取
MemoryFile
的描述符,则传递它
确实需要调用
dup
:在原始进程中销毁共享内存区域将使其对所有人都不可访问。此外,您必须在本地代码中执行
dup
或保留对创建的
ParcelFileDescriptor
的引用,直到接收方完成对您的
MemoryFile
的操作。
传递和接收描述符
有两种方法可以传递和接收文件描述符:通过子进程继承创建者的描述符和通过进程间通信。
在Linux中,让进程的子进程继承创建者打开的文件、管道和套接字是一种常见的做法,但在Android的本机代码中需要使用fork - Runtime.exec()
和 ProcessBuilder
在创建子进程后会关闭所有额外描述符。如果您选择自己进行fork
操作,请确保关闭不必要的描述符。
目前,在Android上支持传递文件描述符的唯一IPC工具是Binder和Linux域套接字。
Binder允许您将ParcelFileDescriptor
提供给任何接受可打包对象的东西,包括将它们放入Bundle中,从内容提供程序返回并通过AIDL调用传递到服务。
注意,大多数试图在进程外传递带有描述符的Bundle的尝试,包括调用
startActivityForResult
都将被系统拒绝,这很可能是因为及时关闭这些描述符会比较困难。更好的选择是创建ContentProvider(它会为您管理描述符生命周期,并通过
ContentResolver
发布文件)或编写AIDL接口并在传输后立即关闭描述符。还要注意,将
ParcelFileDescriptor
存在任何地方都没有太多意义:它只能在进程死亡之前工作,相应的整数一旦重建您的进程就很可能指向其他内容。
域套接字对于描述符传输而言是低级且有点麻烦的,特别是与提供程序和AIDL相比。然而,它们是本机进程的一个很好的(也是唯一的记录)选项。如果您被迫使用原生二进制文件打开文件或移动数据(这通常是使用root权限的应用程序的情况),请考虑不要浪费精力和CPU资源与那些二进制文件进行复杂的通信,而是编写一个开放式助手(open helper)。[厚颜无耻的广告]顺便说一下,您可以使用
我写的,而不是创建自己的。[/ 厚颜无耻的广告]
确切问题的答案
我希望这个答案能让您了解MediaStore.MediaColumns.DATA存在的问题,以及为什么Android开发团队创建这个列是一个误称。
话虽如此,如果您仍然不信服,想要以任何代价获得文件名,或者只是没有阅读上面压倒性的文字墙,请使用这个已准备好的JNI函数;受Getting Filename from file descriptor in C的启发(编辑:现在有一个pure-Java version):
JNIEXPORT jstring Java_com_example_FdUtil_getFdPathInternal(JNIEnv *env, jint descriptor)
{
char buf[PATH_MAX + 1] = { 0 };
char procFile[25];
sprintf(procFile, "/proc/self/fd/%d", descriptor);
if (readlink(procFile, buf, sizeof(buf)) == -1) {
jclass exClass = (*env) -> FindClass(env, "java/io/IOException");
(*env) -> ThrowNew(env, exClass, "readlink() failed");
return NULL;
}
if (buf[PATH_MAX] != 0) {
jclass exClass = (*env) -> FindClass(env, "java/io/IOException");
(*env) -> ThrowNew(env, exClass, "The path is too long");
return NULL;
}
if (buf[0] != '/') {
jclass exClass = (*env) -> FindClass(env, "java/io/IOException");
(*env) -> ThrowNew(env, exClass, "The descriptor does not belong to file with name");
return NULL;
}
return (*env) -> NewStringUTF(env, buf);
}
这里是一个与之相关的类:
public class FdUtil {
static {
System.loadLibrary(System.mapLibraryName("fdutil"));
}
public static String getFdPath(ParcelFileDescriptor fd) throws IOException {
int intFd = fd.getFd();
if (intFd <= 0)
throw new IOException("Invalid fd");
return getFdPathInternal(intFd);
}
private static native String getFdPathInternal(int fd) throws IOException;
}