插件换肤以及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