本文所引用的源码为Android 6.0版本
Resources创建过程
getResources()调用过程
在Activity中我们经常使用getResources()
来获取Resources。拿到这个对象之后,我可以通过它获取apk中的各种资源。
先看看getResources()的调用过程:
Activity的内部并没有这个方法,它是Activity的父类ContextThemeWrapper的方法。
ContextThemeWrapper位置:android-6.0.0_r1/frameworks/base/core/java/android/view/ContextThemeWrapper.java
getResources()方法代码:
public Resources getResources() {
if (mResources != null) {
return mResources;
}
if (mOverrideConfiguration == null) {
mResources = super.getResources();
return mResources;
} else {
...//这种情况暂不考虑
}
}
如果存在Resources对象,则直接返回。如果不存在,先调用了父类的方法获取,然后返回。
super.getResources()
调用的是父类CotextWrapper的getResources()方法。
CotextWrapper位置:android-6.0.0_r1/frameworks/base/core/java/android/content/ContextWrapper.java
Context mBase;
public Resources getResources()
{
return mBase.getResources();
}
返回的mBase的Resource对象,mBase是ContextImpl对象,通过attachBaseContext(Context base)方法设置的。ContextImpl直接继承了ContextWrapper。
ContextImpl的getResources()方法:
public Resources getResources() {
return mResources;
}
而ContextImpl对象是在ActivityThread执行创建handleLaunchActivity(ActivityClientRecord r, Intent customIntent)
方法创建获取启动Activity时创建的。
Context创建
ActivityThread位置:android-6.0.0_r1/frameworks/base/core/java/android/app/ActivityThread.java
创建和设置ContextImpl对象的相关代码:
private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
...
//创建了ContextImpl对象
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, displayId, r.overrideConfig);
appContext.setOuterContext(activity);
Context baseContext = appContext;
...
return baseContext;
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
try {
...
if (activity != null) {
//创建mBase
Context appContext = createBaseContextForActivity(r, activity);
...
//将值设置给创建的Activity
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
...//执行Activity的onCreate(),onStart()等方法
}
...
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
...
}
return activity;
}
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
//创建和启动Activity
Activity a = performLaunchActivity(r, customIntent);
...
}
ActivityThread内创建了ContextImpl对象,通过Activity的attach()方法将值传递给Activity。
Activity位置:android-6.0.0_r1/frameworks/base/core/java/android/app/Activity.java
继续看Activity的attach()方法
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
//通过父类ContextThemeWrapper的方法将值传递给CotextWrapper
attachBaseContext(context);
...
}
执行attachBaseContext(context);
这行代码后,便能在Activity中操作Context的相关方法。于是能调用getResources()获取资源。
看了这么多代码,但是Resources是如何被创建的并没有介绍到。其实它是在ContextImpl.createActivityContext()方法中创建的。
Resources创建
ContextImpl位置:android-6.0.0_r1/frameworks/base/core/java/android/app/ContextImpl.java
ContextImpl.createActivityContext()方法中调用了私有的构造方法创建对象。
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
return new ContextImpl(null, mainThread, packageInfo, null, null, false,
null, overrideConfiguration, displayId);
}
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
...
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (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);
}
}
mResources = resources;
...
}
LoadedApk用来记录加载的apk的信息。packageInfo.getResources(mainThread)
这句代码最终还是会调用ResourcesManager的getTopLevelResources()方法来获取Resources对象,与第二个if代码块中调用的为同一个方法。
packageInfo.getResources(mainThread)
调用的方法:
LoadedApk位置:android-6.0.0_r1/frameworks/base/core/java/android/app/LoadedApk.java
//LoadedApk的getResources()方法
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
//ActivityThread的getTopLevelResources()方法
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
String[] libDirs, int displayId, Configuration overrideConfiguration,
LoadedApk pkgInfo) {
return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs,
displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo());
}
如果mResources不存在,则调用ActivityThread的getTopLevelResources()获取,并赋值给mResources。然后返回该值。ActivityThread的getTopLevelResources()方法内又直接调用了ResourcesManager的getTopLevelResources()方法。
继续查看ResourcesManager的getTopLevelResources()方法。
ResourcesManager位置:android-6.0.0_r1/frameworks/base/core/java/android/app/ResourcesManager.java
Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
...
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
Resources r;
synchronized (this) {
...
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
...
//如果缓存中有,且没有过期,则返回查找到的Resources对象
if (r != null && r.getAssets().isUpToDate()) {
//
return r;
}
}
//创建Resosurces对象
//首先,创建AssetManager对象,并将加resDir,splitResDirs等目录中的资源
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) {
//加载该目录的资源到AssetManager对象,非0为加载成功
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
...//使用addXxxPath(resDir)方法加载splitResDirs,overlayDirs和libDirs目录资源
...//设置Configuration等参数
//创建Resources对象
r = new Resources(assets, dm, config, compatInfo);
synchronized (this) {
//再次从缓存中查找
WeakReference<Resources> wr = mActiveResources.get(key);
Resources existing = wr != null ? wr.get() : null;
//存在且没有过期,则返回查找到的Resources对象
if (existing != null && existing.getAssets().isUpToDate()) {
// Someone else already created the resources while we were
// unlocked; go ahead and use theirs.
//释放AssetManager
r.getAssets().close();
return existing;
}
// XXX need to remove entries when weak references go away
//存入缓存集合
mActiveResources.put(key, new WeakReference<>(r));
...
return r;
}
}
先创建ResourcesKey对象,然后从缓存mActiveResources查找。如果有符合的,则直接返回。如果无,则先创建AssetManager对象,并使用该对象加载相关目录的资源,然后在创建Resources对象。并不会直接将刚创建的Resources对象缓存和返回,而是再次从缓存中查找。如果有符合的,则释放创建的。没有,则执行缓存和返回创建的Resources对象。
在ContextImpl的构造方法内,执行Resources resources = packageInfo.getResources(mainThread);
这行代码便能获取到资源对象。一般情况下,if (resources != null)代码块中的if语句不会进入。至此,Resources的创建过程就分析完了。
Resources获取资源
getString()获取字符串过程
一,Resources调用过程
调用Resources的getString()方法。
public String getString(@StringRes int id) throws NotFoundException {
final CharSequence res = getText(id);
if (res != null) {
return res.toString();
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mAssets.getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
getString()方法内部调用了getText()方法,而getText()方法内部又是通过mAssets的getResourceText()方法来获取字符序列。mAssets便是之前getTopLevelResources()方法中创建的AssetManager对象。
二,AssetManager调用过程
AssetManager位置:android-6.0.0_r1/frameworks/base/core/java/android/content/res/AssetManager.java
AssetManager的getResourceText()方法
/*package*/ final CharSequence getResourceText(int ident) {
synchronized (this) {
TypedValue tmpValue = mValue;
int block = loadResourceValue(ident, (short) 0, tmpValue, true);
if (block >= 0) {
if (tmpValue.type == TypedValue.TYPE_STRING) {
return mStringBlocks[block].get(tmpValue.data);
}
//非字符串情况,暂不分析
return tmpValue.coerceToString();
}
}
return null;
}
首先使用native方法loadResourceValue()获取基本信息,block为id为ident的字符串在mStringBlocks数组中的位置,tmpValue.data记录其在StringBlock中的位置。
StringBlock中有两个属性来存储字符序列:mStrings,为字符序列CharSequence[]类型;mSparseStrings,为SparseArray<CharSequence>类型。在StringBlock的get()中会先从mStrings,如果不存在,则从mSparseStrings中查找。如果还未查找到,则调用native方法nativeGetString(long obj, int idx)去获取。
在AssetManager的makeStringBlocks()方法中对mStringBlocks进行初始化。
/*package*/ final void makeStringBlocks(StringBlock[] seed) {
final int seedNum = (seed != null) ? seed.length : 0;
final int num = getStringBlockCount();
mStringBlocks = new StringBlock[num];
if (localLOGV) Log.v(TAG, "Making string blocks for " + this
+ ": " + num);
for (int i=0; i<num; i++) {
if (i < seedNum) {
mStringBlocks[i] = seed[i];
} else {
mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
}
}
}
getStringBlockCount()是native方法,用于获取资源包中字符串块的数量,然后创建该数量的StringBlock数组。
这个方法在addAssetPath(String path),addOverlayPath(String idmapPath)和addAssetPaths(String[] paths)方法中被调用,而这个三个方法又在Resources被创建时被调用。ResourcesManager的getTopLevelResources()方法中AssetManager对象加载resDir, splitResDirs, overlayDirs和libDirs目录资源时调用到这3个addXxxPath()方法。
getDrawable()获取图片过程
只分析获取png图片,且不分析Resources中图片缓存情况。
调用Resources的getDrawable()
public Drawable getDrawable(@DrawableRes int id) throws NotFoundException {
final Drawable d = getDrawable(id, null);
...
return d;
}
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
...
final Drawable res = loadDrawable(value, id, theme);
...
return res;
}
Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
...
Drawable dr;
if (cs != null) {
...
} else if (isColorDrawable) {
...
} else {
dr = loadDrawableForCookie(value, id, null);
}
...
return dr;
}
getDrawable()最终会调用到loadDrawable()方法,这里我没有分析从缓存获取和缓存情况,只分析从AssetManager中获取资源流。loadDrawable()中又调用了loadDrawableForCookie()方法。
继续查看loadDrawableForCookie()方法:
private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) {
...
try {
if (file.endsWith(".xml")) {
...
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
dr = Drawable.createFromResourceStream(this, value, is, file, null);
is.close();
}
} catch (Exception e) {
...
}
...
return dr;
}
调用了mAssets.openNonAsset()获取资源流,然后将流转换为Bitmap,再生成Drawable对象。
小结
getString()和getDrawable()获取的资源并不由Resources提供,Resources只是对AssetManager做了一层封装。真正获取资源的操作是由AssetManager执行的。通过分析Resources的创建过程,我们知道可以通过addAssetPath(String path),addOverlayPath(String idmapPath)和addAssetPaths(String[] paths)这三个方法向AssetManager添加资源包,AssetManager会根据路径去加载和解析资源包,也就是说我们可以通过这3个方法动态的加载资源。
AssetManager动态加载apk
查找入口
先看看AssetManager中最简单的addAssetPath()方法:
/**
* 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;
}
}
Not for use by applications.好像不能用这个方法。再Activity中使用getAssets().addAssetPath(“”),编辑器会提示错误,没有这个方法。其实是这个方法被隐藏(hide)了。不能直接使用,那我们就间接使用,使用反射方式来间接使用这个方法。
创建工程测试
第一步:创建一个简单的Android工程,命名为ResDemo,包名为com.plugin.test。保留strings.xml文件,将Activity相关的资源和java文件全部删除,越简单越好。AndroidManifest.xml文件只需要保留application信息。然后生成apk。
第二步:创建另一个的Android工程,命名为HostDemo,包名为com.plugin。
在MainActivity添加如下代码:
private void testDynamicLoadResource(){
try{
AssetManager assetManager = getAssets();
Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
if(!method.isAccessible()){
method.setAccessible(true);
}
//TODO 需要改为resApk实际存放的路径
String resPath = "/mnt/sdcard/res.apk";
int cook = (int) method.invoke(assetManager, resPath);
Log.i("Test", "cook = " + cook);
int strId = getResources().getIdentifier("app_name", "string", "com.test.res");
Log.i("Test", "app_name = " + getString(strId) );
}catch (Exception e){
e.printStackTrace();
}
}
**path一定要改为resApk实际存放的路径。**getResources().getIdentifier()方法中的三个测试:第一个为资源名称;第二个为资源类型,如:string,drawable等deng;第三个为资源apk的包名。
然后再onCreate()方法中调用testDynamicLoadResource()方法。
运行HostApk工程,查看结果。日志显示’cook = 0’,后面直接抛出异常。
异常信息:
Caused by: android.content.res.Resources$NotFoundException: String resource ID #0x0
at android.content.res.Resources.getText(Resources.java:343)
at android.content.res.CoollifeUIResources.getText(CoollifeUIResources.java:110)
at android.content.res.Resources.getString(Resources.java:441)
at android.content.Context.getString(Context.java:382)
出现这种情况是因为resPath与resApk实际存放的路径不一致,导致resApk未被加载到AssetManager。因而getResources().getIdentifier(“app_name”, “string”, “com.test.res”)获取不到资源id。
如果cook信息不为0,也出现这种异常,则可能为getIdentifier()方法中的参数错误。需要查看名称,类型和包名是否与ResDemo工程中的一种。
修改错误,再次运行。日志显示’cook = 4’,但是app_name显示的信息确不是ResApk。这是因为ResDemo中R.java文件的资源id与HostDemo的资源id相同,导致去获取HostDemo的资源。
这个错误是由资源id相同导致,只能修改aapt模块,添加自定义的资源id头。因为应用默认的资源id头为0x7f,如果我们将ResDemo的资源id头改为比0x7f大,则就可以正确的获取到资源。
修改aapt模块代码请阅读修改aapt和自定义资源ID,使用命令构建apk请阅读手工构建Android应用。
使用修改过的aapt文件生成R.java文件和资源包,然后使用命令编译java文件和生成dex文件,使用ApkBuidler将资源包和dex文件合成Apk,对Apk文件进行签名。
然后替换之前的apk,运行HostDemo,在打印的日志里app_name = ResDemo。
在成功加载ResDemo.apk后,首先通过getResources().getIdentifier()获取资源id,然后根据id加载资源,HostDemo.apk可以获取ResDemo的任何资源。
在ResDemo工程添加一个继承Activity的MainActivity,添加一个布局文件,在drawable和mipmap目录个添加一张图片,再在strings文件添加一些字符串。手工构建工程,生产新的resDemo.apk。
然后,在HostDemo工程的MainActivity中获取ResDemo添加和字符串和drawable中的图片,再获取ResDemo中的布局文件并在MainActivity中显示。运行工程。
首先看看ResDemo运行截图:
再看HostDemo加载resDemo.apk后,动态获取它的资源的显示效果截图:
(请忽略输入框和按钮上的文字,它们与本文所介绍的内容无关)
图中两红色框内的资源都是从resDemo.apk中获取。动态加载的RelativeLayout与resDemo.apk显示除高度不同,其它都一样。
补充
这种方式只在Android 5.0及以上版本有效。Android 4.4及以下版本即便使用addAssetPath()方法添加,也无法获取获取到资源id,或根据资源id获取资源。
原因貌似是并没有将该路径的apk的资源包解析并添加到native的AssetManager对象中。
Android 4.4及以下版本可以通过创建新的Resources对象,在通过反射方式将新创建的Resources对象设置给Activity的Context的mResources属性。这样就可以获取资源包中的资源,可以通过get Application().getResources()获取Host中的资源,但是不能加载Host中的layout文件,在解析布局xml时会出现获取资源失败异常。
总结
动态加载资源的步骤:
一,主工程需要通过反射调用AssetManager动态加载子apk中的资源,先获取子apk中的资源id,然后通过id加载该资源。
二,子工程生成的资源id必须与主工程的不同,即在生产R.java文件和资源包时需要指定资源id的头。这一步骤需要自己修改android源码中的aapt和androidfw模块代码,并重新编译生成可执行的aapt文件,使用该文件构建子apk。