目录
- FileProvider的基本面
- 最小原型
- 源应用各项配置的说明
- 怎么实现端对端的uri传递
- FileProvider的展开
- 权限管理
- 多个FileProvider并存
- 自定义Uri格式
- FileProvider的深入
- FileProvider文件共享的本质
- FD跨进程传输
- FileProvider以外的FD跨进程传递
第一、二章见上一篇《FileProvider学习笔记之一》。
FileProvider的深入
FileProvider文件共享的本质
假设目标应用通过ContentResolver#openInputStream()
方法访问文件:
public final @Nullable InputStream openInputStream(@NonNull Uri uri) throws FileNotFoundException {
String scheme = uri.getScheme();
if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
//...
} else if (SCHEME_FILE.equals(scheme)) {
//...
} else {
AssetFileDescriptor fd = openAssetFileDescriptor(uri, "r", null);
//...
}
}
跟随如上代码继续阅读,最终能调用到ContentProvider#openTypedAssetFile()
方法:
public @Nullable AssetFileDescriptor openTypedAssetFile(@NonNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts) throws FileNotFoundException {
//...
return openAssetFile(uri, "r");
//...
}
ContentProvider#openAssetFile()
方法:
1710 public @Nullable AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode)
1711 throws FileNotFoundException {
1712 ParcelFileDescriptor fd = openFile(uri, mode);
1713 return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
1714 }
FileProvider#openFile()
方法:
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
final int fileMode = modeToMode(mode);
return ParcelFileDescriptor.open(file, fileMode);
}
最终收敛到ParcelFileDescriptor.open(file, fileMode)
方法上。目标应用通过其他接口访问文件最终基本都收敛到此方法上。
根据上述分析,可以得到以下结论:
- 文件共享的本质不是跨进程传输文件字节流,而是跨进程传输FD;
- FD可以通过binder传输,且保障了FD跨进程可用;
FD跨进程传输
Linux的FD跟Windows的Handle本质上类似,都是对进程中一个指针数组的索引,这个指针数组的每个元素分别指向了一个内核态数据。
每个进程都有自己的指针数组,不同进程的指针数据一个各不相同,所以FD的值只有在进程内有意义,另一个进程的相同FD取值可能指向的完全是另一个对象,或根本没有指向任何对象(野指针或空指针)。所以直接跨进程传送FD的值是没有意义的。
可以推测binder可能对FD做了特殊处理。这一推测可以从binder.c
的binder_transaction
函数中(源码)找到证据:
static void binder_transaction(struct binder_proc *proc, struct binder_thread *thread, struct binder_transaction_data *tr, int reply) {
//...
switch (fp->type) {
//...
case BINDER_TYPE_FD: {
int target_fd;
struct file *file;
//...
file = fget(fp->handle); // 1. 用源进程的FD获得file*
//...
security_binder_transfer_file(proc->tsk, target_proc->tsk, file);
//...
target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC); // 2. 在目标进程分配新的FD
//...
task_fd_install(target_proc, target_fd, file); // 3. 把file*赋值给新FD
//...
fp->handle = target_fd; // 4. 把新FD发给目标进程
} break;
//...
}
//...
}
binder通过上面代码中的4个关键步骤,在目标进程分配新的FD并让其指向内核的file
对象。FD的跨进程传递的本质是file
对象指针的跨进程传递。
关于上述步骤的相关源码可参考:
- fget → __fget → fcheck_files参考
- task_get_unused_fd_flags → __alloc_fd → __set_open_fd
- task_fd_install → __fd_install
上述源码中还调用了security_binder_transfer_file
函数,本质上是对selinux_binder_transfer_file
函数的调用。该函数负责校验进程双方是否具有传递此FD的权限,更多介绍可参考罗升阳的《SEAndroid安全机制对Binder IPC的保护分析》,这里不做展开。
FileProvider以外的FD跨进程传递
既然FD是通过binder保障了跨进程传递,那么FileProvider
就不是文件共享的唯一途径,其他基于binder的IPC方法应该也可以传递FD,例如:
- 通过
Intent
调用四大组件,如Activity
、Service
、BroadcastReceiver
; - 通过
FileProvider
以外的ContentProvider
; - 通过
aidl
调用;
aidl调用
例如有如下aidl
:
interface ISampleAidl {
void sendFile(in ParcelFileDescriptor fd);
ParcelFileDescriptor recvFile();
}
通过入参可以从主调进程把FD传递给被调进程;通过返回值可以从被调进程把FD传递给主调进程。除了直接在入参和返回值使用ParcelFileDescriptor
,还可以通过其他Parcelable
类型(如Bundle
等)携带FD。
FileProvider以外的ContentProvider
例如在源应用做如下自定义Provider:
public class MyContentProvider extends ContentProvider {
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
File file = ...;
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
}
在目标应用有类似如下调用:
Uri uri = ...;
try (ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(uri, "r")) {
readFromFD(fd);
} catch (IOException e) {
e.printStackTrace();
}
本质上FileProvider
就是基于此模型而封装的库。
通过Intent
调用四大组件
调用四大组件的方法有但不限于以下方法:
Context#startActivity(Intent)
Context#startService(Intent)
Context#sendBroadcast(Intent)
例如通过如下方法携带FD:
ParcelFileDescriptor fd = ...;
Intent intent = new Intent();
//...
intent.putExtra("file", fd);
startActivity(intent); // RuntimeException: Not allowed to write file descriptors here
意外的是,调用startActivity
的时候发生了异常:
java.lang.RuntimeException: Not allowed to write file descriptors here
at android.os.Parcel.nativeWriteFileDescriptor(Native Method)
at android.os.Parcel.writeFileDescriptor(Parcel.java:809)
at android.os.ParcelFileDescriptor.writeToParcel(ParcelFileDescriptor.java:1057)
at android.os.Parcel.writeParcelable(Parcel.java:1801)
at android.os.Parcel.writeValue(Parcel.java:1707)
at android.os.Parcel.writeArrayMapInternal(Parcel.java:928)
at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1584)
at android.os.Bundle.writeToParcel(Bundle.java:1253)
at android.os.Parcel.writeBundle(Parcel.java:997)
at android.content.Intent.writeToParcel(Intent.java:10495)
at android.app.IActivityManager$Stub$Proxy.startService(IActivityManager.java:5153)
at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1601)
at android.app.ContextImpl.startService(ContextImpl.java:1571)
at android.content.ContextWrapper.startService(ContextWrapper.java:669)
at android.content.ContextWrapper.startService(ContextWrapper.java:669)
看起来并不是binder不允许传递FD。下面分析相关源码,尝试寻找原因和突破点。先从抛出异常的代码开始。
/* http://aospxref.com/android-10.0.0_r47/xref/frameworks/base/core/jni/android_util_Binder.cpp#814 */
void signalExceptionForError(JNIEnv* env, jobject obj, status_t err, bool canThrowRemoteException, int parcelSize) {
switch (err) {
//...
case FDS_NOT_ALLOWED:
jniThrowException(env, "java/lang/RuntimeException", "Not allowed to write file descriptors here");
break;
//...
}
}
/* http://aospxref.com/android-10.0.0_r47/xref/frameworks/native/libs/binder/Parcel.cpp#553 */
status_t Parcel::appendFrom(const Parcel *parcel, size_t offset, size_t len) {
//...
if (!mAllowFds) {
err = FDS_NOT_ALLOWED;
}
//...
return err;
}
看得出跟属性mAllowFds
的设置有关。设置mAllowFds
的代码在:
/* http://aospxref.com/android-10.0.0_r47/xref/frameworks/native/libs/binder/Parcel.cpp#575 */
bool Parcel::pushAllowFds(bool allowFds) {
const bool origValue = mAllowFds;
if (!allowFds) {
mAllowFds = false;
}
return origValue;
}
/* http://aospxref.com/android-10.0.0_r47/xref/frameworks/base/core/java/android/os/Bundle.java#1250 */
public void writeToParcel(Parcel parcel, int flags) {
final boolean oldAllowFds = parcel.pushAllowFds((mFlags & FLAG_ALLOW_FDS) != 0);
try {
super.writeToParcelInner(parcel, flags);
} finally {
parcel.restoreAllowFds(oldAllowFds);
}
}
跟踪标志位FLAG_ALLOW_FDS
的设置:
/* http://aospxref.com/android-10.0.0_r47/xref/frameworks/base/core/java/android/os/Bundle.java#204 */
public boolean setAllowFds(boolean allowFds) {
final boolean orig = (mFlags & FLAG_ALLOW_FDS) != 0;
if (allowFds) {
mFlags |= FLAG_ALLOW_FDS;
} else {
mFlags &= ~FLAG_ALLOW_FDS;
}
return orig;
}
Bundle#setAllowFds(false)
在多处代码有调用,如:Intent#prepareToLeaveProcess
、LoadedApk$ReceiverDispatcher#performReceive
、BroadcastReceiver$PendingResult#sendFinished
、等,其中Intent#prepareToLeaveProcess
被Instrumentation#execStartActivity
调用。
上面涉及的代码都属于非公开接口,应用程序不应该调用。由此可知Android不希望在四大组件调用过程中传递FD,故意设置了门槛。
通过Bundle#putBinder
突破Intent
和四大组件的限制
已知通过aidl
可以传递FD,且Bundle
类有putBinder
方法可以传递IBinder
,那么不妨发一个IBinder
到目标进程,然后用这个IBinder
传递FD。虽然本质上还是aidl
的调用,但可以不用依赖bindService
等方法建立连接,而是通过Intent
直接发到目标进程。
首先定义aidl
:
interface IFileBinder {
ParcelFileDescriptor openFileDescriptor(int mode);
}
实现IFileBinder
:
public class FileBinder extends IFileBinder.Stub {
final File mFile;
public FileBinder(File file) {
mFile = file;
}
@Override
public ParcelFileDescriptor openFileDescriptor(int mode) throws RemoteException {
try {
return ParcelFileDescriptor.open(mFile, mode);
} catch (FileNotFoundException e) {
throw new RemoteException(e.getMessage());
}
}
}
发送FD:
File file = ...;
Bundle bundle = new Bundle();
bundle.putBinder("file", new FileBinder(file).asBinder());
Intent intent = new Intent(/*...*/);
intent.putExtras(bundle);
startActivity(intent);
接收FD:
Bundle bundle = intent.getExtras();
IBinder binder = bundle.getBinder("file");
IFileBinder fileBinder = IFileBinder.Stub.asInterface(binder);
ParcelFileDescriptor fd = fileBinder.openFileDescriptor(ParcelFileDescriptor.MODE_READ_ONLY);
//...
通过IPC发送FD的结论
上面例举了若干跨进程传递FD的方法,相互各有优劣。如要从上述各方法中做选择,可以至少从以下几点来考虑:
- 源应用是否需要定制;
- 目标应用是否需要定制;
- 目标应用是否需要对FD做细粒度的控制;
- 源应用是否需要对目标应用做权限校验和控制;
- 代码的易维护性和易扩展性;
综合来说,FileProvider
在各方面都是比较完备和可靠的文件共享机制。