前言
APP更换皮肤的方式有很多,如系统自带的黑夜模式、插件换肤、通过下发配置文件加载不同主题等等,我们这里就浅谈下插件换肤方式。想实现插件换肤功能,我们就需要先弄清楚 :APP是如何完成资源加载的。
资源加载流程
这里我们以ImageView加载图片
来进行分析,我们先看下ImageView获取drawable的源码:
public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initImageView();
...
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
...
}
重点在a.getDrawable(R.styleable.ImageView_src)
这段代码,我们继续跟进:
TypedArray.getDrawable()
public Drawable getDrawable(@StyleableRes int index) {
return getDrawableForDensity(index, 0);
}
TypedArray.getDrawableForDensity()
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
...
return mResources.loadDrawable(value, value.resourceId, density, mTheme);
}
return null;
}
Resources.loadDrawable()
Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme) throws NotFoundException {
return mResourcesImpl.loadDrawable(this, value, id, density, theme);
}
ResourcesImpl.loadDrawable()
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws NotFoundException {
...
//如果使用缓存,先从缓存中取cachedDrawable
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
if (cachedDrawable != null) {
cachedDrawable.setChangingConfigurations(value.changingConfigurations);
return cachedDrawable;
}
}
...
// 重点就是loadDrawableForCookie方法
dr = loadDrawableForCookie(wrapper, value, id, density);
...
}
}
从上面我们可以看到,资源加载通过Resources
这个类,而它又将任务交给它的实现类ResourcesImpl
,我们重点分析下ResourcesImpl.loadDrawableForCookie
方法:
private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density) {
...
final String file = value.string.toString();
...
try {
//加载xml资源,如drawable下定义的shape.xml文件
if (file.endsWith(".xml")) {
final String typeName = getResourceTypeName(id);
if (typeName != null && typeName.equals("color")) {
dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
} else {
dr = loadXmlDrawable(wrapper, value, id, density, file);
}
} else {
//通过mAssets(AssetManager类型)打开资源文件流实现加载
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
final AssetInputStream ais = (AssetInputStream) is;
dr = decodeImageDrawable(ais, wrapper, value);
}
} finally {
stack.pop();
}
} catch (Exception | StackOverflowError e) {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
final NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
...
}
}
return dr;
}
这里我们可以看到最终是交给AssetManager来进行资源文件访问,读取数据流完成资源加载
。
通过上面源码分析,我们知道可以通过Resources
来实现资源加载,那系统中Resources
又是如何创建的呢?
Resources创建流程分析
我们在代码中经常这样使用:context.getResources().getDrawable()
,那我们就从context的实现类ContextImpl
抓起:
### ContextImpl
public Context createApplicationContext(ApplicationInfo application, int flags)
throws NameNotFoundException {
// 找到createResources方法
c.setResources(createResources(mToken, pi, null, displayId, null,
getDisplayAdjustments(displayId).getCompatibilityInfo(), null));
if (c.mResources != null) {
return c;
}
}
}
createResources方法跟进
private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo,
List<ResourcesLoader> resourcesLoader) {
final String[] splitResDirs;
final ClassLoader classLoader;
try {
splitResDirs = pi.getSplitPaths(splitName);
classLoader = pi.getSplitClassLoader(splitName);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
return ResourcesManager.getInstance().getResources(activityToken,
pi.getResDir(),
splitResDirs,
pi.getOverlayDirs(),
pi.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfig,
compatInfo,
classLoader,
resourcesLoader);
}
ResourcesManager
的getResources方法:
public @Nullable Resources getResources(
@Nullable IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader,
@Nullable List<ResourcesLoader> loaders) {
try {
...
return createResources(activityToken, key, classLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
createResources方法如下:
private @Nullable Resources createResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
...
// Resources的创建需要resourcesImpl
ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
if (resourcesImpl == null) {
return null;
}
if (activityToken != null) {
return createResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
} else {
return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
}
}
createResourcesLocked方法如下:
private @NonNull Resources createResourcesLocked(@NonNull ClassLoader classLoader,
@NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
cleanupReferences(mResourceReferences, mResourcesReferencesQueue);
//系统源码中其实就是通过classLoader直接new了一个Resources,并初始化了resourcesImpl方便后续资源加载
Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
: new Resources(classLoader);
resources.setImpl(impl);
resources.setCallbacks(mUpdateCallbacks);
mResourceReferences.add(new WeakReference<>(resources, mResourcesReferencesQueue));
if (DEBUG) {
Slog.d(TAG, "- creating new ref=" + resources);
Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
}
return resources;
}
通过上面源码分析,我们可以得出结论:在ApplicationContext创建的时候,就完成了Resources的创建,创建是通过ResourcesManager来完成的。
那我们是不是就可以通过创建新的Resources
来实现插件中资源的访问呢!!
插件换肤案例
我们先看下Resources
的构造方法:
@Deprecated
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
/**
* @hide
*/
@UnsupportedAppUsage
public Resources(@Nullable ClassLoader classLoader) {
mClassLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;
}
/**
* Only for creating the System resources.
*/
@UnsupportedAppUsage
private Resources() {
this(null);
final DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
final Configuration config = new Configuration();
config.setToDefaults();
mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
new DisplayAdjustments());
}
这里有三个构造方法,由于我们需要加载插件中的资源文件,通过上面的分析,我们知道资源访问是需要通过AssetManager
来完成的,因此我们使用Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)
这个方式来完成插件资源加载:
private lateinit var iv: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
iv = findViewById<ImageView>(R.id.iv)
iv.setImageDrawable(getDrawable(R.drawable.b))
findViewById<Button>(R.id.btn).setOnClickListener {
//更新皮肤
updateSkin()
}
}
private fun updateSkin() {
//反射调用AssetManager的addAssetPath方法
val assetMangerClazz = AssetManager::class.java
val assetManger = assetMangerClazz.newInstance()
//皮肤存放在当前包路径下
val skinPath = filesDir.path + File.separator + "skin.skin"
val method = assetMangerClazz.getDeclaredMethod("addAssetPath", String::class.java)
method.isAccessible = true
method.invoke(assetManger, skinPath)
//创建皮肤的Resources对象
val skinResources = Resources(assetManger, resources.displayMetrics, resources.configuration)
//通过资源名称,类型,包获取Id
val skinId = skinResources.getIdentifier("a", "drawable", "com.crystal.skin")
val skinDrawable = skinResources.getDrawable(skinId, null)
iv.setImageDrawable(skinDrawable)
}
测试效果:
总结
通过源码分析,了解了资源加载的基本流程,对插件换肤的实现有了进一步的认知。
参考文档
插件式换肤框架搭建 - 资源加载源码分析
结语
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )