1.适用场景
所谓的换肤,更换主题,其实是对资源文件的替换,或者说使用额外的同id的资源文件进行更新;而换肤能操作的也只能是资源,比如颜色,图片,必须是应用中预先定义好id的资源类型。
也就是说,换主题是要额外的外置资源插件,可以是apk文件,而这个插件中有和应用内相同id名称的资源文件。当需要更换主题时,去加载这个插件,得到一个新的Resources
对象(这个和原应用的进行区分,同时保留,根据特定的id名称进行查找替换)
, 重新加载或刷新页面,如果有插件中同名的资源id的文件,那么就使用主题的Resources
,否则使用原来的Resources
2. Resources
我们在使用资源文件时,最常用的方式就是从预先的资源文件中进行获取,通常使用context.getResources()
这个方法,那么这个方法是怎么定位到资源的呢?
说起Context
,我们查看源码时基本上都只会盯着ContextImpl
这个实现类进行,这个基本上算是Context
的最核心的实现类了,就比如Window
我们只会盯着PhoneWindow
一样。
class ContextImpl extends Context {
private @NonNull Resources mResources;
......
@Override
public Resources getResources() {
return mResources;
}
void setResources(Resources r) {
if (r instanceof CompatResources) {
((CompatResources) r).setContext(this);
}
mResources = r;
}
}
在ContextImpl
中并没有具体创建Resources
的代码,那么我就得去源头去理,资源文件的初始化应该是在app创建时候进行的,而这个的创建应该也是伴随着Context
一起进行创建,那么就去ActivityThread
中去查找。
private void attach(boolean system, long startSeq) {
sCurrentActivityThread = this;
mSystemThread = system;
if (!system) {
......
RuntimeInit.setApplicationObject(mAppThread.asBinder());
final IActivityManager mgr = ActivityManager.getService();
try {
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
......
}
因为我们创建的是非system
的应用,那么再这里会通过ActivityManager.getService()
也就是ActivityManagerService
中的attachApplication
方法去绑定一个Application
而这里的attachApplication
会进行一些初始化参数设置后,回调到ActivityThread
的bindApplication
发送一个BIND_APPLICATION
的Message
,然后最终走到ActivityThread
中的handleBindApplication
方法中来
private void handleBindApplication(AppBindData data) {
......
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
......
Application app;
try {
app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
......
try {
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
}
......
}
public Application getApplication() {
return mInitialApplication;
}
这里会通过makeApplication
方法创建一个Application
对象,这个也是我们平时获取到那个App
,然后调用callApplicationOnCreate
方法去执行Application
的onCreate
方法。
这里的data.info
比较重要,后面会使用到,data.info
本身是一个LoadedApk
对象
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
java.lang.ClassLoader cl = getClassLoader();
......
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
}
mActivityThread.mAllApplications.add(app);
mApplication = app;
.....
return app;
}
这个方法最终会调用ContextImpl
的createAppContext
方法中来,使用这个创建一个Context
,然后把这个传给Instrumentation
的newApplication
方法创建最终的App
对象出来。
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = getFactory(context.getPackageName())
.instantiateApplication(cl, className);
app.attach(context);
return app;
}
这里的attach
方法中调用了我们经常重写的attachBaseContext
方法,所以attachBaseContext
方法可以认为是资源初始化完成后马上调用的一个方法。
回到上面的createAppContext
方法中
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
return createAppContext(mainThread, packageInfo, null);
}
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
String opPackageName) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
null, opPackageName);
context.setResources(packageInfo.getResources());
return context;
}
这里可以看到我们上面的设置资源的方法setResources
,而资源是通过packageInfo
获取的
注意到执行ContextImpl.createAppContext(mActivityThread, this);
传入了一个this
,这个对应了上面创建的data.info
,也就是LoadedApk
对象,所以getResource
方法是
public Resources getResources() {
if (mResources == null) {
final String[] splitPaths;
try {
splitPaths = getSplitPaths(null);
} catch (NameNotFoundException e) {
throw new AssertionError("null split not found");
}
mResources = ResourcesManager.getInstance().getResources(null, mResDir,
splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
getClassLoader());
}
return mResources;
}
而这个ResourcesManager.getInstance().getResources()
最终会通过getOrCreateResources
方法去创建一个Resources
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
.....
//前面的代码是一些缓存的查找
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
// Add this ResourcesImpl to the cache.
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
final Resources resources;
if (activityToken != null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
return resources;
}
}
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
return impl;
}
其实这里可以看出Resources
的核心是mResourcesImpl
,Resoureces
可以看成一个代理方式,里面的逻辑都交给Impl去处理,比如getDrawable
最终会执行到这个方法
public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValueForDensity(id, density, value, true);
return impl.loadDrawable(this, value, id, density, theme);
} finally {
releaseTempTypedValue(value);
}
}
那么我们就可以猜想,只要能创建一个ResourceImpl
,那么就能创建Resources
,如果能动态切换系统加载的资源,就能实现更换主题的功能了,那么问题回到上面,注意到ResourceImpl
中有一个核心的AssetManager
对象,只要能创建这个对象,那么就能创建ResourceImpl
了吧
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
final AssetManager.Builder builder = new AssetManager.Builder();
if (key.mResDir != null) {
try {
builder.addApkAssets(loadApkAssets(key.mResDir, false /
} catch (IOException e) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
}
if (key.mSplitResDirs != null) {
for (final String splitResDir : key.mSplitResDirs) {
try {
builder.addApkAssets(loadApkAssets(splitResDir, false
} catch (IOException e) {
Log.e(TAG, "failed to add split asset path " + splitResDir);
return null;
}
}
}
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
try {
builder.addApkAssets(loadApkAssets(idmapPath, false
} catch (IOException e) {
Log.w(TAG, "failed to add overlay path " + idmapPath);
}
}
}
if (key.mLibDirs != null) {
for (final String libDir : key.mLibDirs) {
if (libDir.endsWith(".apk")) {
try {
builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}
return builder.build();
}
这里区分了好几种路径资源的加载,其实我们只关心build
方法的处理
public Builder addApkAssets(ApkAssets apkAssets) {
mUserApkAssets.add(apkAssets);
return this;
}
public AssetManager build() {
// Retrieving the system ApkAssets forces their creation as well.
final ApkAssets[] systemApkAssets = getSystem().getApkAssets();
final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];
System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);
final int userApkAssetCount = mUserApkAssets.size();
for (int i = 0; i < userApkAssetCount; i++) {
apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
}
// Calling this constructor prevents creation of system ApkAssets, which we took care
// of in this Builder.
final AssetManager assetManager = new AssetManager(false /*sentinel*/);
assetManager.mApkAssets = apkAssets;
AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
false /*invalidateCaches*/);
return assetManager;
}
可以看出,会把内置的资源库和系统自带的资源库目录资源进行合并,得到最终的AssetManager
并返回,那么我们的推论就是满足的了,
那么我们只要能反射这个Builder
去构建一个新的AssetManager
,那么就能得到我们的Resources
了
其他有更简单的方法
public int addAssetPath(String path) {
return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
}
private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
Preconditions.checkNotNull(path, "path");
synchronized (this) {
ensureOpenLocked();
final int count = mApkAssets.length;
for (int i = 0; i < count; i++) {
if (mApkAssets[i].getAssetPath().equals(path)) {
return i + 1;
}
}
final ApkAssets assets;
try {
if (overlay) {
final String idmapPath = "/data/resource-cache/"
+ path.substring(1).replace('/', '@')
+ "@idmap";
assets = ApkAssets.loadOverlayFromPath(idmapPath, false /*system*/);
} else {
assets = ApkAssets.loadFromPath(path, false /*system*/, appAsLib);
}
} catch (IOException e) {
return 0;
}
mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
mApkAssets[count] = assets;
nativeSetApkAssets(mObject, mApkAssets, true);
invalidateCachesLocked(-1);
return count + 1;
}
}
AssetManager
本身提供了addAssetPath
方法,通过这个方法可以给当前的AssetManager
新增新的资源目录,代码和上面是一样的,但因为AssetManager
本身并不提供公开的实例化方法,这个需要我们自主去反射
其中Resources
的一个创建构造方法是这样
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
那么一个简单的创建插件资源的方式就出来了
fun initTargetResource():Resources {
try {
val constru = AssetManager::class.java.getDeclaredConstructor()
val assetManger = constru.newInstance()
val method =
AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
method.isAccessible = true
method.invoke(assetManger, skinPath)
skinResouce =
Resources(assetManger, appResource!!.displayMetrics, appResource!!.configuration)
return skinResouce
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return null
}
那么资源问题已经解决了,后续的操作就涉及到View
的加载处理问题了,这个将在下一篇中进行讲解