美文网首页
一个Uri引起的思考

一个Uri引起的思考

作者: kwbsky | 来源:发表于2019-12-05 18:14 被阅读0次

    今天有同事问我问题,调用系统分享,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开头的。这里注意,如果一个应用开启了多进程,那么虽然是跨进程了但是并不算跨包。

    相关文章

      网友评论

          本文标题:一个Uri引起的思考

          本文链接:https://www.haomeiwen.com/subject/gjtjgctx.html