Android底层资源加载过程浅析
相关链接
AssetManager.h AssetManager.cpp
ResourceTypes.h ResourceTypes.cpp
Android5.0资源加载过程
addAssetPath
// core/jni/android_util_AssetManager.cpp
static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz,
jstring path)
{
ScopedUtfChars path8(env, path);
if (path8.c_str() == NULL) {
return 0;
}
AssetManager* am = assetManagerForJavaObject(env, clazz);
if (am == NULL) {
return 0;
}
int32_t cookie;
bool res = am->addAssetPath(String8(path8.c_str()), &cookie);
return (res) ? static_cast<jint>(cookie) : 0;
}
// this guy is exported to other jni routines
AssetManager* assetManagerForJavaObject(JNIEnv* env, jobject obj)
{
jlong amHandle = env->GetLongField(obj, gAssetManagerOffsets.mObject);
AssetManager* am = reinterpret_cast<AssetManager*>(amHandle);
if (am != NULL) {
return am;
}
jniThrowException(env, "java/lang/IllegalStateException", "AssetManager has been finalized!");
return NULL;
}
当我们在 Java 层调用 android.content.res.AssetManager.addAssetPath() 这个方法的时候,其实质是调用 native 层的 android_content_AssetManager_addAssetPath 方法,这个方法会将这个资源 path 添加到 native 层的 AssetManager 对象中。
// libs/androidfw/AssetManager.cpp
bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
{
......
// Skip if we have it already.
for (size_t i=0; i<mAssetPaths.size(); i++) {
if (mAssetPaths[i].path == ap.path) {
if (cookie) {
*cookie = static_cast<int32_t>(i+1);
}
return true;
}
}
ALOGV("In %p Asset %s path: %s", this,
ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string());
// Check that the path has an AndroidManifest.xml
Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
kAndroidManifest, Asset::ACCESS_BUFFER, ap);
if (manifestAsset == NULL) {
// This asset path does not contain any resources.
delete manifestAsset;
return false;
}
delete manifestAsset;
mAssetPaths.add(ap);
// new paths are always added at the end
if (cookie) {
*cookie = static_cast<int32_t>(mAssetPaths.size());
}
#ifdef HAVE_ANDROID_OS
// Load overlays, if any
asset_path oap;
for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) {
mAssetPaths.add(oap);
}
#endif
if (mResources != NULL) {
appendPathToResTable(ap);
}
return true;
}
AssetManager::addAssetPath() 方法先判断该资源是否加载过了,如果加载过了就返回其 cookie;如果还没加载,就将资源 path 添加的 mAssetPaths 列表,并为其分配一个 cookie(即其在列表的 index + 1),然后 如果 mResources 不为空(已经加载过资源),则调用 appendPathToResTable 方法加载(解析)该资源, 最后返回这个资源的cookie。
需要注意的是,Android5.0以下系统,此处不会触发资源加载过程,每个 AssetManager 只会触发一次资源加载的过程。
实际上,只有当真正要使用 resource 文件的时候才会去触发加载:
// libs/androidfw/AssetManager.cpp
const ResTable* AssetManager::getResTable(bool required) const
{
ResTable* rt = mResources;
if (rt) {
return rt;
}
// Iterate through all asset packages, collecting resources from each.
AutoMutex _l(mLock);
if (mResources != NULL) {
return mResources;
}
if (required) {
LOG_FATAL_IF(mAssetPaths.size() == 0, "No assets added to AssetManager");
}
if (mCacheMode != CACHE_OFF && !mCacheValid) {
const_cast<AssetManager*>(this)->loadFileNameCacheLocked();
}
mResources = new ResTable();
updateResourceParamsLocked();
bool onlyEmptyResources = true;
const size_t N = mAssetPaths.size();
for (size_t i=0; i<N; i++) {
bool empty = appendPathToResTable(mAssetPaths.itemAt(i));
onlyEmptyResources = onlyEmptyResources && empty;
}
if (required && onlyEmptyResources) {
ALOGW("Unable to find resources file resources.arsc");
delete mResources;
mResources = NULL;
}
return mResources;
}
appendPathToResTable
// libs/androidfw/AssetManager.cpp
bool AssetManager::appendPathToResTable(const asset_path& ap) const {
Asset* ass = NULL;
ResTable* sharedRes = NULL;
bool shared = true;
bool onlyEmptyResources = true;
MY_TRACE_BEGIN(ap.path.string());
Asset* idmap = openIdmapLocked(ap);
size_t nextEntryIdx = mResources->getTableCount();
ALOGV("Looking for resource asset in '%s'\n", ap.path.string());
if (ap.type != kFileTypeDirectory) {
if (nextEntryIdx == 0) {
// The first item is typically the framework resources,
// which we want to avoid parsing every time.
sharedRes = const_cast<AssetManager*>(this)->
mZipSet.getZipResourceTable(ap.path);
if (sharedRes != NULL) {
// skip ahead the number of system overlay packages preloaded
nextEntryIdx = sharedRes->getTableCount();
}
}
if (sharedRes == NULL) {
ass = const_cast<AssetManager*>(this)->
mZipSet.getZipResourceTableAsset(ap.path);
if (ass == NULL) {
ALOGV("loading resource table %s\n", ap.path.string());
ass = const_cast<AssetManager*>(this)->
openNonAssetInPathLocked("resources.arsc",
Asset::ACCESS_BUFFER,
ap);
if (ass != NULL && ass != kExcludedAsset) {
ass = const_cast<AssetManager*>(this)->
mZipSet.setZipResourceTableAsset(ap.path, ass);
}
}
if (nextEntryIdx == 0 && ass != NULL) {
// If this is the first resource table in the asset
// manager, then we are going to cache it so that we
// can quickly copy it out for others.
ALOGV("Creating shared resources for %s", ap.path.string());
sharedRes = new ResTable();
sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
#ifdef HAVE_ANDROID_OS
const char* data = getenv("ANDROID_DATA");
LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set");
String8 overlaysListPath(data);
overlaysListPath.appendPath(kResourceCache);
overlaysListPath.appendPath("overlays.list");
addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx);
#endif
sharedRes = const_cast<AssetManager*>(this)->
mZipSet.setZipResourceTable(ap.path, sharedRes);
}
}
} else {
ALOGV("loading resource table %s\n", ap.path.string());
ass = const_cast<AssetManager*>(this)->
openNonAssetInPathLocked("resources.arsc",
Asset::ACCESS_BUFFER,
ap);
shared = false;
}
if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
ALOGV("Installing resource asset %p in to table %p\n", ass, mResources);
if (sharedRes != NULL) {
ALOGV("Copying existing resources for %s", ap.path.string());
mResources->add(sharedRes);
} else {
ALOGV("Parsing resources for %s", ap.path.string());
mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
}
onlyEmptyResources = false;
if (!shared) {
delete ass;
}
} else {
ALOGV("Installing empty resources in to table %p\n", mResources);
mResources->addEmpty(nextEntryIdx + 1);
}
if (idmap != NULL) {
delete idmap;
}
MY_TRACE_END();
return onlyEmptyResources;
}
appendPathToResTable 方法是加载(解析)资源文件的入口,其中 asset_path 对象里的 idmap 实际上保存的是 idmapPath,因此 appendPathToResTable() 方法首先尝试加载该 idmap 文件,然后再通过 ResTable::add() 方法去加载 resource 文件。ResTable::add() 方法经过一系列预处理(头部解析)之后,最后会调用 ResTable::parsePackage() 方法解析完整的 resource 文件(包括解析idmap文件)。
idmap是根据新旧资源(overlay)生成的id映射表:详情可参考_libs/androidfw/ResourceTypes.cpp#ResTable::createIdmap() _。
-
在加载新资源的时候,通过这个表可以将新资源的 entryList 加载到旧资源对应的 typeIndex 上,因为上层代码使用的是旧资源的 resId,需要根据旧资源的 packageId 和 typeId 查找。
-
而在查找资源的时候,通过这个表可以找到新资源的 entryIndex。IdmapEntries 的结构是__“稀疏列表”__:即开头不存在的映射数保存为 entryOffset,中间不存在的映射填充NO_ENTRY(0xffffffff),尾端不存在的映射不保存,可以通过entryOffset + entryCount的范围进行判断。
-
Android5.0以下的系统会根据新旧资源文件生成 idmap,而在Android5.0及以上系统,则将这部分逻辑移除,idmap文件需要根据命令行生成。Runtime resource overlay
parsePackage
......
uint32_t id = dtohl(pkg->id);
KeyedVector<uint8_t, IdmapEntries> idmapEntries;
if (header->resourceIDMap != NULL) {
uint8_t targetPackageId = 0;
status_t err = parseIdmap(header->resourceIDMap, header->resourceIDMapSize, &targetPackageId, &idmapEntries);
if (err != NO_ERROR) {
ALOGW("Overlay is broken");
return (mError=err);
}
id = targetPackageId;
}
if (id >= 256) {
LOG_ALWAYS_FATAL("Package id out of range");
return NO_ERROR;
} else if (id == 0) {
// This is a library so assign an ID
id = mNextPackageId++;
}
parsePackage 方法首先会校验数据格式,然后如果存在 idmap 的话,就调用 parseIdmap() 方法解析 idmap 文件,并保存到 idmapEntries 中,并以 idmap 中的 targetPackageId 作为该资源新的packageId。
如果 idmap 的 targetPackageId 为0,或者该资源的 packageId 为 0(即共享/动态的资源,如WebView的资源),那么就以 id = mNextPackageId++ 的值作为该资源的 packageId,mNextPackageId 默认从 0x02 开始(因为 0x01 是系统资源)。
PackageGroup* group = NULL;
Package* package = new Package(this, header, pkg);
if (package == NULL) {
return (mError=NO_MEMORY);
}
......
size_t idx = mPackageMap[id];
if (idx == 0) {
idx = mPackageGroups.size() + 1;
char16_t tmpName[sizeof(pkg->name)/sizeof(char16_t)];
strcpy16_dtoh(tmpName, pkg->name, sizeof(pkg->name)/sizeof(char16_t));
group = new PackageGroup(this, String16(tmpName), id);
if (group == NULL) {
delete package;
return (mError=NO_MEMORY);
}
err = mPackageGroups.add(group);
if (err < NO_ERROR) {
return (mError=err);
}
mPackageMap[id] = static_cast<uint8_t>(idx);
// Find all packages that reference this package
size_t N = mPackageGroups.size();
for (size_t i = 0; i < N; i++) {
mPackageGroups[i]->dynamicRefTable.addMapping(group->name, static_cast<uint8_t>(group->id));
}
} else {
group = mPackageGroups.itemAt(idx - 1);
if (group == NULL) {
return (mError=UNKNOWN_ERROR);
}
}
err = group->packages.add(package);
紧接着,为当前 resource 文件创建一个新的 Package,然后通过 mPackageMap 查找是否已存在PackageGroup,如果不存在则创建一个并添加到到 mPackageGroups,然后将这个 packageId : index 的对应关系保存到 mPackageMap 中。mPackageMap 中保存着每个 packageId 对应的PackageGroup 的 index 信息。而 PackageGroup 保存着 packageId 相同的资源的信息,每个resource 文件都以 Package 的形式保存在 PackageGroup->packages 列表中。
接下来就要进入正题了--- resource 文件的解析,先来看一下 arsc 文件的格式:
/* Arsc struct
* +-----------------------+
* | Table Header |
* +-----------------------+
* | Res string pool |
* +-----------------------+
* | Package Header |
* +-----------------------+
* | Type strings |
* +-----------------------+
* | Key strings |
* +-----------------------+
* | DynamicRefTable chunk |
* +-----------------------+
* | Type spec | |
* |-----------------| * N |
* | Type info * M | |
* +-----------------------+
*/
parsePackage 方法解析的是 Type spec 和 Type info 这一部分的内容,而前面的头部内容已经在 ResTable::addInternal() 方法解析过了,这里就不再赘述。
Type spec 保存的是某一类资源的基础信息,而 Type info 则是这一类资源的具体内容,比如String类型的数据。“N" 表示可能有多个不同类型的资源数据,如string,layout,anim等等。而 “M” 则表示每一类的资源,可能会有多种不同的配置(即适配资源),资源查找的时候,会从这些资源中找到最为匹配的资源。因此从某种意义来说,Android系统从一开始就支持加载 resId 相同的资源。
当 ctype == RES_TABLE_TYPE_SPEC_TYPE 时,即开始解析 Type spec 的内容,这个过程会重复 N 次:
uint8_t typeIndex = typeSpec->id - 1;
ssize_t idmapIndex = idmapEntries.indexOfKey(typeSpec->id);
if (idmapIndex >= 0) {
typeIndex = idmapEntries[idmapIndex].targetTypeId() - 1;
}
如果存在 idmap,那么先通过 idmap 将新资源的 typeIndex 转换为为旧资源的 typeIndex。这部分逻辑主要与资源查找的逻辑有关。
TypeList& typeList = group->types.editItemAt(typeIndex);
if (!typeList.isEmpty()) {
const Type* existingType = typeList[0];
if (existingType->entryCount != newEntryCount && idmapIndex < 0) {
ALOGW("ResTable_typeSpec entry count inconsistent: given %d, previously %d", (int) newEntryCount, (int) existingType->entryCount);
// We should normally abort here, but some legacy apps declare
// resources in the 'android' package (old bug in AAPT).
}
}
然后查询 group->types 中是否已经有该 typeIndex 的资源存在,如果存在则校验 entryCount 是否一致。值得注意的是,Android5.0以下系统是不支持多个 packageId 相同的资源加载的,仅支持 overlay 资源加载,因此如果 entryCount 不一致,则会中断解析并退出。而Android5.0及以上系统,则是支持多个 packageId 相同的资源加载,因此此处仅仅只是输出一条warn信息。
Type* t = new Type(header, package, newEntryCount);
t->typeSpec = typeSpec;
t->typeSpecFlags = (const uint32_t*)(((const uint8_t*)typeSpec) + dtohs(typeSpec->header.headerSize));
if (idmapIndex >= 0) {
t->idmapEntries = idmapEntries[idmapIndex];
}
typeList.add(t);
group->largestTypeId = max(group->largestTypeId, typeSpec->id);
entryCount 校验一致之后,即为当前 typeId 的资源创建一个 Type 类型的数据结构,用以保存接下来要解析的具体资源数据。
Type spec 解析完之后,紧着就会开始解析 Type info 的内容,此时 ctype == RES_TABLE_TYPE_TYPE。这个过程会重复 M * N 次:
uint8_t typeIndex = type->id - 1;
ssize_t idmapIndex = idmapEntries.indexOfKey(type->id);
if (idmapIndex >= 0) {
typeIndex = idmapEntries[idmapIndex].targetTypeId() - 1;
}
首先,依然是根据 idmap 数据,转换 typeIndex,此处不再赘述。
TypeList& typeList = group->types.editItemAt(typeIndex);
if (typeList.isEmpty()) {
ALOGE("No TypeSpec for type %d", type->id);
return (mError=BAD_TYPE);
}
Type* t = typeList.editItemAt(typeList.size() - 1);
if (newEntryCount != t->entryCount) {
ALOGE("ResTable_type entry count inconsistent: given %d, previously %d", (int)newEntryCount, (int)t->entryCount);
return (mError=BAD_TYPE);
}
if (t->package != package) {
ALOGE("No TypeSpec for type %d", type->id);
return (mError=BAD_TYPE);
}
t->configs.add(type);
紧接着依然是校验 entryCount 是不是一致。注意这里拿到的 Type 对象是上一步RES_TABLE_TYPE_SPEC_TYPE 时创建的,因此理论上来说不会存在 entryCount 不一致的问题,如果存在则说明 resource 文件有问题,因此此处直接中断并退出解析。
entryCount 校验一致之后,就将 ResTable_type 资源数据添加到 t->configs,t->configs 存储的是同一 typeId 不同维度的资源,即针对不同版本、系统或者分辨率的适配资源。资源查找的时候,就是通过循环遍历这个列表,找到最适合的资源。
if (group->dynamicRefTable.entries().size() == 0) {
status_t err = group->dynamicRefTable.load((const ResTable_lib_header*) chunk);
if (err != NO_ERROR) {
return (mError=err);
}
// Fill in the reference table with the entries we already know about.
size_t N = mPackageGroups.size();
for (size_t i = 0; i < N; i++) {
group->dynamicRefTable.addMapping(mPackageGroups[i]->name, mPackageGroups[i]->id);
}
}
最后,如果 ctype == RES_TABLE_LIBRARY_TYPE,说明这部分数据是共享(动态)资源映射表,这是Android5.0新加的数据类型,保存的是 packageName : packageId 的映射关系,表示该资源的 packageId 是动态分配的。其中 packageName 和 packageId 均为其他(共享)资源文件编译时的包名和ID,通过 DynamicRefTable::load() 方法解析之后,保存在 mEntries 中。
然后将已知(即已经解析的resource)的运行时包名和ID的对应关系通过 DynamicRefTable::addMappings() 方法更新并保存到 mLookupTable中。这样我们通过编译时packageId 就可以在 mLookupTable 中查询的到运行时对应的 packageId 了。packageName 只是作为建立这个对应关系的居间key值,并无其他特别意义。
例如 packageId 为 0x7f 的资源文件可以 @dref/0x7030005 的方式引用共享资源文件的资源 0x7030005,这个资源文件编译时 packageId 为 0x70,但是其运行时 packageId 可能还是 0x70,也有可能为其他动态分配的值。另外__当引用的共享(动态)资源的 packageId为 0x00 时,如 @dref/0x0030005 则表示引用的是当前资源文件的共享资源。__
DynamicRefTable
DynamicRefTable的数据结构如下:
// include/androidfw/ResourceTypes.h
/**
* Holds the shared library ID table. Shared libraries are assigned package IDs at
* build time, but they may be loaded in a different order, so we need to maintain
* a mapping of build-time package ID to run-time assigned package ID.
*
* Dynamic references are not currently supported in overlays. Only the base package
* may have dynamic references.
*/
class DynamicRefTable
{
public:
DynamicRefTable(uint8_t packageId);
// Loads an unmapped reference table from the package.
status_t load(const ResTable_lib_header* const header);
// Adds mappings from the other DynamicRefTable
status_t addMappings(const DynamicRefTable& other);
// Creates a mapping from build-time package ID to run-time package ID for
// the given package.
status_t addMapping(const String16& packageName, uint8_t packageId);
// Performs the actual conversion of build-time resource ID to run-time
// resource ID.
inline status_t lookupResourceId(uint32_t* resId) const;
inline status_t lookupResourceValue(Res_value* value) const;
inline const KeyedVector<String16, uint8_t>& entries() const {
return mEntries;
}
private:
const uint8_t mAssignedPackageId;
uint8_t mLookupTable[256];
KeyedVector<String16, uint8_t> mEntries;
};
其中,mEntries 保存的是 packageName : packageId 键值对,而 mLookupTable 保存的则是 packageId : assignedPackageId。 packageName 和 packageId 是指编译时包名和ID,而 assignedPackageId 是指运行时分配的ID。
附注
Android5.0以下系统不支持加载 packageId 相同的 arsc 资源文件,但是支持加载 overlay 资源。另外Android系统本身是支持加载多个 resId 相同的资源,这些资源就是在同一个 arsc 文件里的适配资源。适配资源是指针对不同分辨率,不同系统版本而配置的资源,Android系统在查找资源的过程中,会从这些资源中找到最为匹配的资源,这也是Android系统底层为适配提供的技术支持。
Android5.0系统为了支持 splits apk 的功能,修改了资源加载的机制,支持加载多个 packageId 相同的 arsc 资源文件,同时也支持加载共享资源(如webview资源,这类资源的packageId是动态变化的,与加载顺序有关,默认从0x02开始)。详情见:Support multiple resource tables with same package
Android5.0 WebView(跨应用加载代码和资源)的加载过程
未完待续
" aapt -I " 命令 ( -I add an existing package to base include set )
未完待续
xml文件引用系统资源,如 “@android:dimen/app_icon_size”
因为__Android系统资源的ID是固定的__,因此,当我们在xml文件里引用系统资源时,aapt会直接将其转换成系统资源的ID,如 “@android:dimen/app_icon_size” 转换成 “ @ref/0x01050000” 。