爲什麼加載不了資源
我們知道獲取資源時實際都是使用了Resource。
mResource這個變量在Activity父類ContextThemeWrapper中。
ContextThemeWrapper的mResource的獲得是通過Context的實現類得到的。
Context實現類ContextImpl中獲得Resources 是通過
Resources resources = packageInfo.getResources(mainThread);
packageInfo 是一個LoadedApk對象。
這個對象相當於是一個apk在內存中的表示, Apk文件的相關信息、資源都可以通過此對象獲取。
這個LoadedApk是怎麼創建的呢。
在ActivityThread的getPackageInfo方法中:
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode,boolean registerPackage) {
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
//在這裏創建
packageInfo = new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode
&& aInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
······
}
return packageInfo;
}
}
可以看到構造參數裏面有個參數是ApplicationInfo 。
所以說不管前面是hook IActivityManager合併dex還是hook Instrumentation 傳入我們的ClassLoader,這個LoadedApk裏傳入的ApplicationInfo其實還是我們宿主的,並不是插件apk中的。
所以我們在插件apk中直接使用資源,等到插件apk被宿主調用器後,使用的是宿主的資源庫,而宿主的資源中並沒有我們插件apk中的資源,所以一運行的時候就會報錯。
那麼怎麼解決呢。
我們創建一個Resource給我們的插件Activity就可以了。
如何加載資源
我們知道一般得到資源調用的是getResource方法,最終調用了Context.getResources()方法。
我們看看Context實現類ContextImpl對應的方法。
@Override
public Resources getResources() {
return mResources;
}
再看看mResource是如何賦值的。
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
...
mPackageInfo = packageInfo;
//這裏拿到了一個ResourcesManager,單例的,說明我們應用當中使用的都是同一套資源
mResourcesManager = ResourcesManager.getInstance();
...
//LoadedApk對象中得到Resources對象
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (activityToken != null
|| displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo, activityToken);
}
}
//給mResources賦值
mResources = resources;
...
}
ResourcesManager是一個單例,通過它獲得mResources,保證了我們每個Context獲取的都是同樣的資源。
resources通過getTopLevelResources方法賦值,我們具體看看是如何創建的
public Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
······
//創建一個AssetManager對象
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 (resDir != null) {'
//添加資源路徑
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
if (splitResDirs != null) {
for (String splitResDir : splitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
return null;
}
}
}
if (overlayDirs != null) {
for (String idmapPath : overlayDirs) {
assets.addOverlayPath(idmapPath);
}
}
if (libDirs != null) {
for (String libDir : libDirs) {
if (assets.addAssetPath(libDir) == 0) {
Slog.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
//Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
DisplayMetrics dm = getDisplayMetricsLocked(displayId);
Configuration config;
boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
final boolean hasOverrideConfig = key.hasOverrideConfiguration();
if (!isDefaultDisplay || hasOverrideConfig) {
config = new Configuration(getConfiguration());
if (!isDefaultDisplay) {
applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
}
if (hasOverrideConfig) {
config.updateFrom(key.mOverrideConfiguration);
}
} else {
config = getConfiguration();
}'
//創建一個Resource對象
r = new Resources(assets, dm, config, compatInfo, token);
if (false) {
Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
+ r.getConfiguration() + " appScale="
+ r.getCompatibilityInfo().applicationScale);
}
synchronized (this) {
WeakReference<Resources> wr = mActiveResources.get(key);
Resources existing = wr != null ? wr.get() : null;
if (existing != null && existing.getAssets().isUpToDate()) {
// Someone else already created the resources while we were
// unlocked; go ahead and use theirs.
r.getAssets().close();
return existing;
}
// XXX need to remove entries when weak references go away
mActiveResources.put(key, new WeakReference<Resources>(r));
return r;
}
}
從上述源碼可以得到如下創建Resource的步驟
- 創建一個AssetManager對象
- 通過addAssetPath方法將資源路徑添加給AssetManager,需要注意的是,這個方法是hide的,需要反射調用。
- 通過AssetManager對象創建一個Resource對象,構造方法中,需要一個AssetManager,其他的可以使用宿主的。
這樣就好辦了。
我們在activity剛創建好時,將我們的Resource通過反射添加到ContextThemeWrapper中。
這個時機該是什麼時候呢?
那就是Instrumentation的newActivity方法中。
看一下代碼實現:
public PluginApp loadPluginApk(String apkPath) {
String addAssetPathMethod = "addAssetPath";
PluginApp pluginApp = null;
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod(addAssetPathMethod,
String.class);
addAssetPath.invoke(assetManager, apkPath);
Resources pluginRes = new Resources(assetManager,
mContext.getResources().getDisplayMetrics(),
mContext.getResources().getConfiguration());
pluginApp = new PluginApp(pluginRes);
pluginApp.mClassLoader = createDexClassLoader(apkPath);
} catch (IllegalAccessException
| InstantiationException
| NoSuchMethodException
| InvocationTargetException e) {
e.printStackTrace();
}
return pluginApp;
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if (mPluginManager.hookToPluginActivity(intent)) {
String targetClassName = intent.getComponent().getClassName();
PluginApp pluginApp = mPluginManager.getLoadedPluginApk();
Activity activity = mBase.newActivity(pluginApp.mClassLoader, targetClassName, intent);
ReflectUtil.setField(ContextThemeWrapper.class, activity, Constants.FIELD_RESOURCES, pluginApp.mResources);
return activity;
}
return super.newActivity(cl, className, intent);
}
先創建好我們的Resource,然後在newActivity方法內通過反射賦值。