FileProvider学习笔记之二

目录

  1. FileProvider的基本面
    • 最小原型
    • 源应用各项配置的说明
    • 怎么实现端对端的uri传递
  2. FileProvider的展开
    • 权限管理
    • 多个FileProvider并存
    • 自定义Uri格式
  3. 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)方法上。目标应用通过其他接口访问文件最终基本都收敛到此方法上。

根据上述分析,可以得到以下结论:

  1. 文件共享的本质不是跨进程传输文件字节流,而是跨进程传输FD;
  2. FD可以通过binder传输,且保障了FD跨进程可用;

FD跨进程传输

Linux的FD跟Windows的Handle本质上类似,都是对进程中一个指针数组的索引,这个指针数组的每个元素分别指向了一个内核态数据。

FD内存模型

每个进程都有自己的指针数组,不同进程的指针数据一个各不相同,所以FD的值只有在进程内有意义,另一个进程的相同FD取值可能指向的完全是另一个对象,或根本没有指向任何对象(野指针或空指针)。所以直接跨进程传送FD的值是没有意义的。

可以推测binder可能对FD做了特殊处理。这一推测可以从binder.cbinder_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对象指针的跨进程传递。

FD跨进程传输

关于上述步骤的相关源码可参考:

  1. fget__fgetfcheck_files参考
  2. task_get_unused_fd_flags__alloc_fd__set_open_fd
  3. 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,例如:

  1. 通过Intent调用四大组件,如ActivityServiceBroadcastReceiver
  2. 通过FileProvider以外的ContentProvider
  3. 通过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调用四大组件

调用四大组件的方法有但不限于以下方法:

  1. Context#startActivity(Intent)
  2. Context#startService(Intent)
  3. 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#prepareToLeaveProcessLoadedApk$ReceiverDispatcher#performReceiveBroadcastReceiver$PendingResult#sendFinished、等,其中Intent#prepareToLeaveProcessInstrumentation#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的方法,相互各有优劣。如要从上述各方法中做选择,可以至少从以下几点来考虑:

  1. 源应用是否需要定制;
  2. 目标应用是否需要定制;
  3. 目标应用是否需要对FD做细粒度的控制;
  4. 源应用是否需要对目标应用做权限校验和控制;
  5. 代码的易维护性和易扩展性;

综合来说,FileProvider在各方面都是比较完备和可靠的文件共享机制。


原文链接

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容