FileProvider 路径配置策略的理解
★ FileProvider的使用
在AndroidManifest.xml中
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="set_your_package_name"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepath_data" />
</provider>
通常设置android:exported="false"
,以保证权限最小化。
android:resource="@xml/filepath_data"
中,filepath_data.xml文件是配置哪些路径是可以通过FileProvider访问的。
meta-data是以键值对的方式保存(key-value pairs)。android.support.FILE_PROVIDER_PATHS
作为meta-data的键(key),@xml/filepath_data
作为meta-data的值(value)。在FileProvider中会读取meta-data中的android.support.FILE_PROVIDER_PATHS
对应的值。
filepath_data.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="my_files" path="tempfiles" />
<external-path name="my_external" path="Download"/>
<cache-path name="my_cache" path="/" />
</paths>
files-path
对应app的/data/data/<package_name>/files/
目录,path="tempfiles"
是指子目录,即完整的目录为/data/data/<package_name>/files/tempfiles
。
external-path
对应的是内置的sdcard目录/sdcard/
,path="Download"
是子目录,完整目录为 /sdcard/Download
。
cache-path
对应的是/data/data/<package_name>/cache/
,path="/"
, 没有子目录。
name
属性相当于这些路径的别名,通过name可以获取到相对应的路径。
★ 如何更好地理解这几个路径的用法?
通过学习Android中解析filepath_data.xml文件的源代码,可以更容易理解和掌握这些路径的具体含义。
代码请参考FileProvider的parsePathStrategy()方法。如果想了解如何执行到此方法的,可以参考Android ContentProvider的加载过程
parsePathStrategy()
方法的代码如下(省略了一些代码):
XML文件中的TAG和属性:
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
private static final String ATTR_NAME = "name";
private static final String ATTR_PATH = "path";
XML中各个tag对应的路径,如下表:
Tag | 对应的路径 |
---|---|
root-path | 根目录/ |
files-path | /data/user/0/<package_name>/files 或者/data/data/<package_name>/files 这两个目录指向相同的位置 |
cache-path | /data/user/0/<package_name>/cache 或者 /data/data/<package_name>/cache |
external-path | /storage/emulated/0 或者/sdcard/ |
external-files-path | /storage/emulated/0/Android/data/<package_name>/files 或者 /sdcard/Android/data/<package_name>/files |
external-cache-path | /storage/emulated/0/Android/data/<package_name>/cache 或者 /sdcard/Android/data/<package_name>/cache |
parsePathStrategy() @FileProvider
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
// META_DATA_FILE_PROVIDER_PATHS 为"android.support.FILE_PROVIDER_PATHS", 这是在AndroidManifest.xml中所使用的。
// 读取filepath_data.xml文件(本文中的例子)
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
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)) {
// "root-path"标签,DEVICE_ROOT = new File("/"),系统的根目录
target = DEVICE_ROOT;
} else if (TAG_FILES_PATH.equals(tag)) {
// "files-path"标签,getFilesDir(),对应"/data/user/0/<package_name>/files"目录
target = context.getFilesDir();
} else if (TAG_CACHE_PATH.equals(tag)) {
// "cache-path"标签,对应"/data/user/0/<package_name>/cache"目录
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {
// "external-path"标签,对应内置sdcard目录,例如"/storage/emulated/0", 或者"/sdcard/"
target = Environment.getExternalStorageDirectory();
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
// "external-files-path"标签,对应 "/storage/emulated/0/Android/data/<package_name>/files"目录
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
}
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
// "external-cache-path"标签,对应"/storage/emulated/0/Android/data/<package_name>/cache"目录
File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
}
if (target != null) {
// 将路径拼起来,name作为key,完整路径是value
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
注意:/data/user/0
是指向/data/data
目录,所以/data/user/0/<package_name>/files
也就是/data/data/<package_name>/files
。
执行下面的命令可以看到:
ls -ld /data/user/0
lrwxrwxrwx 1 root root 10 2017-04-15 00:25 /data/user/0 -> /data/data
以 filepath_data.xml 这个文件为例,再看一下都配置了哪些路径:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="my_files" path="tempfiles" />
<external-path name="my_external" path="Download"/>
<cache-path name="my_cache" />
</paths>
files-path
对应app的/data/data/<package_name>/files/
目录,path="tempfiles"
是指子目录,拼起来的完整路径为/data/data/<package_name>/files/tempfiles
。
external-path
对应的是内置的sdcard目录/sdcard/
,path="Download"
是子目录,完整目录为 /sdcard/Download
。
cache-path
对应的是/data/data/<package_name>/cache/
,这个例子里没有子目录。
★ 如何使用filepath_data.xml中配置的路径?
◇ 通过uri来访问文件
以<files-path name="my_files" path="tempfiles" />
为例。
通过Uri content://<authority>/my_files/<file_name>
来访问my_files
标签对应的目录中的文件<file_name>
。
my_files
标签对应的目录是 /data/data/<package_name>/files/tempfiles/
例如,content://my_authority/my_files/path/to/file001.txt
对应的就是/data/data/<package_name>/files/tempfiles/path/to/file001.txt
。
以 uri content://my_authority/my_files/path/to/file001.txt
为例,解释下面的代码。
代码可以参考FileProvider的getFileForUri(),下面是部分主要代码。
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf('/', 1);
// 解析出 tag,在此例中 tag 是 my_files
final String tag = Uri.decode(path.substring(1, splitIndex));
// path是uri中的 <file_name>,path 可以只是文件名,也可以是带路径的文件名,此例中, path 为 'path/to/file001.txt'
path = Uri.decode(path.substring(splitIndex + 1));
// 这个tag就是`<files-path name="my_files" path="tempfiles" />`中的属性name
// 这里获取到的 root 是 my_files 对应的路径,即 '/data/data/<package_name>/files/tempfiles'
final File root = mRoots.get(tag); // mRoots 中记录的是 key/value, key 为 filepath_data.xml 中的 name 值,value 为完整路径
// 将路径拼起来,构成实际的文件路径,此例中,完整路径为 '/data/data/<package_name>/files/tempfiles/path/to/file001.txt'
File file = new File(root, path);
// 略
return file;
}
}
◇ 获取文件对应的Uri
参考FileProvider中的getUriForFile()
注:所有出错处理的代码都忽略了。
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch...
// 这段代码是为了找到文件file最匹配的路径,即取匹配最长的那个root
Map.Entry<String, File> mostSpecific = null;
// mRoots 中记录的是 key/value, key 为 filepath_data.xml 中的 name 值,value 为完整路径
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath(); // rootPath 为完整路径
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) { // 找到最长匹配的路径
mostSpecific = root;
}
}
final String rootPath = mostSpecific.getValue().getPath(); // 完整路径
// path 是以 / 开头的, rootPath 也以 / 开头
if (rootPath.endsWith("/")) {
// 如果 rootPath 以 / 结尾,则将 rootPath 长度的内容去掉后,剩下的就是 uri 中使用的路径
path = path.substring(rootPath.length());
} else {
// 如果 rootPath 不是以 / 结尾,则去掉 rootPath 长度后,再去掉一个 / ,剩下的就是uri中使用的路径
path = path.substring(rootPath.length() + 1);
}
// mostSpecific.getKey() 对应的是路径配置文件中的属性 name
// 最终拼起来像这样:content://<authority>/<name>/<file_path>
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
以<external-path name="my_external" path="Download"/>
为例。
对于内置sdcard中Download目录下的文件file002.txt,其路径为/sdcard/Download/file002.txt
。对应的uri为content://<authority>/my_external/file002.txt
。
★ Android ContentProvider的加载过程
当某个app的进程要启动时,Dalvik虚拟机先fork出一个新的进程,然后将此进程的名字命名为这个app的包名,然后通过反射的方式,执行 ActivityThread 的静态的main()
方法,在main()
中创建主线程 ActivityThread,并将app中的各种组件信息附加到该进程中,即调用attach()
方法。
从这个attach()
方法开始,来描述ContentProvider的加载过程。
说明:@ActivityThread
表示代码在ActivityThread类中。
-> main() @ActivityThread
ActivityThread thread = new ActivityThread();
thread.attach(false);
-> attach() @ActivityThread
-> mgr.attachApplication(mAppThread); @ActivityThread
mgr是IActivityManager类型的接口,是 ActivityManagerProxy 的实例,最终调用 ActivityManagerService的对应的方法。
这里会切换进程到system_server进程中(ActivityManagerService所在的进程)
-> attachApplication() @ActivityManagerService
-> attachApplicationLocked() @ActivityManagerService
List<ProviderInfo> providers = normalMode ? generateApplicationProvidersLocked(app) : null;
thread.bindApplication(processName, appInfo, providers, ...);
thread 是 IApplicationThread 类型的接口,用来向app所在进程发送消息,即调用app进程中的方法。切换进程到app进程。
-> bindApplication() @ActivityThread
AppBindData data = new AppBindData();
data.providers = providers;
sendMessage(H.BIND_APPLICATION, data);
-> handleMessage() @ActivityThread
case BIND_APPLICATION:
handleBindApplication(data);
-> handleBindApplication() @ActivityThread
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
installContentProviders(app, data.providers);
}
}
-> installContentProviders() @ActivityThread
-> installProvider() @ActivityThread
final java.lang.ClassLoader cl = c.getClassLoader();
localProvider = (ContentProvider)cl.loadClass(info.name).newInstance();
localProvider.attachInfo(c, info);
c 是context,info是ProviderInfo对象。info.name是provider的名字。
由于FileProvider中重写了attachInfo(),所以,这里的localProvider.attachInfo()将执行FileProvider的attachInfo()。
-> attachInfo() @FileProvider
super.attachInfo(context, info); // 调用父类ContentProvider的attachInfo(),设置 ContentProvider 的各种属性,并调用Provider 的onCreate()
mStrategy = getPathStrategy(context, info.authority);
getPathStrategy()解析filepath_data.xml文件(在本文中的例子)。
-> getPathStrategy() @FileProvider
-> parsePathStrategy() @FileProvider
parsePathStrategy()用来解析filepath_data.xml文件,这对理解filepath_data.xml文件很有帮助。