当前位置: 首页 > 工具软件 > svg-android > 使用案例 >

SVG-Android(资源替代详解)源码详解

邓正真
2023-12-01

上一篇文章讲到SVG-Android框架帮我们生成了svg.xml的渲染类来帮助我们画图,从而达到矢量图不失真的效果。如果没有看过上篇的小伙伴请先看上一篇博客SVG-Android(gradle插件生成器)源码详解,在生成好这些渲染类之后,只需要在在你的项目的Application中加入SVGLoader.load(this)加入这句代码,然后在控件上(在5.0以下系统)像使用普通的drable一样的使用svg.xml格式的文件了。

 ok,先来看看SVGLoader.load(this)帮我们做了什么,怎么有这么神奇的效果

 sPreloadedDrawables = SVGHelper.hackPreloadDrawables(context.getResources());
        if (sPreloadedDrawables == null) {
            return;
        }
        add(context, R.drawable.ic_android_red, SVGDrawable.SVGDrawableConstantState.create(new ic_android_red(context)));
......

省略号下的代码都是add,这里不再列出,这个方法做的工作就是将所有生成的渲染类加入集合,通过图片id标识。这里的sPreloadedDrawables 为LongSparseArray<Drawable.ConstantState>,那么它是怎么得到呢?SVGHelper.hackPreloadDrawables(context.getResources())进入这个方法一探究竟

  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            return hackPreloadDrawablesV15(res);
        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR2) {
            return hackPreloadDrawablesV18(res);
        } else {
            return hackPreloadDrawablesV19(res);
        }
很明显这里进行了版本判断,如果当前系统版本低于4.3用hackPreloadDrawablesV15,是4.3的话用hackPreloadDrawablesV18处理,否则用hackPreloadDrawablesV19处理,好,先从4.3以下版本看起

private static LongSparseArray<Drawable.ConstantState> hackPreloadDrawablesV15(Resources res) {
        try {
            Field field = Resources.class.getDeclaredField("sPreloadedDrawables");
            field.setAccessible(true);
            return (LongSparseArray<Drawable.ConstantState>) field.get(res);
        } catch (NoSuchFieldException e) {
            // ignore
        } catch (IllegalAccessException e) {
            // ignore
        } catch (IllegalArgumentException e) {
            // ignore
        }
        return null;
    }
这个方法很明显是利用反射获取到Framwork层Resources 对象的sPreloadedDrawables属性,既然上方采用了版本判断,那么说明这几个级别的android的系统的Resources源码略有不同,那么这个属性到底是做什么的呢,ok现在以4.0源码为例看看Resource究竟干了什么!打开Resources源码,果然有这么一个属性

 private static final LongSparseArray<Drawable.ConstantState> sPreloadedDrawables
            = new LongSparseArray<Drawable.ConstantState>();

这个属性只有在loadDrawable方法里面有使用,如下:

Drawable loadDrawable(TypedValue value, int id)
            throws NotFoundException {

        if (TRACE_FOR_PRELOAD) {
            // Log only framework resources
            if ((id >>> 24) == 0x1) {
                final String name = getResourceName(id);
                if (name != null) android.util.Log.d("PreloadDrawable", name);
            }
        }

        final long key = (((long) value.assetCookie) << 32) | value.data;
        boolean isColorDrawable = false;
        if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
                value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
            isColorDrawable = true;
        }
		//获得缓存中是否有drawable
        Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key);

        if (dr != null) {
            return dr;
        }
        //解析drable是哪种类型的
		//最重要的部分,记得application里面向集合添加的什么吗,其实实现了替换的作用
        Drawable.ConstantState cs = isColorDrawable ?
                sPreloadedColorDrawables.get(key) : sPreloadedDrawables.get(key);
        if (cs != null) {
            dr = cs.newDrawable(this);
        } else {
            if (isColorDrawable) {
                dr = new ColorDrawable(value.data);
            }

            if (dr == null) {
                if (value.string == null) {
                    throw new NotFoundException(
                            "Resource is not a Drawable (color or path): " + value);
                }

                String file = value.string.toString();

                if (TRACE_FOR_MISS_PRELOAD) {
                    // Log only framework resources
                    if ((id >>> 24) == 0x1) {
                        final String name = getResourceName(id);
                        if (name != null) android.util.Log.d(TAG, "Loading framework drawable #"
                                + Integer.toHexString(id) + ": " + name
                                + " at " + file);
                    }
                }

                if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie "
                        + value.assetCookie + ": " + file);

                if (file.endsWith(".xml")) {
                    try {
                        XmlResourceParser rp = loadXmlResourceParser(
                                file, id, value.assetCookie, "drawable");
                        dr = Drawable.createFromXml(this, rp);
                        rp.close();
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }

                } else {
                    try {
                        InputStream is = mAssets.openNonAsset(
                                value.assetCookie, file, AssetManager.ACCESS_STREAMING);
        //                System.out.println("Opened file " + file + ": " + is);
                        dr = Drawable.createFromResourceStream(this, value, is,
                                file, null);
                        is.close();
        //                System.out.println("Created stream: " + dr);
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }
                }
            }
        }

        if (dr != null) {
            dr.setChangingConfigurations(value.changingConfigurations);
            cs = dr.getConstantState();
            if (cs != null) {
                if (mPreloading) {
                    if (isColorDrawable) {
                        sPreloadedColorDrawables.put(key, cs);
                    } else {
                        sPreloadedDrawables.put(key, cs);
                    }
                } else {
                    synchronized (mTmpValue) {
                        //Log.i(TAG, "Saving cached drawable @ #" +
                        //        Integer.toHexString(key.intValue())
                        //        + " in " + this + ": " + cs);
                        if (isColorDrawable) {
                            mColorDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs));
                        } else {
                            mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs));
                        }
                    }
                }
            }
        }

        return dr;
    }

最重要的部分是第25行,还记得我们在Aplication添加的什么吗,再贴一下

add(context, R.drawable.ic_android_red, SVGDrawable.SVGDrawableConstantState.create(new ic_android_red(context)));
我们朝sPreloadedDrawables加入了若干我们自定义的VGDrawableConstantState,所以每次想获取svg.xml时都会调用我们自己的VGDrawableConstantState去创建Drable,从而实现替代作用,也就是现在经常提的黑科技,原理和插件化原理有点类似。这里需要注意一点SVGDrawableConstantStateextendsConstantState SVGDrawableConstantState 是基于系统的类。接下来看一下其他版本的获取,系统4.3版本的如下

 private static LongSparseArray<Drawable.ConstantState> hackPreloadDrawablesV18(Resources res) {
        try {
            Field field = Resources.class.getDeclaredField("sPreloadedDrawables");
            field.setAccessible(true);
            return ((LongSparseArray<Drawable.ConstantState>[]) field.get(res))[0];
        } catch (NoSuchFieldException e) {
            // ignore
        } catch (IllegalAccessException e) {
            // ignore
        } catch (IllegalArgumentException e) {
            // ignore
        }
        return null;
    }

系统4.4版本以上的获取

 private static LongSparseArray<Drawable.ConstantState> hackPreloadDrawablesV19(Resources res) {
        try {
            Method method = Resources.class.getDeclaredMethod("getPreloadedDrawables");
            method.setAccessible(true);
            return (LongSparseArray<Drawable.ConstantState>) method.invoke(res);
        } catch (NoSuchMethodException e) {
            // ignore
        } catch (IllegalAccessException e) {
            // ignore
        } catch (IllegalArgumentException e) {
            // ignore
        } catch (InvocationTargetException e) {
            // ignore
        }
        return null;
    }

这里注意了,如果我们想自己hook源码的话,必须得看多个版本的源码,因为Google每次升级都有可能改变源码的一些细节,导致hook源码在不同系统的不准确性,既然这部分被替换掉了,那么loadDrawable方法什么时候被调用呢?此时怎么想也应该在控件设置背景,或者src时传入drable的id的时候调用的,ok,以ImageView控件的setImageResource为例
public void setImageResource(@DrawableRes int resId) {
        // The resource configuration may have changed, so we should always
        // try to load the resource even if the resId hasn't changed.
        final int oldWidth = mDrawableWidth;
        final int oldHeight = mDrawableHeight;

        updateDrawable(null);
        mResource = resId;
        mUri = null;

        resolveUri();

        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
        }
        invalidate();
    }
这里的Drawable是通过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.
                mUri = null;
            }
        } else if (mUri != null) {
            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;
        }


最终Drawable是通过context的getDrawable方法将id转化为|Drawable的,而context的真正实现类为ContextImpl,为什么是它,如果你对这个不明白的话,请看一看关于app进程启动的入口类ActivityThread,不过这里最后的获取还是在父类Context中实现的,如下:

public final Drawable getDrawable(@DrawableRes int id) {
        return getResources().getDrawable(id);
    }

最后又交给Resources的getDrawable方法

 public Drawable getDrawable(int id) throws NotFoundException {
        synchronized (mTmpValue) {
            TypedValue value = mTmpValue;
            getValue(id, value, true);
            return loadDrawable(value, id);
        }
    }
看到了什么,最后还是调用了loadDrawable方法来加载drable,而调用这个方法的时候集合中已经存在数据,所以就会加载我们已经加载到集合的SVGDrawableConstantState类,然后通过

 
public Drawable newDrawable() {
    return new SVGDrawable(this);
}

获取SVGDrawable类,ok,最后的Drable类是框架中的一个类SVGDrawable,这又设计到自定义Drawable的知识了,这一块小伙伴们可以查看文档或者百度一下,这里不再阐述,
自定义Drawable最重要的一个方法就是Drawable,最后是调用的draw方法将需要的图形画入,好看一下


 public void draw(@NonNull Canvas canvas) {
        // We will offset the bounds for draw, so copyBounds() here instead
        // of getBounds().
        copyBounds(mTmpBounds);
        if (mTmpBounds.width() <= 0 || mTmpBounds.height() <= 0) {
            // Nothing to draw
            return;
        }

        // Color filters always override tint filters.
        final ColorFilter colorFilter = (mColorFilter == null ? mTintFilter : mColorFilter);

        canvas.getMatrix(mTmpMatrix);
        mTmpMatrix.getValues(mTmpFloats);
        float canvasScaleX = Math.abs(mTmpFloats[Matrix.MSCALE_X]);
        float canvasScaleY = Math.abs(mTmpFloats[Matrix.MSCALE_Y]);

        float canvasSkewX = Math.abs(mTmpFloats[Matrix.MSKEW_X]);
        float canvasSkewY = Math.abs(mTmpFloats[Matrix.MSKEW_Y]);

        // When there is any rotation / skew, then the scale value is not valid.
        if (canvasSkewX != 0 || canvasSkewY != 0) {
            canvasScaleX = 1.0f;
            canvasScaleY = 1.0f;
        }

        int scaledWidth = (int) (mTmpBounds.width() * canvasScaleX);
        int scaledHeight = (int) (mTmpBounds.height() * canvasScaleY);

        if (scaledWidth <= 0 || scaledHeight <= 0) {
            return;
        }

        final int saveCount = canvas.save();
        canvas.translate(mTmpBounds.left, mTmpBounds.top);

        // Handle RTL mirroring.
        final boolean needMirroring = needMirroring();
        if (needMirroring) {
            canvas.translate(mTmpBounds.width(), 0);
            canvas.scale(-1.0f, 1.0f);
        }

        // At this point, canvas has been translated to the right position.
        // And we use this bound for the destination rect for the drawBitmap, so
        // we offset to (0, 0);
        mTmpBounds.offsetTo(0, 0);

        // Use the renderer to draw.
        mState.mRenderer.draw(canvas, scaledWidth, scaledHeight, colorFilter, mTmpBounds);

        canvas.restoreToCount(saveCount);
    }
直接定位位到321行mState.mRenderer.draw(canvas, scaledWidth, scaledHeight, colorFilter, mTmpBounds),最后的画图是在Gradle为每一个svg.xml生成的渲染类实现的,也就是把svg.xml文件中的path标签中的路径映射到渲染类的Path路径中,最后通过SVGDrawable间接将路径画出来,从而实现矢量图的呈现。从最上面的版本判断可以看出,作者并没有看4.0以下的源码是怎么加载Drawable的,所以说此框架只兼容到4.0,不过以现在的手机市场,版本系统所占的比例,也没有必用兼容到4.0以下的了。









 

 类似资料: