今天有同事問我問題,調(diào)用系統(tǒng)分享,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);
}
后來把判斷去掉,統(tǒng)一只用Uri#parse就沒問題了。
雖然他解決了問題,但是卻激起了我的好奇心,然后就開始看源碼,然后就有了這篇文章。
在分析之前,咱們先來科普一下7.0以前及以后通過file獲取uri的方法和內(nèi)部具體實現(xiàn)。
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的絕對路徑創(chuàng)建一個叫PathPart的對象,然后創(chuàng)建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;
}
}
其實就是維護(hù)了兩個變量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的不能復(fù)寫。
getDecoded方法意思是如果decoded的值不是默認(rèn)值NOT_CACHED,就需要做編碼,否則不需要。
String getEncoded() {
@SuppressWarnings("StringEquality")
boolean hasEncoded = encoded != NOT_CACHED;
// Don't encode '/'.
return hasEncoded ? encoded : (encoded = encode(decoded, "/"));
}
這是PathPart的具體實現(xiàn)方法,其實跟父類的getDecoded是一個意思。
然后我們來看一下創(chuàng)建并返回的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);
}
}
他維護(hù)了幾個變量,都是通過構(gòu)造函數(shù)傳入的。
return new HierarchicalUri(
"file", Part.EMPTY, path, Part.NULL, Part.NULL);
我再貼一次代碼,對照著看一下。第一個參數(shù)協(xié)議傳入了file,第二個參數(shù)授權(quán)傳入Part.EMPTY,他也是AbstractPart的子類,encoded和decoded都是空字符串,第三個參數(shù)路徑就是我們剛才創(chuàng)建的PathPart,第四個參數(shù)查詢傳入的是 Part.NULL,他也是AbstractPart的子類,encoded和decoded都是null,第五個參數(shù)碎片傳入的也是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默認(rèn)是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();
}
其實就是把維護(hù)的幾個變量拼接起來。首先是協(xié)議+冒號
private void appendSspTo(StringBuilder builder) {
String encodedAuthority = authority.getEncoded();
if (encodedAuthority != null) {
// Even if the authority is "", we still want to append "http://".
builder.append("http://").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));
}
授權(quán)之前傳的是空字符串,所以返回空字符串。
因為空字符串不等于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開始循環(huán)到s的長度-1,里面還有個while循環(huán),nextToEncode從0開始,直接s的長度-1,相當(dāng)于遍歷s的每一個字符,如果都沒有問題,循環(huán)正常結(jié)束,nextToEncode的會等于s的長度,因為current等于0,直接返回了s。結(jié)論就是path#getEncoded返回的是文件的絕對路徑。
最后兩個query和fragment我們都傳的是PART.NULL,所以不需要拼接。
那么最終拼出來的就是協(xié)議+冒號+雙斜杠+文件的絕對路徑。
比如我們在外部存儲上創(chuàng)建了一個文件叫110.jpg,那么uri就是file:///storage/emulated/0/110.jpg
7.0以前 Uri#parse
public static Uri parse(String uriString) {
return new StringUri(uriString);
}
創(chuàng)建了一個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返回的就是構(gòu)造函數(shù)傳入的值。如果還是在外部存儲的文件110.jpg,那么結(jié)果就是/storage/emulated/0/110.jpg,那么是否意味著沒有協(xié)議了呢?是的,我們來看下:
public String getScheme() {
@SuppressWarnings("StringEquality")
boolean cached = (scheme != NOT_CACHED);
return cached ? scheme : (scheme = parseScheme());
}
scheme默認(rèn)為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默認(rèn)等于NOT_CALCULATED,uriString并不包含冒號,所以ssi就等于-1,而NOT_FOUND是-1,所以協(xié)議為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的這個靜態(tài)方法獲取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是一個靜態(tài)變量hashmap,以authority為key保存PathStrategy,如果能取到就取出來用,不能取出就去創(chuàng)建并保存。PathStrategy是通過parsePathStrategy創(chuàng)建的。
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>
遍歷每一個標(biāo)簽,拿標(biāo)簽名跟一些靜態(tài)常量做對比。
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配置的標(biāo)簽對上了。比如標(biāo)簽名是external-path,就會創(chuàng)建一個路徑為外部存儲根目錄的文件,標(biāo)簽名是files-path,就會創(chuàng)建context#getFilesDir的文件,然后把這些跟我們xml配置的標(biāo)簽匹配上而創(chuàng)建的文件都存入SimplePathStrategy并返回SimplePathStrategy對象。
保存的key是我們在xml配置時的name對應(yīng)的值,比如external,value是經(jīng)過處理的文件。怎么處理的呢?
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);
}
其實很簡單,就是以之前創(chuàng)建的文件為根目錄,再以我們xml中配置的path的值為目錄名創(chuàng)建一個新目錄。以external為例,path的值是".",那么最終文件的路徑就是/storage/emulated/0/.
然后調(diào)用addRoot方法,注意,這里會把文件的路徑轉(zhuǎn)成標(biāo)準(zhǔn)路徑,比如我上面的路徑轉(zhuǎn)成標(biāo)準(zhǔn)路徑,最后的點就沒有了,最后把文件存入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已經(jīng)創(chuàng)建完成,他是SimplePathStrategy,也是PathStrategy的實現(xiàn)類,調(diào)用了他的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是我們最初自己文件的路徑,我們的最終目的也就是要把這個文件的路徑轉(zhuǎn)為uri。然后循環(huán)剛才的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我們之前已經(jīng)分析過,只要傳入的參數(shù)是合法的,那么就是返回參數(shù)本身。所以path又變成了external/110.jpg。最后通過builder方法構(gòu)建了一個uri。我之前說過,builder構(gòu)建法直接看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,這個類之前已經(jīng)分析過了,那么最終的uri字符串就是content://包名$.FileProvider/external/110.jpg
源碼分析完了,我們發(fā)現(xiàn)其實fileProvider創(chuàng)建的uri跟Uri#fromFile創(chuàng)建的uri對象是一樣的,都是HierarchicalUri,只不過拼接規(guī)則不一樣,比如協(xié)議一個是file,一個是content;比如外部路徑一個是/storage/emulated/0,一個是/external,僅此而已。那7.0開始為什么要這么做呢?我們用一個7.0以上的手機(jī)用Uri#fromFile返回的uri來調(diào)用系統(tǒng)相機(jī),看看錯誤日志里的方法棧是怎么樣的。
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這行報錯了,為什么叫準(zhǔn)備離開進(jìn)程呢?因為下一行就是會通過binder去調(diào)用ams的startActivity,ams我們之前分析過不多說了,確實下一行代碼就離開我們自己的進(jìn)程了。
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獲取的包名是目標(biāo)包名,也就是系統(tǒng)相機(jī)的包名,很顯然跟context獲取的我們自己的包名,不同,所以leavingPackage為true。因為是跨包的,所以最后會調(diào)用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的協(xié)議是file,會直接拋異?!,F(xiàn)在總算是明白了,只要啟動的activity所在進(jìn)程的包名,和目標(biāo)activity所在進(jìn)程的包名不同相同,就會去做檢測uri的協(xié)議是不是file開頭的。這里注意,如果一個應(yīng)用開啟了多進(jìn)程,那么雖然是跨進(jìn)程了但是并不算跨包。