今天有同事问我问题,调用系统分享,QQ分享总是提示图片获取不到。过来几分钟又告诉我搞定了,原来是uri的问题。他开始是这样写的:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
imageUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".FileProvider", file);
} else {
imageUri = Uri.fromFile(imageFile);
}
后来把判断去掉,统一只用Uri#parse就没问题了。
虽然他解决了问题,但是却激起了我的好奇心,然后就开始看源码,然后就有了这篇文章。
在分析之前,咱们先来科普一下7.0以前及以后通过file获取uri的方法和内部具体实现。
7.0以前 Uri#fromFile
val uri = Uri.fromFile(file)
public static Uri fromFile(File file) {
if (file == null) {
throw new NullPointerException("file");
}
PathPart path = PathPart.fromDecoded(file.getAbsolutePath());
return new HierarchicalUri(
"file", Part.EMPTY, path, Part.NULL, Part.NULL);
}
先通过我们传入的file的绝对路径创建一个叫PathPart的对象,然后创建HierarchicalUri并返回。
static PathPart fromDecoded(String decoded) {
return from(NOT_CACHED, decoded);
}
static PathPart from(String encoded, String decoded) {
if (encoded == null) {
return NULL;
}
if (encoded.length() == 0) {
return EMPTY;
}
return new PathPart(encoded, decoded);
}
其实就是new了一个PathPart,encoded是NOT_CACHED,decoded是文件绝对路径。
static class PathPart extends AbstractPart {
private PathPart(String encoded, String decoded) {
super(encoded, decoded);
}
}
再去父类AbstractPart 看看:
static abstract class AbstractPart {
volatile String encoded;
volatile String decoded;
AbstractPart(String encoded, String decoded) {
this.encoded = encoded;
this.decoded = decoded;
}
}
其实就是维护了两个变量encoded和decoded。
abstract String getEncoded();
final String getDecoded() {
@SuppressWarnings("StringEquality")
boolean hasDecoded = decoded != NOT_CACHED;
return hasDecoded ? decoded : (decoded = decode(encoded));
}
两个变量的get方法,encoded的抽象方法,decoded是final的不能复写。
getDecoded方法意思是如果decoded的值不是默认值NOT_CACHED,就需要做编码,否则不需要。
String getEncoded() {
@SuppressWarnings("StringEquality")
boolean hasEncoded = encoded != NOT_CACHED;
// Don't encode '/'.
return hasEncoded ? encoded : (encoded = encode(decoded, "/"));
}
这是PathPart的具体实现方法,其实跟父类的getDecoded是一个意思。
然后我们来看一下创建并返回的HierarchicalUri,很显然他是一个Uri。
private static class HierarchicalUri extends AbstractHierarchicalUri {
/** Used in parcelling. */
static final int TYPE_ID = 3;
private final String scheme; // can be null
private final Part authority;
private final PathPart path;
private final Part query;
private final Part fragment;
private HierarchicalUri(String scheme, Part authority, PathPart path,
Part query, Part fragment) {
this.scheme = scheme;
this.authority = Part.nonNull(authority);
this.path = path == null ? PathPart.NULL : path;
this.query = Part.nonNull(query);
this.fragment = Part.nonNull(fragment);
}
}
他维护了几个变量,都是通过构造函数传入的。
return new HierarchicalUri(
"file", Part.EMPTY, path, Part.NULL, Part.NULL);
我再贴一次代码,对照着看一下。第一个参数协议传入了file,第二个参数授权传入Part.EMPTY,他也是AbstractPart的子类,encoded和decoded都是空字符串,第三个参数路径就是我们刚才创建的PathPart,第四个参数查询传入的是 Part.NULL,他也是AbstractPart的子类,encoded和decoded都是null,第五个参数碎片传入的也是Part.NULL。
然后我们来看下他的toString方法:
private volatile String uriString = NOT_CACHED;
@Override
public String toString() {
@SuppressWarnings("StringEquality")
boolean cached = (uriString != NOT_CACHED);
return cached ? uriString
: (uriString = makeUriString());
}
uriString默认是NOT_CACHED,所以会走makeUriString方法:
private String makeUriString() {
StringBuilder builder = new StringBuilder();
if (scheme != null) {
builder.append(scheme).append(':');
}
appendSspTo(builder);
if (!fragment.isEmpty()) {
builder.append('#').append(fragment.getEncoded());
}
return builder.toString();
}
其实就是把维护的几个变量拼接起来。首先是协议+冒号
private void appendSspTo(StringBuilder builder) {
String encodedAuthority = authority.getEncoded();
if (encodedAuthority != null) {
// Even if the authority is "", we still want to append "//".
builder.append("//").append(encodedAuthority);
}
String encodedPath = path.getEncoded();
if (encodedPath != null) {
builder.append(encodedPath);
}
if (!query.isEmpty()) {
builder.append('?').append(query.getEncoded());
}
}
authority#getEncoded
String getEncoded() {
@SuppressWarnings("StringEquality")
boolean hasEncoded = encoded != NOT_CACHED;
return hasEncoded ? encoded : (encoded = encode(decoded));
}
授权之前传的是空字符串,所以返回空字符串。
因为空字符串不等于null,所以要拼接双斜杠。
路径是我们new出来的PathPart对象,他的encoded == NOT_CACHED,所以path#getEncoded返回的就是编码过的decoded。然后我们来看下编码过程。
public static String encode(String s, String allow) {
if (s == null) {
return null;
}
// Lazily-initialized buffers.
StringBuilder encoded = null;
int oldLength = s.length();
// This loop alternates between copying over allowed characters and
// encoding in chunks. This results in fewer method calls and
// allocations than encoding one character at a time.
int current = 0;
while (current < oldLength) {
// Start in "copying" mode where we copy over allowed chars.
// Find the next character which needs to be encoded.
int nextToEncode = current;
while (nextToEncode < oldLength
&& isAllowed(s.charAt(nextToEncode), allow)) {
nextToEncode++;
}
// If there's nothing more to encode...
if (nextToEncode == oldLength) {
if (current == 0) {
// We didn't need to encode anything!
return s;
} else {
// Presumably, we've already done some encoding.
encoded.append(s, current, oldLength);
return encoded.toString();
}
}
if (encoded == null) {
encoded = new StringBuilder();
}
if (nextToEncode > current) {
// Append allowed characters leading up to this point.
encoded.append(s, current, nextToEncode);
} else {
// assert nextToEncode == current
}
// Switch to "encoding" mode.
// Find the next allowed character.
current = nextToEncode;
int nextAllowed = current + 1;
while (nextAllowed < oldLength
&& !isAllowed(s.charAt(nextAllowed), allow)) {
nextAllowed++;
}
// Convert the substring to bytes and encode the bytes as
// '%'-escaped octets.
String toEncode = s.substring(current, nextAllowed);
try {
byte[] bytes = toEncode.getBytes(DEFAULT_ENCODING);
int bytesLength = bytes.length;
for (int i = 0; i < bytesLength; i++) {
encoded.append('%');
encoded.append(HEX_DIGITS[(bytes[i] & 0xf0) >> 4]);
encoded.append(HEX_DIGITS[bytes[i] & 0xf]);
}
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
current = nextAllowed;
}
// Encoded could still be null at this point if s is empty.
return encoded == null ? s : encoded.toString();
}
s就是文件的绝对路径,current从0开始循环到s的长度-1,里面还有个while循环,nextToEncode从0开始,直接s的长度-1,相当于遍历s的每一个字符,如果都没有问题,循环正常结束,nextToEncode的会等于s的长度,因为current等于0,直接返回了s。结论就是path#getEncoded返回的是文件的绝对路径。
最后两个query和fragment我们都传的是PART.NULL,所以不需要拼接。
那么最终拼出来的就是协议+冒号+双斜杠+文件的绝对路径。
比如我们在外部存储上创建了一个文件叫110.jpg,那么uri就是file:///storage/emulated/0/110.jpg
7.0以前 Uri#parse
public static Uri parse(String uriString) {
return new StringUri(uriString);
}
创建了一个StringUri并返回,他也是个uri的子类。
private static class StringUri extends AbstractHierarchicalUri {
private StringUri(String uriString) {
if (uriString == null) {
throw new NullPointerException("uriString");
}
this.uriString = uriString;
}
}
public String toString() {
return uriString;
}
toString返回的就是构造函数传入的值。如果还是在外部存储的文件110.jpg,那么结果就是/storage/emulated/0/110.jpg,那么是否意味着没有协议了呢?是的,我们来看下:
public String getScheme() {
@SuppressWarnings("StringEquality")
boolean cached = (scheme != NOT_CACHED);
return cached ? scheme : (scheme = parseScheme());
}
scheme默认为NOT_CACHED,会走parseScheme方法
private String parseScheme() {
int ssi = findSchemeSeparator();
return ssi == NOT_FOUND ? null : uriString.substring(0, ssi);
}
private int findSchemeSeparator() {
return cachedSsi == NOT_CALCULATED
? cachedSsi = uriString.indexOf(':')
: cachedSsi;
}
cachedSsi默认等于NOT_CALCULATED,uriString并不包含冒号,所以ssi就等于-1,而NOT_FOUND是-1,所以协议为null。
7.0后 fileProvider
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
我们通过fileProvider的这个静态方法获取uri。authority是我们自己定义的,一般就是包名+"FileProvider"。先来看getPathStrategy方法。
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
}
sCache.put(authority, strat);
}
}
return strat;
}
sCache是一个静态变量hashmap,以authority为key保存PathStrategy,如果能取到就取出来用,不能取出就去创建并保存。PathStrategy是通过parsePathStrategy创建的。
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);
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
}
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
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)) {
target = context.getFilesDir();
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
}
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
}
if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
这里实际上是解析我们配置路径的xml文件:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<paths>
<!-- common -->
<files-path
name="files"
path="."/>
<root-path
name="root"
path="."/>
<external-path
name="external"
path="."/>
</paths>
</paths>
遍历每一个标签,拿标签名跟一些静态常量做对比。
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 TAG_EXTERNAL_MEDIA = "external-media-path";
是不是跟我们xml配置的标签对上了。比如标签名是external-path,就会创建一个路径为外部存储根目录的文件,标签名是files-path,就会创建context#getFilesDir的文件,然后把这些跟我们xml配置的标签匹配上而创建的文件都存入SimplePathStrategy并返回SimplePathStrategy对象。
保存的key是我们在xml配置时的name对应的值,比如external,value是经过处理的文件。怎么处理的呢?
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;
}
void addRoot(String name, File root, HashMap<String, File> mRoots) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}
try {
// Resolve to canonical path to keep path checking fast
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
mRoots.put(name, root);
}
其实很简单,就是以之前创建的文件为根目录,再以我们xml中配置的path的值为目录名创建一个新目录。以external为例,path的值是".",那么最终文件的路径就是/storage/emulated/0/.
然后调用addRoot方法,注意,这里会把文件的路径转成标准路径,比如我上面的路径转成标准路径,最后的点就没有了,最后把文件存入hashmap。
ok,我们再回到最初的代码
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
strategy已经创建完成,他是SimplePathStrategy,也是PathStrategy的实现类,调用了他的getUriForFile方法
@Override
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
path是我们最初自己文件的路径,我们的最终目的也就是要把这个文件的路径转为uri。然后循环刚才的hashmap,如果我们自己文件的路径是以hashmap中的某个文件的路径开始的,那么就拿这个文件的路径作为根目录,而我们这里的例子就是/storage/emulated/0,path是/storage/emulated/0/110.jpg。
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
这里实际上就把path变成了110.jpg。Uri#encode我们之前已经分析过,只要传入的参数是合法的,那么就是返回参数本身。所以path又变成了external/110.jpg。最后通过builder方法构建了一个uri。我之前说过,builder构建法直接看build方法即可
public Uri build() {
if (opaquePart != null) {
if (this.scheme == null) {
throw new UnsupportedOperationException(
"An opaque URI must have a scheme.");
}
return new OpaqueUri(scheme, opaquePart, fragment);
} else {
// Hierarchical URIs should not return null for getPath().
PathPart path = this.path;
if (path == null || path == PathPart.NULL) {
path = PathPart.EMPTY;
} else {
// If we have a scheme and/or authority, the path must
// be absolute. Prepend it with a '/' if necessary.
if (hasSchemeOrAuthority()) {
path = PathPart.makeAbsolute(path);
}
}
return new HierarchicalUri(
scheme, authority, path, query, fragment);
}
}
就是new了一个HierarchicalUri,这个类之前已经分析过了,那么最终的uri字符串就是content://包名$.FileProvider/external/110.jpg
源码分析完了,我们发现其实fileProvider创建的uri跟Uri#fromFile创建的uri对象是一样的,都是HierarchicalUri,只不过拼接规则不一样,比如协议一个是file,一个是content;比如外部路径一个是/storage/emulated/0,一个是/external,仅此而已。那7.0开始为什么要这么做呢?我们用一个7.0以上的手机用Uri#fromFile返回的uri来调用系统相机,看看错误日志里的方法栈是怎么样的。
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1975)
at android.net.Uri.checkFileUriExposed(Uri.java:2363)
at android.content.ClipData.prepareToLeaveProcess(ClipData.java:941)
at android.content.Intent.prepareToLeaveProcess(Intent.java:9952)
at android.content.Intent.prepareToLeaveProcess(Intent.java:9937)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1622)
at android.app.Activity.startActivityForResult(Activity.java:4762)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:767)
at android.app.Activity.startActivityForResult(Activity.java:4702)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:754)
我们来看Instrumentation#execStartActivity
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
是Intent.prepareToLeaveProcess这行报错了,为什么叫准备离开进程呢?因为下一行就是会通过binder去调用ams的startActivity,ams我们之前分析过不多说了,确实下一行代码就离开我们自己的进程了。
public void prepareToLeaveProcess(Context context) {
final boolean leavingPackage = (mComponent == null)
|| !Objects.equals(mComponent.getPackageName(), context.getPackageName());
prepareToLeaveProcess(leavingPackage);
}
public void prepareToLeaveProcess(boolean leavingPackage) {
if (mClipData != null) {
mClipData.prepareToLeaveProcess(leavingPackage, getFlags());
}
}
public void prepareToLeaveProcess(boolean leavingPackage) {
// Assume that callers are going to be granting permissions
prepareToLeaveProcess(leavingPackage, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
public void prepareToLeaveProcess(boolean leavingPackage, int intentFlags) {
final int size = mItems.size();
for (int i = 0; i < size; i++) {
final Item item = mItems.get(i);
if (item.mIntent != null) {
item.mIntent.prepareToLeaveProcess(leavingPackage);
}
if (item.mUri != null && leavingPackage) {
if (StrictMode.vmFileUriExposureEnabled()) {
item.mUri.checkFileUriExposed("ClipData.Item.getUri()");
}
if (StrictMode.vmContentUriWithoutPermissionEnabled()) {
item.mUri.checkContentUriWithoutPermission("ClipData.Item.getUri()",
intentFlags);
}
}
}
}
通过mComponent#getPackageName获取的包名是目标包名,也就是系统相机的包名,很显然跟context获取的我们自己的包名,不同,所以leavingPackage为true。因为是跨包的,所以最后会调用Uri#checkFileUriExposed
public void checkFileUriExposed(String location) {
if ("file".equals(getScheme())
&& (getPath() != null) && !getPath().startsWith("/system/")) {
StrictMode.onFileUriExposed(this, location);
}
}
public static void onFileUriExposed(Uri uri, String location) {
final String message = uri + " exposed beyond app through " + location;
if ((sVmPolicy.mask & PENALTY_DEATH_ON_FILE_URI_EXPOSURE) != 0) {
throw new FileUriExposedException(message);
} else {
onVmPolicyViolation(new FileUriExposedViolation(message));
}
}
原来如此,只要这个uri的协议是file,会直接抛异常。现在总算是明白了,只要启动的activity所在进程的包名,和目标activity所在进程的包名不同相同,就会去做检测uri的协议是不是file开头的。这里注意,如果一个应用开启了多进程,那么虽然是跨进程了但是并不算跨包。
网友评论