目錄
- FileProvider的基本面
- 最小原型
- 源應(yīng)用各項配置的說明
- 怎么實現(xiàn)端對端的uri傳遞
- FileProvider的展開
- 權(quán)限管理
- 多個FileProvider并存
- 自定義Uri格式
- FileProvider的深入
- FileProvider文件共享的本質(zhì)
- FD跨進程傳輸
- FileProvider以外的FD跨進程傳遞
第一膘怕、二章見上一篇《FileProvider學(xué)習(xí)筆記之一》惧磺。
FileProvider的深入
FileProvider文件共享的本質(zhì)
假設(shè)目標(biāo)應(yīng)用通過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);
//...
}
}
跟隨如上代碼繼續(xù)閱讀兆蕉,最終能調(diào)用到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)
方法上玄货。目標(biāo)應(yīng)用通過其他接口訪問文件最終基本都收斂到此方法上专钉。
根據(jù)上述分析捺典,可以得到以下結(jié)論:
- 文件共享的本質(zhì)不是跨進程傳輸文件字節(jié)流娇斩,而是跨進程傳輸FD茅主;
- FD可以通過binder傳輸部念,且保障了FD跨進程可用弃酌;
FD跨進程傳輸
Linux的FD跟Windows的Handle本質(zhì)上類似,都是對進程中一個指針數(shù)組的索引儡炼,這個指針數(shù)組的每個元素分別指向了一個內(nèi)核態(tài)數(shù)據(jù)妓湘。
每個進程都有自己的指針數(shù)組,不同進程的指針數(shù)據(jù)一個各不相同乌询,所以FD的值只有在進程內(nèi)有意義榜贴,另一個進程的相同F(xiàn)D取值可能指向的完全是另一個對象,或根本沒有指向任何對象(野指針或空指針)妹田。所以直接跨進程傳送FD的值是沒有意義的唬党。
可以推測binder可能對FD做了特殊處理鹃共。這一推測可以從binder.c
的binder_transaction
函數(shù)中(源碼)找到證據(jù):
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. 在目標(biāo)進程分配新的FD
//...
task_fd_install(target_proc, target_fd, file); // 3. 把file*賦值給新FD
//...
fp->handle = target_fd; // 4. 把新FD發(fā)給目標(biāo)進程
} break;
//...
}
//...
}
binder通過上面代碼中的4個關(guān)鍵步驟,在目標(biāo)進程分配新的FD并讓其指向內(nèi)核的file
對象驶拱。FD的跨進程傳遞的本質(zhì)是file
對象指針的跨進程傳遞霜浴。
關(guān)于上述步驟的相關(guān)源碼可參考:
- fget → __fget → fcheck_files參考
- task_get_unused_fd_flags → __alloc_fd → __set_open_fd
- task_fd_install → __fd_install
上述源碼中還調(diào)用了security_binder_transfer_file
函數(shù),本質(zhì)上是對selinux_binder_transfer_file
函數(shù)的調(diào)用蓝纲。該函數(shù)負責(zé)校驗進程雙方是否具有傳遞此FD的權(quán)限阴孟,更多介紹可參考羅升陽的《SEAndroid安全機制對Binder IPC的保護分析》,這里不做展開税迷。
FileProvider以外的FD跨進程傳遞
既然FD是通過binder保障了跨進程傳遞永丝,那么FileProvider
就不是文件共享的唯一途徑,其他基于binder的IPC方法應(yīng)該也可以傳遞FD翁狐,例如:
- 通過
Intent
調(diào)用四大組件类溢,如Activity
凌蔬、Service
露懒、BroadcastReceiver
; - 通過
FileProvider
以外的ContentProvider
砂心; - 通過
aidl
調(diào)用懈词;
aidl調(diào)用
例如有如下aidl
:
interface ISampleAidl {
void sendFile(in ParcelFileDescriptor fd);
ParcelFileDescriptor recvFile();
}
通過入?yún)⒖梢詮闹髡{(diào)進程把FD傳遞給被調(diào)進程;通過返回值可以從被調(diào)進程把FD傳遞給主調(diào)進程辩诞。除了直接在入?yún)⒑头祷刂凳褂?code>ParcelFileDescriptor坎弯,還可以通過其他Parcelable
類型(如Bundle
等)攜帶FD。
FileProvider以外的ContentProvider
例如在源應(yīng)用做如下自定義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);
}
}
在目標(biāo)應(yīng)用有類似如下調(diào)用:
Uri uri = ...;
try (ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(uri, "r")) {
readFromFD(fd);
} catch (IOException e) {
e.printStackTrace();
}
本質(zhì)上FileProvider
就是基于此模型而封裝的庫译暂。
通過Intent
調(diào)用四大組件
調(diào)用四大組件的方法有但不限于以下方法:
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
意外的是抠忘,調(diào)用startActivity
的時候發(fā)生了異常:
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。下面分析相關(guān)源碼外永,嘗試尋找原因和突破點崎脉。先從拋出異常的代碼開始。
/* 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
的設(shè)置有關(guān)伯顶。設(shè)置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);
}
}
跟蹤標(biāo)志位FLAG_ALLOW_FDS
的設(shè)置:
/* 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)
在多處代碼有調(diào)用囚灼,如:Intent#prepareToLeaveProcess
、LoadedApk$ReceiverDispatcher#performReceive
祭衩、BroadcastReceiver$PendingResult#sendFinished
灶体、等,其中Intent#prepareToLeaveProcess
被Instrumentation#execStartActivity
調(diào)用掐暮。
上面涉及的代碼都屬于非公開接口蝎抽,應(yīng)用程序不應(yīng)該調(diào)用。由此可知Android不希望在四大組件調(diào)用過程中傳遞FD路克,故意設(shè)置了門檻樟结。
通過Bundle#putBinder
突破Intent
和四大組件的限制
已知通過aidl
可以傳遞FD锥涕,且Bundle
類有putBinder
方法可以傳遞IBinder
,那么不妨發(fā)一個IBinder
到目標(biāo)進程狭吼,然后用這個IBinder
傳遞FD层坠。雖然本質(zhì)上還是aidl
的調(diào)用,但可以不用依賴bindService
等方法建立連接刁笙,而是通過Intent
直接發(fā)到目標(biāo)進程破花。
首先定義aidl
:
interface IFileBinder {
ParcelFileDescriptor openFileDescriptor(int mode);
}
實現(xiàn)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());
}
}
}
發(fā)送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發(fā)送FD的結(jié)論
上面例舉了若干跨進程傳遞FD的方法,相互各有優(yōu)劣疲吸。如要從上述各方法中做選擇座每,可以至少從以下幾點來考慮:
- 源應(yīng)用是否需要定制;
- 目標(biāo)應(yīng)用是否需要定制摘悴;
- 目標(biāo)應(yīng)用是否需要對FD做細粒度的控制峭梳;
- 源應(yīng)用是否需要對目標(biāo)應(yīng)用做權(quán)限校驗和控制;
- 代碼的易維護性和易擴展性蹂喻;
綜合來說葱椭,FileProvider
在各方面都是比較完備和可靠的文件共享機制。