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在各方面都是比较完备和可靠的文件共享机制。


原文链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容