上一篇我们已经完成了一个真正可运行的插件化demo,而且demo中也解决了插件中不可以使用资源的问题,但是由于篇幅的问题我们并没有对原理讲解,所以这一篇是对上一篇的一个收尾,如果没有看过上一篇建议先看Android插件化完美实现代码资源加载及原理讲解 附可运行demo.
demo地址 : https://github.com/ljqloveyou123/LiujiaqiAndroid
我们的宿主应用调用一个未安装的插件apk,正常的情况下是不能访问插件中的资源的,例如R.,因为我们宿主中根本就不存在这个资源id,所以就会崩溃。还有另一种情况,基于我们上一篇的demo中,我们使用了占坑的方式加载了插件中apk中的Activity,上一篇我们也分析了创建Activity的时候需要Classloader,这个Classloader是通过 r.packageInfo.getClassloader()来获取的,而 r.packageInfo是一个LoadedApk类型的对象,这个对象是一个apk在内存中的标示, Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等四大组件的信息我们都可以通过此对象获取。我们再看一下这个LoadedApk对象怎么创建
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;
}
}
我们看一下构造参数的第二个参数,aInfo,这是一个ApplicationInfo类型的对象,我们上一篇并没有构造我们自己的LoadedApk对象,而只是将我们的dex和宿主的合并了而已,那么也就是说了,这个LoadedApk里传入的ApplicationInfo其实是我们宿主的,并不是插件apk中的。那么也就是说如果我们在插件apk中直接使用资源,等到插件apk被宿主调用器后,使用的是宿主的资源库,而宿主的资源中并没有我们插件apk中的资源,所有一运行的时候就会报错。那么要解决这个问题我们就得想办法,让插件apk运行的时候使用自己的资源才行,下面我们分析。
我们在代码中使用资源的时候都是通过R.,或者是Context.getResources(),这两种方式,其实R.也是通过Context.getResources()查找对应id的。那么我们直接分析Context.getResources()就好了,那么Context的实现类是ContextImpl,我们去看看。
platform_frameworks_base-master\core\java\android\app\ContextImpl.java
@Override
public Resources getResources() {
return mResources;
}
我们再看一下这个mResources怎么创建的,5.1源码,在这里说一下
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);
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (activityToken != null
|| displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
//给resource赋值
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo, activityToken);
}
}
//给mResources赋值
mResources = resources;
...
}
ResourcesManager.getInstance()是单例的这样保证了我们每个Context获取的都是同样的资源,resources通过getTopLevelResources方法赋值,我们看看getTopLevelResources方法干了什么
public Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
final float scale = compatInfo.applicationScale;
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
Resources r;
synchronized (this) {
// Resources is app scale dependent.
if (false) {
Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
}
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
//if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
if (r != null && r.getAssets().isUpToDate()) {
if (false) {
Slog.w(TAG, "Returning cached resources " + r + " " + resDir
+ ": appScale=" + r.getCompatibilityInfo().applicationScale);
}
return r;
}
}
//if (r != null) {
// Slog.w(TAG, "Throwing away out-of-date resources!!!! "
// + r + " " + resDir);
//}
//创建一个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;
}
}
方法有点长,但其实主要就3步,这里说明一下,每个版本Resource的赋值过程可能不太一样,但是最终都是通过一下三步来创建的Resource
1.创建AssetManager对象
2.通过addAssetPath方法将资源路径添加给AssetManager,这个方法是hint的,我们要通过反射调用
/**
* 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) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
3.通过AssetManager对象创建一个Resource对象,我们选择一个参数比较少的,参数说明第一个是AssetManager,后后面两个是和设备相关的配置参数,我们可以直接使用宿主的
/**
* Create a new Resources object on top of an existing set of assets in an
* AssetManager.
*
* @param assets Previously created AssetManager.
* @param metrics Current display metrics to consider when
* selecting/computing resource values.
* @param config Desired device configuration to consider when
* selecting/computing resource values (optional).
*/
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
}
那么我们通过上面的分析后,知道了Resource怎么创建的,那么我们就可以给插件apk创建一个属于自己的Resource对象,这样他就可以自由的使用资源了。但是还要留意一点就是在我们的插件apk默认拿的是宿主的Resource对象,如果想让插件apk可以自由的使用资源,那么我们就必须要在宿主中提供一个返回插件apk自己资源的方法,然后在插件apk中我们要重写Context的getResource方法,这样才算真正的完成
1.在宿主程序中,一定要在调用插件apk前执行下面代码,demo中在Application中执行
//创建我们自己的Resource
String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/chajian_demo.apk";
//创建AssetManager
assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, apkPath);
Method ensureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
ensureStringBlocks.setAccessible(true);
ensureStringBlocks.invoke(assetManager);
Resources supResource = getResources();
Log.e("Main", "supResource = " + supResource);
newResource = new Resources(assetManager, supResource.getDisplayMetrics(), supResource.getConfiguration());
mTheme = newResource.newTheme();
mTheme.setTo(super.getTheme());
2.在宿主中提供给插件apk返回自己Resource对象的方法,demo中为了简单直观,直接在Application中重写了getResources和getAssets方法,这个可以根据自己的需求来定,只是提供一种思路
@Override
public AssetManager getAssets() {
return assetManager == null ? super.getAssets() : assetManager;
}
@Override
public Resources getResources() {
return newResource == null ? super.getResources() : newResource;
}
3.在插件apk中使用资源的Activity中一定要重新getResources和getAssets方法,因为我们的插件apk默认使用的是宿主的Resource,而宿主中并没有我们插件中的资源id,一定要拿自己的Resource才可以使用。这里说的使用资源包括R.和Context.getResource()。
@Override
public AssetManager getAssets() {
if(getApplication() != null && getApplication().getAssets() != null){
return getApplication().getAssets();
}
return super.getAssets();
}
@Override
public Resources.Theme getTheme() {
if(getApplication() != null && getApplication().getTheme() != null){
return getApplication().getTheme();
}
return super.getTheme();
}
ok了,到这里可以愉快的在插件apk中使用资源了,以上demo代码为了简单明了,所有很多地方需要自己根据需求改进,这里只给出了实现的思路和方法。到这里插件化所有的东西全部说完了,研究这些东西也花了不少的时间,拿出来和大家分享,如果觉得不错的话,还请给点个赞,项目上给个star,
demo地址 : https://github.com/ljqloveyou123/LiujiaqiAndroid