欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Uri内部处理流程分析

程序员文章站 2022-03-10 18:07:08
...

*本篇文章已授权微信公众号guolin_blog (郭霖) 独家发布


 

前几天在讨论一个很有趣的事情:同事在交流时发现Uri在parse生成时,在里面加入一段其他无关的字符串,同样可以得到想要的值,这么说不太直观,直接上代码吧:

ImageView img = findViewById(R.id.img);

Uri uri =Uri.parse("android.resource://"+getPackageName()+R.drawable.mkf);
img.setImageURI(uri);

简单说明一下:要做的是通过一个Uri去给ImageView控件添加src图片。通过setImageURI的方法。

在过程中发现了一个很有趣的问题:上述代码如果改成这几个样子,同样可以达到效果:

Uri uri =Uri.parse("android.resource://"+getPackageName()+"/adasdasd/"+R.drawable.mkf);

Uri uri =Uri.parse("android.resource://"+getPackageName()+"/我就是瞎写的/"+R.drawable.mkf);

Uri uri =Uri.parse("android.resource://"+getPackageName()+"//"+R.drawable.mkf);

发现在getPackageName后面添加一段什么东西(但是格式必须是前面有‘/’),都会成功的把图片加载出来。当时就感觉很奇怪,于是拔了一下Uri关于其中的部分源码,最后知道了其中的所以然,伙伴们不要着急哈,让我慢慢道来:


关于Uri的介绍

可能有些朋友对Uri只是使用过,但是不知道到底是个什么东西,在这里笔者简单的说明一下。Uri全称为统一资源标识符(Uniform Resource Identifier),这里不想扯太多,简单理解的话就是对于一个资源名称的命名,如果有想详细了解的话请看这个大神的博客:Java魔法堂:URI、URL(含URL Protocol Handler)和URN

而Java中还有一个URI(全大写),他们两个都是统一资源标识符,只是Uri是在URI的基础上拓展了一些属性,用来适用于Android开发而已。

Uri结构

从结构程度上,一般可以把Uri的结构分为三种:

1.基本结构:这种结构是Uri最简单的结构,分为三个部分

[scheme:]scheme-specific-part[#fragment]

2.进一步划分:将scheme-specific-part进一步划分为了authority、path、query

[scheme:][//authority][path][?query][#fragment]

3.最终结构:将authority划分为host和post

[scheme:][//host:port][path][?query][#fragment]

举个例子来说明:

https://mp.csdn.net/postedit/81806443
scheme: https:
authority(host) //mp.csdn.net
path /postedit/81806443

简单说明一下规则:

  • 对于Uri,scheme和authority是不能省略的,其他的部分可以有也可以没有。
  • 一般authority中如果没有出现post:host格式,都是post出现
  • path和query可以连缀重复出现,其中query需要通过&俩连缀,而path只是继续添加/

好了,对Uri的介绍就到这里,下面是我们的追源码时间。


处理流程

既然Uri是我们创建的,我们就从parse方法看起:

    public static Uri parse(String uriString) {
        return new StringUri(uriString);
    }

他返回了一个StringUri对象,点进构造方法我们看一下:

    private StringUri(String uriString) {
        if (uriString == null) {
            throw new NullPointerException("uriString");
        }

        this.uriString = uriString;
    }

他只是将传入的String赋值给了这个StringUri,让他储存起来。

在这里有必要说一下StringUri是个什么东西。对于Uri来说,本身是有一定限制的,比如无法实现层次化和透明化的Uri,但是StringUri在其基础上继承了AbstractHierarchicalUri类,从而达到可以层次化、透明的效果,关于层次、透明具体可以看上面的那篇文章。

对于StringUri只需要知道他只是Uri的一个子类,上面的如果不想深入只了解一下就可以的。

看来我们parse方法没有干什么大事,就是原封不动的全扔进去了,看来我们只能从ImageView的setImageURI方法入手了:

    public void setImageURI(@Nullable Uri uri) {
        if (mResource != 0 || (mUri != uri && (uri == null || mUri == null || !uri.equals(mUri)))) {
            updateDrawable(null);
            mResource = 0;
            mUri = uri;

            final int oldWidth = mDrawableWidth;
            final int oldHeight = mDrawableHeight;

            resolveUri();

            if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
                requestLayout();
            }
            invalidate();
        }
    }

首先说一下这个方法大体流程:

  1. 一开始对ImageView的uri和res属性以及参数uri进行判空操作,相信这个部分大家都还是可以理解的。
  2. 然后执行了undateDrawable方法,由于今天我们的主场是Uri,所以这些方法只是说一下功能就好,首先在updateDrawable方法传入null,是为了让如果之前存在视图资源,就隐藏显示。
  3. resolveUri是我们等会儿重点看的方法,
  4. 与之前宽高不一样话会重新执行onLayout方法重新放置布局。
  5. invalidate最后调用onDraw方法重新绘制

接下来我们看一下resolveUri方法是如何实现的:

    private void resolveUri() {
        if (mDrawable != null) {
            return;
        }

        if (getResources() == null) {
            return;
        }

        Drawable d = null;

        if (mResource != 0) {
            try {
                d = mContext.getDrawable(mResource);
            } catch (Exception e) {
                Log.w(LOG_TAG, "Unable to find resource: " + mResource, e);
                // Don't try again.
                mResource = 0;
            }
        } else if (mUri != null) {
            //我们是要从这里开始看的,因为我们并没有给Img传入res资源值
            d = getDrawableFromUri(mUri);

            if (d == null) {
                Log.w(LOG_TAG, "resolveUri failed on bad bitmap uri: " + mUri);
                // Don't try again.
                mUri = null;
            }
        } else {
            return;
        }

        updateDrawable(d);
    }

这个方法的整体流程就是获取Drawable,最后更新Img的图片资源,不用说太多了。我们需要看的是mUri属性,因为在之前mUri属性已经获取到传入的uri的引用。

接下来进入getDrawableFromUri方法:

    private Drawable getDrawableFromUri(Uri uri) {
        final String scheme = uri.getScheme();
        if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
            try {
                // Load drawable through Resources, to get the source density information
                ContentResolver.OpenResourceIdResult r =
                        mContext.getContentResolver().getResourceId(uri);
                return r.r.getDrawable(r.id, mContext.getTheme());
            } catch (Exception e) {
                Log.w(LOG_TAG, "Unable to open content: " + uri, e);
            }
        } 

        /*中间删除了两个部分,因为没有用到*/
        return null;
    }

首先通过uri.getScheme方法获取到uri的scheme段,我们看一下这个方法如何实现的:

    public String getScheme() {
        @SuppressWarnings("StringEquality")
        boolean cached = (scheme != NOT_CACHED);
        return cached ? scheme : (scheme = parseScheme());
    }

NOT_CHANGED状态是scheme初始化的状态,代表没有获取过scheme,接下来要通过parseScheme方法获取scheme字段,点进去看一下:

    private String parseScheme() {
        int ssi = findSchemeSeparator();
        return ssi == NOT_FOUND ? null : uriString.substring(0, ssi);
    }

进入findSchemeSeparator方法:

    private int findSchemeSeparator() {
        return cachedSsi == NOT_CALCULATED
                ? cachedSsi = uriString.indexOf(':')
                : cachedSsi;
    }

看来这个方法是从StringUri收到的String字段,查找':'的位置。

回到parseScheme方法,如果查到了':'的位置就返回起始位置到':'位置的字符串,其中String.subString是返回两个索引之间的子串。

 

接下来我们回到了getDrawableFromUri方法,现在已经获取到了scheme字段(如果有的话)。

    private Drawable getDrawableFromUri(Uri uri) {
        final String scheme = uri.getScheme();
        if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
            try {
                // Load drawable through Resources, to get the source density information
                ContentResolver.OpenResourceIdResult r =
                        mContext.getContentResolver().getResourceId(uri);
                return r.r.getDrawable(r.id, mContext.getTheme());
            } catch (Exception e) {
                Log.w(LOG_TAG, "Unable to open content: " + uri, e);
            }
        } 

        /*中间删除了两个部分,因为没有用到*/
        return null;
    }
public static final String SCHEME_ANDROID_RESOURCE = "android.resource";

其中这个字串就是android.resource,所以这个只是判断scheme是不是为这个,接下来获取了内容提供器ContentProvider,执行了他的getResourceId方法,我们进去看一下:

    public OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException {
        String authority = uri.getAuthority();
        Resources r;
        /*......*/
        List<String> path = uri.getPathSegments();
        if (path == null) {
            throw new FileNotFoundException("No path: " + uri);
        }
        int len = path.size();
        int id;
        if (len == 1) {
            try {
                id = Integer.parseInt(path.get(0));
            } catch (NumberFormatException e) {
                throw new FileNotFoundException("Single path segment is not a resource ID: " + uri);
            }
        } else if (len == 2) {
            id = r.getIdentifier(path.get(1), path.get(0), authority);
        } else {
            throw new FileNotFoundException("More than two path segments: " + uri);
        }
        if (id == 0) {
            throw new FileNotFoundException("No resource found for: " + uri);
        }
        OpenResourceIdResult res = new OpenResourceIdResult();
        res.r = r;
        res.id = id;
        return res;
    }

一开始获取authority字段,其实这个字段是获取只是为了判断是不是有这个字段,没有的话会抛一个异常,反之会给r(Resource)属性赋值,这部分代码被我删掉了,大家可以自行查看一下。但是还是看一下getAuthority方法吧哈哈:

    public String getAuthority() {
        return getAuthorityPart().getDecoded();
    }

看来又是很多方法循环套用了,没事我们稳住慢慢看,先从getAuthorityPart开始看:

    private Part getAuthorityPart() {
        if (authority == null) {
            String encodedAuthority
                    = parseAuthority(this.uriString, findSchemeSeparator());
            return authority = Part.fromEncoded(encodedAuthority);
        }

        return authority;
    }

进入parseAuthority方法会获取到authority的string字段,在这里出现了一个熟悉的方法——findSchemeSeparator,这个方法是获取到了scheme后出现的冒号的索引。由于之前已经获取到了,所以这里就直接返回刚才的索引了,我们看一下parseAuthority方法:

    static String parseAuthority(String uriString, int ssi) {
        int length = uriString.length();

        // If "//" follows the scheme separator, we have an authority.
        if (length > ssi + 2
                && uriString.charAt(ssi + 1) == '/'
                && uriString.charAt(ssi + 2) == '/') {
            // 如果:后面的两个字符是//说明是存在authority的
            //在遇到/,?,#字符,分别代表进入了path、query、fragment的部分,说明authority部分结束
            int end = ssi + 3;
            LOOP: while (end < length) {
                switch (uriString.charAt(end)) {
                    case '/': // Start of path
                    case '?': // Start of query
                    case '#': // Start of fragment
                        break LOOP;
                }
                end++;
            }

            return uriString.substring(ssi + 3, end);
        } else {
            return null;
        }

    }

这个在备注中写的比较详细了,所以不同多说太多了吧。最后他返回的是authority的字段。

    private Part getAuthorityPart() {
        if (authority == null) {
            String encodedAuthority
                    = parseAuthority(this.uriString, findSchemeSeparator());
            return authority = Part.fromEncoded(encodedAuthority);
        }

        return authority;
    }

回到getAuthorityPart方法,注意返回的是Authority的Part格式,对于Part类其实没什么多说的,他是各个字段的基类,继承自AbstractPart。

这里对Part的fromEncode方法简单说明一下:他会把对应部分string字段放入一个新的Part中,对于的属性为encode。感兴趣的朋友可以自行查看源码。

    public String getAuthority() {
        return getAuthorityPart().getDecoded();
    }

现在跳回到getAuthority方法,现在已经获取到了Authority的Part类,调用getDecoded方法:

    final String getDecoded() {
        @SuppressWarnings("StringEquality")
        boolean hasDecoded = decoded != NOT_CACHED;
        return hasDecoded ? decoded : (decoded = decode(encoded));
    }

上面说到会将字段放入encoded属性中,decoded和encoded属性默认都是NOT_CHANGED,所以这里最后返回的还是之前Authority的String字段。

关于获取Authority字段的部分就结束了。我们接着往下走。


    public OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException {
        String authority = uri.getAuthority();
        Resources r;
        /*......*/
        List<String> path = uri.getPathSegments();
        if (path == null) {
            throw new FileNotFoundException("No path: " + uri);
        }
        int len = path.size();
        int id;
        if (len == 1) {
            try {
                id = Integer.parseInt(path.get(0));
            } catch (NumberFormatException e) {
                throw new FileNotFoundException("Single path segment is not a resource ID: " + uri);
            }
        } else if (len == 2) {
            id = r.getIdentifier(path.get(1), path.get(0), authority);
        } else {
            throw new FileNotFoundException("More than two path segments: " + uri);
        }
        if (id == 0) {
            throw new FileNotFoundException("No resource found for: " + uri);
        }
        OpenResourceIdResult res = new OpenResourceIdResult();
        res.r = r;
        res.id = id;
        return res;
    }

接着调用了uri的getPathSegments方法,该方法返回了一字符串集合,我们点进去看一下:

    public List<String> getPathSegments() {
        return getPathPart().getPathSegments();
    }

getPathPart方法格式跟getAuthorityPart方法很像,最终是获取到了Path的字段,然后封装成Part类,这个方法就不介绍了,大家下去自行查看源码。我们要说的是后面的getPathSegments:

        PathSegments getPathSegments() {
            if (pathSegments != null) {
                return pathSegments;
            }

            String path = getEncoded();//获取到path的String字段
            if (path == null) {
                return pathSegments = PathSegments.EMPTY;
            }

            PathSegmentsBuilder segmentBuilder = new PathSegmentsBuilder();

            int previous = 0;
            int current;
            while ((current = path.indexOf('/', previous)) > -1) {
                //首先拿到每一个path段的开头,也就是‘/’。
                if (previous < current) {
                    String decodedSegment
                            = decode(path.substring(previous, current));
                    segmentBuilder.add(decodedSegment);
                }
                previous = current + 1;
            }

            // 把每一个path字段添加
            if (previous < path.length()) {
                segmentBuilder.add(decode(path.substring(previous)));
            }

            //通过构建者创建PathSegmentsBuilder对象
            return pathSegments = segmentBuilder.build();
        }

PathSegments是继承List的一个集合,其他的注释中写的应该很清楚了。

这样我们就获取到了Path的每一个字段的集合。回到getResorceId方法:

    public OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException {
        String authority = uri.getAuthority();
        Resources r;
        /*......*/
        List<String> path = uri.getPathSegments();
        if (path == null) {
            throw new FileNotFoundException("No path: " + uri);
        }
        int len = path.size();
        int id;
        if (len == 1) {
            try {
                id = Integer.parseInt(path.get(0));
            } catch (NumberFormatException e) {
                throw new FileNotFoundException("Single path segment is not a resource ID: " + uri);
            }
        } else if (len == 2) {
            id = r.getIdentifier(path.get(1), path.get(0), authority);
        } else {
            throw new FileNotFoundException("More than two path segments: " + uri);
        }
        if (id == 0) {
            throw new FileNotFoundException("No resource found for: " + uri);
        }
        OpenResourceIdResult res = new OpenResourceIdResult();
        res.r = r;
        res.id = id;
        return res;
    }

下面对path进行长度判断,发现最多只有两个,超过两个path的话会直接抛出异常,一个的话会直接把path转换成资源id(int值)。两个的话也是把第二个path作为资源id,我们可以点进getIndentifier方法看一下他的介绍:

/**
     * Return a resource identifier for the given resource name.  A fully
     * qualified resource name is of the form "package:type/entry".  The first
     * two components (package and type) are optional if defType and
     * defPackage, respectively, are specified here.
     * 
     * <p>Note: use of this function is discouraged.  It is much more
     * efficient to retrieve resources by identifier than by name.
     * 
     * @param name The name of the desired resource.
     * @param defType Optional default resource type to find, if "type/" is
     *                not included in the name.  Can be null to require an
     *                explicit type.
     */

因为这个方法最后追到了JNI的部分,本人也是很无奈啊,总之根据备注是把第一个path作为了资源id。


现在再回到我们最开始的问题,无论我们在前面的那块输入什么内容,其实最后都是指向了R.drawable.mkf,所以能够显示出来图片也是很正常的啦。

从这个问题我们从头到尾走了一遍Uri在调用的时候内部是自己如何处理的。相信大家现在已经很明确了吧。

 

关于这篇文章就到这里了,如果有不同观点或者发现问题的朋友希望多多支出!!!!!