基礎(chǔ)
為了安全地共享本 app 的文件,您需要配置您的應(yīng)用程序,以 Content URI 形式為文件提供安全的句柄。 FileProvider 會(huì)根據(jù)你在 XML 中提供的規(guī)范生成文件的 Content URI 。
FileProvider 繼承于 ContentProvider ,它只是內(nèi)容提供者的一個(gè)特殊子類。
也可以不使用 FileProvider ,而自己定義一個(gè) ContentProvider 。
幾個(gè)方法的返回路徑
| 方法名 | 返回路徑 |
|---|---|
| getExternalCacheDir() | sd 卡中 Android/data/包名/cache 目錄 |
| getExternalFilesDir(String) | sd 卡中 Android/data/包名/files/xx 目錄,xx為方法參數(shù) |
| getCacheDir() | 內(nèi)部存儲(chǔ)目錄下的 cache 目錄 |
| getDir() | 內(nèi)部存儲(chǔ)目錄下的 app_xx 目錄 ,其中 xx 為方法參數(shù) |
| getFilesDir() | 內(nèi)部存儲(chǔ)目錄下 files 目錄 |
Manifest 配置
在清單文件中進(jìn)行如下配置:
<application
...>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.myapp.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
...
</application>
<meta-data> 中 resource 屬性指定了 xml 文件名,該文件位于 res/xml 文件夾下。
authorities 指定了 FileProvider 生成 URI 的authorities。
xml
在 res/xml 文件夾下建立以 resource 屬性值為名的 xml 文件,在該文件中指定本 app 可以共享的文件目錄,所有可以共享的目錄只能在該 xml 中指定,無法通過代碼動(dòng)態(tài)完成
可以參考如下示例:
<paths>
<files-path path="images/" name="url_name" />
</paths>
path 屬性指定了共享的子目錄, name 屬性指定了共享目錄在 content URI 中的別名。子目錄與別名是一一對(duì)應(yīng)關(guān)系。
<paths> 中可以有多個(gè)子標(biāo)簽。
父目錄名通過子標(biāo)簽名指定。如上面的 files-path 指定的父目錄就是 getFilesDir() 返回的目錄。
在上面例子中,如果共享的是 images 目錄下的 default_image.jpg ,其對(duì)應(yīng)的 content URI 如下:
content://com.example.myapp.fileprovider/url_name/default_image.jpg
其文件的真實(shí)路徑
Context.getFilesDir() +"/images/default_image.jpg"
可以發(fā)現(xiàn),轉(zhuǎn)成 URI 后,images 目錄下的路徑會(huì)全部真實(shí)地保留。保留的部分叫剩余路徑(為后期分析源碼方便,自己取的名字)。
子標(biāo)簽名
| 標(biāo)簽名 | 對(duì)應(yīng)的目錄 |
|---|---|
| files-path | Context#getFilesDir() |
| cache-path | Context#getCacheDir() |
| external-path | Environment.getExternalStorageDirectory() |
| external-files-path | Context#getExternalFilesDir |
| external-cache-path | Context#getExternalCacheDir() |
源碼分析
因?yàn)槭?ContentProvider ,所以先看其 attachInfo 方法。
attachInfo()
public void attachInfo(Context context, ProviderInfo info) {
super.attachInfo(context, info);
//對(duì) exported 與 grantUriPermissions 屬性值進(jìn)行判斷,其要求兩者值必須分別為 false 與 true,否則就會(huì)扔異常。
……
// authority 的值就是 authorities 屬性的值
mStrategy = getPathStrategy(context, info.authority);
}
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
//以 authority 值為 key 對(duì) PathStrategy 對(duì)象進(jìn)行緩存
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
}
// 省略異常處理代碼
sCache.put(authority, strat);
}
}
return strat;
}
對(duì) strat 進(jìn)行初始化時(shí),會(huì)調(diào)用 parsePathStrategy 方法,其主要就是解析在清單文件中指定的 xml 文件。
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
//讀取清單文件中配置的 xml 文件
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
//對(duì) in 進(jìn)行非空判斷 ,忽略。下面是對(duì) in 進(jìn)行 xml 解析
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
//分別獲取 name 屬性與 path 屬性
final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);
File target = null;
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
} else if (TAG_FILES_PATH.equals(tag)) {// 標(biāo)簽名為 files-path
target = context.getFilesDir();
} else if (TAG_CACHE_PATH.equals(tag)) {// 標(biāo)簽名為 cache-path
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {// 標(biāo)簽名為 external-path
target = Environment.getExternalStorageDirectory();
} else if (TAG_EXTERNAL_FILES.equals(tag)) {// 標(biāo)簽名為 external-files-path
//這段代碼的作用就是將 target 賦值為 getExternalFilesDir() 的返回值
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {// 標(biāo)簽名為 external-cache-path
//這段代碼的作用就是將 target 賦值為 getExternalCacheDir() 的返回值
}
if (target != null) {
// buildPath 會(huì)在 target 目錄下建立一個(gè)以 path 值為名的目錄
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
private static File buildPath(File base, String... segments) {
File cur = base;
for (String segment : segments) {
if (segment != null) {
cur = new File(cur, segment);
}
}
return cur;
}
其余方法
其他幾個(gè)方法的實(shí)現(xiàn),基本上都是通過 PathStrategy 完成的。如
public static Uri getUriForFile(Context context, String authority, File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
PathStrategy 與 SimplePathStrategy
PathStrategy 的唯一子類是 SimplePathStrategy。它會(huì)在 parsePathStrategy() 中被創(chuàng)建。
static class SimplePathStrategy implements PathStrategy {
private final String mAuthority;
//以 name 屬性值為 key ,以標(biāo)簽名與 path 值聯(lián)合指定的目錄 File 為 value
private final HashMap<String, File> mRoots = new HashMap<String, File>();
public SimplePathStrategy(String authority) {
mAuthority = authority;
}
// name 是 xml 中定義 name 屬性值,而 root 是根據(jù) xml 中標(biāo)簽名與 path 屬性值聯(lián)合指定的一個(gè)本地文件目錄,name 與 root 是一一對(duì)應(yīng)關(guān)系
public void addRoot(String name, File root) {
……
mRoots.put(name, root);
}
@Override
public Uri getUriForFile(File file) {
String path;// path 指代 file 的路徑。
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
// 查找最接近 file 路徑的 Map.Entry<String, File> 對(duì)象。
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
// mostSpecific 非空判斷,略
// 以下對(duì) path 進(jìn)行處理,并最終轉(zhuǎn)成 content://authority/name 形式的 URI
// 通過該種方式處理,最終會(huì)將指定的文件根據(jù)清單文件中的 authority 以及 xml 中指定的 name 轉(zhuǎn)換成 URI
final String rootPath = mostSpecific.getValue().getPath();
// 截取指定 file 的除 標(biāo)簽名及 path 屬性指定的剩余部分
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// 將 path 轉(zhuǎn)換成 name/剩余路徑 形式
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
@Override
public File getFileForUri(Uri uri) {
// 獲取到的內(nèi)容形式為:/name/剩余路徑
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf('/', 1);
// xml 中指定的 name 屬性值
final String tag = Uri.decode(path.substring(1, splitIndex));
// 文件的剩余路徑
path = Uri.decode(path.substring(splitIndex + 1));
// 獲取文件的根路徑
final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
}
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
}
return file;
}
}
總結(jié)
- 指定 path 后,該目錄下的所有文件都可以共享,無論是文件還是文件夾。