插件换肤以及Resources源码分析(一)
插件换肤:从后台下载的皮肤文件,其实都是一个打包的apk,接着在它没有安装以及运行的情况下,获取改皮肤文件中的资源属性,并且不需要重启当前app即可更换资源。
所以我们面临一个这样的问题:
如何获取另一个apk中的资源?
在这之前,我们先看看系统是怎么加载资源文件的,以ImageView为例。
<ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" 
    android:src="@drawable/ic_back"
    />
通过配置ImageView的src,我们可以获取不同的图片,但是他是怎么加载进来的呢?我们点击ImageView源码可以看到:
 final TypedArray a = context.obtainStyledAttributes
     (attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
可以看到,是调用了TypedArray的getDrawable方法,我们接着点进去看看。
 public Drawable getDrawable(@StyleableRes int index) {
        if (mRecycled) {
            throw new RuntimeException("Cannot make calls to a recycled instance!");
        }
        final TypedValue value = mValue;
        if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
            if (value.type == TypedValue.TYPE_ATTRIBUTE) {
                throw new UnsupportedOperationException(
                        "Failed to resolve attribute at index " + index + ": " + value);
            }
            return mResources.loadDrawable(value, value.resourceId, mTheme);
        }
        return null;
    }
最终调用了Resources.loadDrawable方法。
mResources.loadDrawable(value, value.resourceId, mTheme);
还有颜色的获取也是,都是通过Resources来获取的。
Resources.loadColorStateList(value, value.resourceId, mTheme);
还记得平常我们在activity自己获取资源文件是怎么写的,是不是也是通过最终调用了Resources
(两个api在api22过时,其实没什么影响,只是多传了theme,防止当传递的资源ID是另一个Drawable资源的别名,造成获取对象不正确的问)
getResources().getDrawable(R.drawable.ic_back);
getResources().getColor(R.color.colorAccent);
他们最终分别调用了上面Resources的方法。
loadDrawable(value, value.resourceId, mTheme);
loadColorStateList(value, value.resourceId, mTheme);
猜想:
既然资源的加载都是通过Resources类,那如果我们想获取别的apk中的资源,是不是可以自己实例化一个Resources对象呢?
我们先看看系统时怎么实现的。
public abstract Resources getResources();
首先它时Context中的抽象方法,在ContextImp类中实现了该方法
 @Override
    public Resources getResources() {
        return mResources;
    }
    ......
     Resources resources = packageInfo.getResources(mainThread);
     mResources =resources;
一系列的调用之后 最终
Resources resources = new Resources(classLoader)
resources.setImpl(impl);
是隐藏的构造方法,我们看它的第二个够造方法
  @Deprecated
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }
第一个构造方法和第二个构造方法其实都是创建了ResourcesImpl的实例,只要我们把这个实例创建出来,调用哪个构造参数其实都一样。(也可以创建ResourcesImpl实例,但是需要反射的太多)
我们分别来看这三个参数
- AssetManager assets
在ResourcesManager和AssetManager的源码中得知
    //ResourcesManager
    @VisibleForTesting
    protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();
        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (key.mResDir != null) {
            if (assets.addAssetPath(key.mResDir) == 0) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }
        ......
    }
    //AssetManager 
    /**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {
        return  addAssetPathInternal(path, false);
    }
     private final int addAssetPathInternal(String path, boolean appAsLib) {
        synchronized (this) {
            int res = addAssetPathNative(path, appAsLib);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }
    private native final int addAssetPathNative(String path, boolean appAsLib);
通过assets.addAssetPath(),来获取资源id,可以是一个目录或者一个ZIP文件,而apk文件就是一个ZIP。
- DisplayMetrics metrics
- Configuration config
在ResourcesImpl中,所以我们也照着new就行了
 private final Configuration mTmpConfig = new Configuration();
 private final DisplayMetrics mMetrics = new DisplayMetrics();
Demo代码如下:
- Step 1- try { // 反射获取AssetManager对象 Class<AssetManager> managerClass = AssetManager.class; AssetManager asset = managerClass.newInstance(); //执行addAssetPath方法。 Method method = managerClass.getDeclaredMethod("addAssetPath", String.class); //后缀名随便命名,这里命名.skin method.invoke(managerClass, Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator+"test.skin"); Resources resources=new Resources(asset,new DisplayMetrics(),new Configuration()); //皮肤文件资源名 资源类型 皮肤文件包名 int id = resources.getIdentifier("test_src", "drawable", "livesun.skintest"); resources.getDrawable(id); } catch (Exception e) { e.printStackTrace(); }
- Step 2
打包皮肤apk 命名为test.sikn

总结
- 自己创建Resouerces的实例
- 通过AssetManager加载皮肤路径
- resources.getIdentifier()获取资源id
