1、前言
android开发者都知道,对于android中的res下的资源大都会生成相应的id,保存在R.java文件中,之后既可以通过R.type.resource来访问相应的资源了,这样大大方便了开发者。对于这些资源文件同代码一样,最终都会被打包值apk中(通过aapt:Android Asset Package Tool),而且大部分文本格式(xml)的文件还会被编译成二进制格式的资源文件。
众所周知,android机型实在太多,不同大小和密度的屏幕、支持多种语言、屏幕的方向转换等等,android的提供的资源组织方式有很多种,应用程序可以根据这些配置信息,找到最匹配的资源,来进行ui展示。针对这么多的资源组织方式(具体可参见https://developer.android.com/guide/topics/resources/providing-resources.html#BestMatch),android有自己的资源管理框架,本文主要是探索android资源管理相关的知识(本文中的源代码都是基于android-api-24来进行的,如果本地无法下载存储大量源代码,可以通过源码在线网站进行查阅http://androidxref.com)。
开发者能看到的android资源框架中几个重要的文件有R.java以及Resource.arsc(解压apk可以看到该文件)。其中:R.java保存着不同资源对应的ID值(除了assets中的资源),开发者可通过该文件中的id进行资源访问。该文件是通过aapt工具产生的;APK中的Resources.arsc文件是用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。该文件同样是由AAPT工具在打包过程中生成的,里面维护者资源ID、Name、Path或者Value的对应关系,AssetManager基于这个索引表,就可以通过资源的ID找到这个资源对应的文件或者数据。Resources的创建是基于ResourceTable对象来进行描述的,并在appt打包的时候依据此生成。
通过这Resources和R.java两个文件,即可快速获取相关资源。Resources resources = packageInfo.getResources(mainThread);//ContextImpl中的Resources初始化
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
}
return mResources;
}
/**
* Creates the top level resources for the given package. Will return an existing
* Resources if one has already been created.
*/
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
String[] libDirs, int displayId, LoadedApk pkgInfo) {
return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
}
public @NonNull 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) {
try {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
final ResourcesKey key = new ResourcesKey(
resDir,
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
return getOrCreateResources(activityToken, key, classLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
5、接着,在该方法中,又调用了getOrCreateResources方法,具体实现如下
private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
if (DEBUG) {
Throwable here = new Throwable();
here.fillInStackTrace();
Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
}
if (activityToken != null) {
final ActivityResources activityResources =
getOrCreateActivityResourcesStructLocked(activityToken);
// Clean up any dead references so they don't pile up.
ArrayUtils.unstableRemoveIf(activityResources.activityResources,
sEmptyReferencePredicate);
// Rebase the key's override config on top of the Activity's base override.
if (key.hasOverrideConfiguration()
&& !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
final Configuration temp = new Configuration(activityResources.overrideConfig);
temp.updateFrom(key.mOverrideConfiguration);
key.mOverrideConfiguration.setTo(temp);
}
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl);
}
// We will create the ResourcesImpl object outside of holding this lock.
} else {
// Clean up any dead references so they don't pile up.
ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
// Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesLocked(classLoader, resourcesImpl);
}
// We will create the ResourcesImpl object outside of holding this lock.
}
}
// If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
ResourcesImpl resourcesImpl = createResourcesImpl(key);
synchronized (this) {
ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
if (existingResourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- got beat! existing impl=" + existingResourcesImpl
+ " new impl=" + resourcesImpl);
}
resourcesImpl.getAssets().close();
resourcesImpl = existingResourcesImpl;
} else {
// Add this ResourcesImpl to the cache.
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
}
final Resources resources;
if (activityToken != null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
}
return resources;
}
}
该方法需要三个入参,第一个代表与该资源相关联的activity,ResourcesKey前面已经提到,最后一个是classLoader,默认是SystemClassLoader。在该方法中主要关注的是ResourcesImpl类型对象的创建,该对象是Resources的核心,Resources实际上是该类的wrapper。因此ResourcesImpl其实是Resources的实际实现接口。接下来的讨论会以该类为参考点。
该方法大致可分为两个过程:一是ResourcesImpl查找过程(findResourcesImplForKeyLocked);二是ResourcesImpl的创建过程(createResourcesImpl)。第一个过程是确定ResourcesImpl对象是否已经生成,如果生成则以此为核心生成Resources对象,并返回。第二个过程则会根据ResourcesKey进行对象的创建。
首先,看findResourcesImplForKeyLocked方法:
private ResourcesImpl findResourcesImplForKeyLocked(@NonNull ResourcesKey key) {
WeakReference weakImplRef = mResourceImpls.get(key);
ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
if (impl != null && impl.getAssets().isUpToDate()) {
return impl;
}
return null;
}
/**
* A mapping of ResourceImpls and their configurations. These are heavy weight objects
* which should be reused as much as possible.
*/
private final ArrayMap> mResourceImpls =
new ArrayMap<>();
mResourceImpls指向了类型为ArrayMap的对象,从该对象的定义可以看出,该对象维持着ResourcesKey与ResourcesImpl的一个映射关系。而ResourcesKey又是基于当前应用程序进程中加载的APK文件路径产生的(在Resources的getResources方法中,传入了参数resDir),也就是说,给定一个Apk文件路径,ActivityThread类的成员函数getTopLevelResources可以在成员变量mResourceImpls中检查是否存在一个对应的ResourcesImpl对象。如果存在,并且这个ResourcesImpl对象里面包含的资源文件没有过时(即调用这个ResourcesImpl对象的成员函数getAssets所获得一个AssetManager对象的成员函数isUpToDate的返回值等于true),那么就会将ResourcesImpl对象返回。然后根据该对象来判断调用方需要的Resources对象是否已经存在。根据ResourcesImpl的状态可分为以下两种:
(1)ResourcesImpl不为null;/**
* Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist
* or the class loader is different.
*/
private @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken,
@NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl) {
final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
activityToken);
final int refCount = activityResources.activityResources.size();
for (int i = 0; i < refCount; i++) {
WeakReference weakResourceRef = activityResources.activityResources.get(i);
Resources resources = weakResourceRef.get();
if (resources != null
&& Objects.equals(resources.getClassLoader(), classLoader)
&& resources.getImpl() == impl) {
if (DEBUG) {
Slog.d(TAG, "- using existing ref=" + resources);
}
return resources;
}
}
Resources resources = new Resources(classLoader);
resources.setImpl(impl);
activityResources.activityResources.add(new WeakReference<>(resources));
if (DEBUG) {
Slog.d(TAG, "- creating new ref=" + resources);
Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
}
return resources;
}
private @NonNull ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
final AssetManager assets = createAssetManager(key);
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
if (DEBUG) {
Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
}
return impl;
}
@VisibleForTesting
protected @NonNull AssetManager createAssetManager(@NonNull final ResourcesKey key) {
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 (key.mResDir != null) {
if (assets.addAssetPath(key.mResDir) == 0) {
throw new Resources.NotFoundException("failed to add asset path " + key.mResDir);
}
}
if (key.mSplitResDirs != null) {
for (final String splitResDir : key.mSplitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
throw new Resources.NotFoundException(
"failed to add split asset path " + splitResDir);
}
}
}
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
assets.addOverlayPath(idmapPath);
}
}
if (key.mLibDirs != null) {
for (final String libDir : key.mLibDirs) {
if (libDir.endsWith(".apk")) {
// Avoid opening files we know do not have resources,
// like code-only .jar files.
if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}
return assets;
}
/**
* Create a new AssetManager containing only the basic system assets.
* Applications will not generally use this method, instead retrieving the
* appropriate asset manager with {@link Resources#getAssets}. Not for
* use by applications.
* {@hide}
*/
public AssetManager() {
synchronized (this) {
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
init(false);
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
ensureSystemAssets();
}
}
private static void ensureSystemAssets() {
synchronized (sSync) {
if (sSystem == null) {
AssetManager system = new AssetManager(true);
system.makeStringBlocks(null);
sSystem = system;
}
}
}
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
@Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
mAssets = assets;
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
mAssets.ensureStringBlocks();
}
该处调用了updateConfiguration方法,config对应的是设备配置信息,updateConfiguration首先是根据参数config和metrics来更新设备的当前配置信息,例如,屏幕大小和密码、国家地区和语言、键盘配置情况等等。ensureStringBlocks()代码如下:
final StringBlock[] ensureStringBlocks() {
synchronized (this) {
if (mStringBlocks == null) {
makeStringBlocks(sSystem.mStringBlocks);
}
return 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
AssetManager类的成员变量mStringBlocks指向的是一个StringBlock数组,其中,每一个StringBlock对象都是用来描述一个字符串资源池。每一个资源表都包含有一个资源项值字符串资源池,AssetManager类的成员变量mStringBlocks就是用来保存所有的资源表中的资源项值字符串资源池的。AssetManager类的成员函数ensureStringBlocks首先检查成员变量mStringBlocks的值是否等于null。如果等于null的话,那么就说明当前应用程序使用的资源表中的资源项值字符串资源池还没有读取出来,这时候就会调用另外一个成员函数makeStringBlocks来进行读取。
至此Resources对象的初始化完成。依据该过程,可知,只要给定相应apk路径,开发者完全可以动态加载apk资源。但是由源码可知,这些机制很多都是native实现,因此需要采取反射手段来进行获取,然后进一步实现。
三、获取资源部分,以Resources.getDrawable为例。@Deprecated
public Drawable getDrawable(@DrawableRes int id) throws NotFoundException {
final Drawable d = getDrawable(id, null);
if (d != null && d.canApplyTheme()) {
Log.w(TAG, "Drawable " + getResourceName(id) + " has unresolved theme "
+ "attributes! Consider using Resources.getDrawable(int, Theme) or "
+ "Context.getDrawable(int).", new RuntimeException());
}
return d;
}
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
return impl.loadDrawable(this, value, id, theme, true);
} finally {
releaseTempTypedValue(value);
}
}
@Nullable
Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
boolean useCache) throws NotFoundException {
try {
if (TRACE_FOR_PRELOAD) {
// Log only framework resources
if ((id >>> 24) == 0x1) {
final String name = getResourceName(id);
if (name != null) {
Log.d("PreloadDrawable", name);
}
}
}
final boolean isColorDrawable;
final DrawableCache caches;
final long key;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
isColorDrawable = true;
caches = mColorDrawableCache;
key = value.data;
} else {
isColorDrawable = false;
caches = mDrawableCache;
key = (((long) value.assetCookie) << 32) | value.data;
}
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
}
final Drawable.ConstantState cs;
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
Drawable dr;
if (cs != null) {
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(wrapper, value, id, null);
}
final boolean canApplyTheme = dr != null && dr.canApplyTheme();
if (canApplyTheme && theme != null) {
dr = dr.mutate();
dr.applyTheme(theme);
dr.clearMutated();
}
if (dr != null && useCache) {
dr.setChangingConfigurations(value.changingConfigurations);
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}
return dr;
} catch (Exception e) {
String name;
try {
name = getResourceName(id);
} catch (NotFoundException e2) {
name = "(missing name)";
}
final NotFoundException nfe = new NotFoundException("Drawable " + name
+ " with resource ID #0x" + Integer.toHexString(id), e);
nfe.setStackTrace(new StackTraceElement[0]);
throw nfe;
}
}
public Drawable getInstance(long key, Resources resources, Resources.Theme theme) {
final Drawable.ConstantState entry = get(key, theme);
if (entry != null) {
return entry.newDrawable(resources, theme);
}
return null;
}
@Override
public Drawable newDrawable() {
return new BitmapDrawable(this, null);
}
/**
* Loads a drawable from XML or resources stream.
*/
private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id,
Resources.Theme theme) {
if (value.string == null) {
throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
+ Integer.toHexString(id) + ") is not a Drawable (color or path): " + value);
}
final String file = value.string.toString();
if (TRACE_FOR_MISS_PRELOAD) {
// Log only framework resources
if ((id >>> 24) == 0x1) {
final String name = getResourceName(id);
if (name != null) {
Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id)
+ ": " + name + " at " + file);
}
}
}
if (DEBUG_LOAD) {
Log.v(TAG, "Loading drawable for cookie " + value.assetCookie + ": " + file);
}
final Drawable dr;
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
try {
if (file.endsWith(".xml")) {
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXml(wrapper, rp, theme);
rp.close();
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
is.close();
}
} catch (Exception 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;
}
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
return dr;
}
private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
Resources.Theme theme, boolean usesTheme, long key, Drawable dr) {
final Drawable.ConstantState cs = dr.getConstantState();
if (cs == null) {
return;
}
if (mPreloading) {
final int changingConfigs = cs.getChangingConfigurations();
if (isColorDrawable) {
if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) {
sPreloadedColorDrawables.put(key, cs);
}
} else {
if (verifyPreloadConfig(
changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) {
if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) {
// If this resource does not vary based on layout direction,
// we can put it in all of the preload maps.
sPreloadedDrawables[0].put(key, cs);
sPreloadedDrawables[1].put(key, cs);
} else {
// Otherwise, only in the layout dir we loaded it for.
sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs);
}
}
}
} else {
synchronized (mAccessLock) {
caches.put(key, theme, cs, usesTheme);
}
}
}
public void put(long key, @Nullable Theme theme, @NonNull T entry, boolean usesTheme) {
if (entry == null) {
return;
}
synchronized (this) {
final LongSparseArray> entries;
if (!usesTheme) {
entries = getUnthemedLocked(true);
} else {
entries = getThemedLocked(theme, true);
}
if (entries != null) {
entries.put(key, new WeakReference<>(entry));
}
}
}
至此,drawable资源获取和缓存分析完毕。