Resource基础类型
-
动画资源 定义预先确定的动画。 补间动画保存在 res/anim/ 中并通过 R.anim 类访问。 帧动画保存在 res/drawable/ 中并通过 R.drawable 类访问。
-
颜色状态列表资源 定义根据 View 状态而变化的颜色资源。 保存在 res/color/ 中并通过 R.color 类访问。
-
可绘制资源 使用位图或 XML 定义各种图形。 保存在 res/drawable/ 中并通过 R.drawable 类访问。
-
布局资源 定义应用界面的布局。 保存在 res/layout/ 中并通过 R.layout 类访问。
-
菜单资源 定义应用菜单的内容。 保存在 res/menu/ 中并通过 R.menu 类访问。
-
字符串资源 定义字符串、字符串数组和复数形式(并包括字符串格式和样式)。 保存在 res/values/ 中,并通过 R.string、R.array 和 R.plurals 类访问。
-
样式资源 定义界面元素的外观和格式。 保存在 res/values/ 中并通过 R.style 类访问。
-
更多资源类型
- Bool:包含布尔值的 XML 资源。
- 颜色:包含颜色值(十六进制颜色)的 XML 资源。
- 维度:包含维度值(及度量单位)的 XML 资源。
- ID:为应用资源和组件提供唯一标识符的 XML 资源。
- 整数:包含整数值的 XML 资源。
- 整数数组:提供整数数组的 XML 资源。
- 类型化数组:提供 TypedArray(可用于可绘制对象数组)的 XML 资源。
Resource加载源码分析
一般情况下,我们都是通过getResources()
的方式获取资源,其实getResources()
的默认指向是当前的context。我们最熟悉的context也就是Activity了,接下来我们去查看看Activity的context的实现类contextImp是如何初始化的。Activity的创建是在ActivityThread中完成的,Activity创建的入口方法是handleLaunchActivity,最后回调到performLaunchActivity,而ContextImpl的创建也是在performLaunchActivity中完成。
Resource创建初始化
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
try {
if (activity != null) {
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, window, r.configCallback);
...
}
上述代码完成了两件事,创建了ContextImpl,将Activity与ContextImpl绑定。由于上文中我们提到getResource()
的调用是通过context的回调,那么ContextImpl必然和Resource有必然联系,进一步查看ContextImpl的实例化代码:
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
final int displayId;
...
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
...
return appContext;
}
通过ContextImpl的静态方法,我们可以看到如下代码:
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
Configuration overrideConfiguration) {
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
activityToken, null, 0, classLoader);
final ResourcesManager resourcesManager = ResourcesManager.getInstance();
// Create the base resources for which all configuration contexts for this Activity
// will be rebased upon.
context.setResources(resourcesManager.createBaseActivityResources(activityToken,
packageInfo.getResDir(),
splitDirs,
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
classLoader));
context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
context.getResources());
return context;
}
创建了一个ContenxtImpl实例,紧接着通过ResourcesManager获取了一个ResourcesManager单例对象,通过这个单例对象创建与当前activity唯一对应的Resources对象并将该Resources对象赋值给ContextImpl 的mResources成员变量。
public @Nullable Resources createBaseActivityResources(@NonNull 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#createBaseActivityResources");
final ResourcesKey key = new ResourcesKey(
resDir,
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
synchronized (this) {
getOrCreateActivityResourcesStructLocked(activityToken);
}
// Update any existing Activity Resources references.
updateResourcesForActivity(activityToken, overrideConfig, displayId,
false /* movedToDifferentDisplay */);
// Now request an actual Resources object.
return getOrCreateResources(activityToken, key, classLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
创建了一个ResourcesKey的对象实例,ResourcesKey中保存着Resources的一些文件信息,例如resDir是资源文件Resource.arsc的路劲,在后期获取资源时,其实也是花样读文件的一个过程。继续往下看:
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
1...
}
//3.重点,创建ResourcesImpl对象
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
// Add this ResourcesImpl to the cache.
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
final Resources resources;
if (activityToken != null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
return resources;
}
同步代码块中都是一些缓存的操作,如果从缓存中获取到那么那么就将ResourcesImpl用Resources进行包装,ResourcesImpl创建是创建了AssetManager对象并将资源文件的path赋值。
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
final AssetManager assets = createAssetManager(key);
...
return impl;
}
目前为止,Resoucrce的创建已经完成,可以知道,我们通过getResoucrce()
方法的使用是通过ContextImpl的mResources变量去获取资源,而mResources的实例是ResourceImpl,最终是通过AssetMananger去获取资源文件的。AssetManager是一个全局唯一的,在createAssetManager方法创建时,通过传入apk的资源文件路径等参数创建,AssetManager可以认为是一个资源文件加载器,多说加载逻辑都在native层面处理,稍后再看。
获取
通过上文Resource的创建和初始化,现在知道要获取一个一个字符串资源或者图片资源其实是通过那个全局唯一的AssetManager获取的,接下来我们看看AssetManager是怎么获取资源的:
获取字符串资源
直接进入AssetManager类中去查看getResourceText方法:
@UnsupportedAppUsage
@Nullable CharSequence getResourceText(@StringRes int resId) {
synchronized (this) {
final TypedValue outValue = mValue;
if (getResourceValue(resId, 0, outValue, true)) {
return outValue.coerceToString();
}
return null;
}
}
@UnsupportedAppUsage
boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue, boolean resolveRefs) {
Preconditions.checkNotNull(outValue, "outValue");
synchronized (this) {
ensureValidLocked();
final int cookie = nativeGetResourceValue(
mObject, resId, (short) densityDpi, outValue, resolveRefs);
if (cookie <= 0) {
return false;
}
// Convert the changing configurations flags populated by native code.
outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
outValue.changingConfigurations);
if (outValue.type == TypedValue.TYPE_STRING) {
outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
}
return true;
}
}
通过resId获取到目标资源,通过native去获取资源保存在TypedValue中,然后返回。TypedValue的获取逻辑后文再讲。
获取图片资源
文本资源处理逻辑比较简单,下面我们看看图片资源如何加载处理的。一般的图片资源我们都是通过getDrawable(id)
来获取的。很容易就可以回调到如下方法:
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
throws NotFoundException {
return getDrawableForDensity(id, 0, theme);
}
getDrawableForDensity()
方法会回调到Resourece
中,然后通过ResourcesImpl
的getValueForDensity()
方法获得id的的值:
void getValueForDensity(@AnyRes int id, int density, TypedValue outValue,
boolean resolveRefs) throws NotFoundException {
boolean found = mAssets.getResourceValue(id, density, outValue, resolveRefs);
if (found) {
return;
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}
得到了id对应的值,保存在TypeValue中。TypeValue并不是一个Drawable而是一个Drawable文件的文件路劲,ResourceImpl.loadDrawable()
通过TypeValue的数值去加载Drawables文件:
@Nullable
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws NotFoundException {
...
try {
...
Drawable dr;
boolean needsNewDrawableAfterCache = false;
if (cs != null) {
...
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(wrapper, value, id, density);
}
...
return dr;
} catch (Exception e) {
...
}
}
处理了很多的逻辑,比如判断是不是ColorDrawable、检查缓存、预加载的资源文件中是否存在需要查找的Drawable等。如果没有缓存,没有预加载,然后通过loadDrawableForCookie()
去创建一个:
@Nullable
private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density) {
...
try {
// Perform a linear search to check if we have already referenced this resource before.
if (stack.contains(id)) {
throw new Exception("Recursive reference in drawable");
}
stack.push(id);
try {
if (file.endsWith(".xml")) {
if (file.startsWith("res/color/")) {
dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
} else {
dr = loadXmlDrawable(wrapper, value, id, density, file);
}
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
AssetInputStream ais = (AssetInputStream) is;
dr = decodeImageDrawable(ais, wrapper, value);
}
} finally {
stack.pop();
}
} catch (Exception | StackOverflowError e) {
...
}
...
return dr;
}
AssetManager在加载Drawable之前先会将要加载的Drawable的信息加载到TypedValue中,然后再通过Drawable的文件路径去获取,通过Drawable文件路劲判断加载类型。如果不是“.xml”类型的直接通过AssetManager去加载,反之在传入的loadXmlDrawable()
中:
private Drawable loadXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density, String file)
throws IOException, XmlPullParserException {
try (
XmlResourceParser rp =
loadXmlResourceParser(file, id, value.assetCookie, "drawable")
) {
return Drawable.createFromXmlForDensity(wrapper, rp, density, null);
}
}
这段代码很清晰,通过loadXmlResourceParser()
返回XmlResourceParser对象,然后Drawable的静态方法createFromXmlForDensity()
解析并返回。在loadXmlResourceParser()
中,有三步操作:获取缓存->如果没有加载资源->存储资源。缓存是通过ResourceImpl的成员变量mCachedXmlBlocks
进行缓存,这是一个大小为4的“循环”数组。加载资源也是通过AssetManager去加载。接下来去AssetManager中继续看:
@NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException {
Preconditions.checkNotNull(fileName, "fileName");
synchronized (this) {
ensureOpenLocked();
final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
if (xmlBlock == 0) {
throw new FileNotFoundException("Asset XML file: " + fileName);
}
final XmlBlock block = new XmlBlock(this, xmlBlock);
incRefsLocked(block.hashCode());
return block;
}
}
可以看到一个Xml文件被打开,然后构建成一个XmlBlock
对象返回,Drawable会读取xml文件中的内容并解析,最后渲染出来。
回到图片资源转载的最开始,AssetManager通过getResourceValue()
加载id对应的值并从底层复制数据给TypeValue实例。如果是个获取字符串资源,TypeValue将会是String类型的字符串,但是如果是drawable类型资源,那么TypeValue就是drawable所对应的xml的文件路径。
在AsssetManager对象创建的时候,我们通过一个”Key“的实例对象传入了apk的信息,其中就保存资源文件的路径,其实这个这个文件也就是resource.arsc文件,接下里看一下他是如何从resouece.ars中加载真实的数据的。
boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
boolean resolveRefs) {
synchronized (this) {
ensureValidLocked();
final int cookie = nativeGetResourceValue(
mObject, resId, (short) densityDpi, outValue, resolveRefs);
outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
outValue.changingConfigurations);
if (outValue.type == TypedValue.TYPE_STRING) {
outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
}
return true;
}
}
可以看到从上面代码开始就回调到了native层。
static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid,
jshort density, jobject typed_value,
jboolean resolve_references) {
ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
Res_value value;
ResTable_config selected_config;
uint32_t flags;
ApkAssetsCookie cookie =
assetmanager->GetResource(static_cast<uint32_t>(resid), false /*may_be_bag*/,
static_cast<uint16_t>(density), &value, &selected_config, &flags);
if (cookie == kInvalidCookie) {
return ApkAssetsCookieToJavaCookie(kInvalidCookie);
}
uint32_t ref = static_cast<uint32_t>(resid);
if (resolve_references) {
cookie = assetmanager->ResolveReference(cookie, &value, &selected_config, &flags, &ref);
if (cookie == kInvalidCookie) {
return ApkAssetsCookieToJavaCookie(kInvalidCookie);
}
}
return CopyValue(env, cookie, value, ref, flags, &selected_config, typed_value);
}
AssetManager2的成员方法GetResource()
获取到ApkAssetsCookie结构体,对应到java层就是指当前资源来源于packageGroup中cookie的index。cookie添加的顺序实际上和package数据包添加的顺序是一致,也就是说,可以通过这个cookief反向查找package数据包。继续看GetResource()
:
ApkAssetsCookie AssetManager2::GetResource(uint32_t resid, bool may_be_bag,
uint16_t density_override, Res_value* out_value,
ResTable_config* out_selected_config,
uint32_t* out_flags) const {
FindEntryResult entry;
ApkAssetsCookie cookie =
FindEntry(resid, density_override, false /* stop_at_first_match */, &entry);
if (cookie == kInvalidCookie) {
return kInvalidCookie;
}
if (dtohs(entry.entry->flags) & ResTable_entry::FLAG_COMPLEX) {
if (!may_be_bag) {
...
return kInvalidCookie;
}
// Create a reference since we can't represent this complex type as a Res_value.
out_value->dataType = Res_value::TYPE_REFERENCE;
out_value->data = resid;
*out_selected_config = entry.config;
*out_flags = entry.type_flags;
return cookie;
}
const Res_value* device_value = reinterpret_cast<const Res_value*>(
reinterpret_cast<const uint8_t*>(entry.entry) + dtohs(entry.entry->size));
out_value->copyFrom_dtoh(*device_value);
// Convert the package ID to the runtime assigned package ID.
entry.dynamic_ref_table->lookupResourceValue(out_value);
*out_selected_config = entry.config;
*out_flags = entry.type_flags;
return cookie;
}
这段代码的核心方法就是FindEntry()
,从名字来开就是查找entry,通过查找到的entry去判断返回值到底是什么。从代码来看返回值就两种:
- resid:当前引用比较复杂,不是一个简单类型,返回resid,这个resid不是一个真正的资源数据,而是一个资源数据的引用id。
- 真是的资源数据。
现在知道了资源数据是从
FindEntry
方法中获取的,那么我们继续去看看它到底是如何获取的:
ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_override,
bool /*stop_at_first_match*/,
FindEntryResult* out_entry) const {
...
const uint32_t package_id = get_package_id(resid);//解析当前资源id中的packageID
const uint8_t type_idx = get_type_id(resid) - 1;//解析当前资源的typeID
const uint16_t entry_idx = get_entry_id(resid);//解析当前资源的entryID
const uint8_t package_idx = package_ids_[package_id];
if (package_idx == 0xff) {
...
return kInvalidCookie;
}
const PackageGroup& package_group = package_groups_[package_idx];//获取资源的packageGroup
const size_t package_count = package_group.packages_.size();
....
//如果desired_config与缓存过的configuration_相同,那么我们可以使用过滤后的列表,并且我们不需要匹配配置,因为它们已经匹配了。
const bool use_fast_path = desired_config == &configuration_;
//遍历当前的packageGroup,根据typeID查找资源。
for (size_t pi = 0; pi < package_count; pi++) {
const ConfiguredPackage& loaded_package_impl = package_group.packages_[pi];
const LoadedPackage* loaded_package = loaded_package_impl.loaded_package_;
ApkAssetsCookie cookie = package_group.cookies_[pi];
// 考虑到typeID在当前包中的偏移,获取TypeSpec
const TypeSpec* type_spec = loaded_package->GetTypeSpecByTypeIndex(type_idx);
if (UNLIKELY(type_spec == nullptr)) {
continue;
}
uint16_t local_entry_idx = entry_idx;
//获取到的typeSpec中的idMap如果不为null,那么就需要idMap进行查找,有没有需要对Entry_id进行替换或者转化,如果没有映射,
//说明资源不再这个包内,说明资源找错了,直接返回。
//简单来说:type_spec中的IdMap是处理资源文件的映射的,有的资源不是直接获取,而是通过IdMap转化后才是当前包中真正的资源id.
if (type_spec->idmap_entries != nullptr) {
if (!LoadedIdmap::Lookup(type_spec->idmap_entries, local_entry_idx, &local_entry_idx)) {
continue;
}
}
type_flags |= type_spec->GetFlagsForEntryIndex(local_entry_idx);
// If the package is an overlay, then even configurations that are the same MUST be chosen.
const bool package_is_overlay = loaded_package->IsOverlay();
const FilteredConfigGroup& filtered_group = loaded_package_impl.filtered_configs_[type_idx];
if (use_fast_path) {//直接从filtered_group.configurations中获取,刚才提到,当前资源的group在之前获取过,被缓存下来了。
const std::vector<ResTable_config>& candidate_configs = filtered_group.configurations;
const size_t type_count = candidate_configs.size();
for (uint32_t i = 0; i < type_count; i++) {
const ResTable_config& this_config = candidate_configs[i];
// We can skip calling ResTable_config::match() because we know that all candidate
// configurations that do NOT match have been filtered-out.
if ((best_config == nullptr || this_config.isBetterThan(*best_config, desired_config)) ||
(package_is_overlay && this_config.compare(*best_config) == 0)) {
const ResTable_type* type_chunk = filtered_group.types[i];
const uint32_t offset = LoadedPackage::GetEntryOffset(type_chunk, local_entry_idx);
if (offset == ResTable_type::NO_ENTRY) {
continue;
}
best_cookie = cookie;
best_package = loaded_package;
best_type = type_chunk;
best_config = &this_config;
best_offset = offset;
}
}
} else {//没有缓存,从头获取。
const auto iter_end = type_spec->types + type_spec->type_count;
//循环typeSpec中映射好的资源关系,先寻找合适的config接着在尝试寻找有没有对应的entryID。
for (auto iter = type_spec->types; iter != iter_end; ++iter) {
ResTable_config this_config;
this_config.copyFromDtoH((*iter)->config);
if (this_config.match(*desired_config)) {//符合设备当前配置
if ((best_config == nullptr || this_config.isBetterThan(*best_config, desired_config)) ||
(package_is_overlay && this_config.compare(*best_config) == 0)) {
const uint32_t offset = LoadedPackage::GetEntryOffset(*iter, local_entry_idx);
if (offset == ResTable_type::NO_ENTRY) {
continue;
}
best_cookie = cookie;
best_package = loaded_package;
best_type = *iter;
best_config_copy = this_config;
best_config = &best_config_copy;
best_offset = offset;
}
}
}
}
}
...
//确定资源的存在,则会通过当前的资源类型以及资源类型中的偏移数组通过方法GetEntryFromOffset获取对应的entry。
const ResTable_entry* best_entry = LoadedPackage::GetEntryFromOffset(best_type, best_offset);
...
return best_cookie;
}
上述代码主要的查找思路:
- 根据资源id,分别得到PackageId、typeIndex、entryIndex
- 根据PackageId找到对应PackageGroup的索引,进而得到对应的PackageGroup
- 从对应的PackageGroup中根据typeID拿到该PackageGroup中所有包含该类型的资源
- 遍历这些资源,根据entryID,从中选取最符合设备当前配置的entry
看到这如果没有一些resource的基础的话其实已经云里雾里了,不过没关系,下一篇Dfasef将会把本文中涉及到的一些知识点进一步讨论,接下来就只有通过Offset去获取真实的entry。下面继续看吧:
uint32_t LoadedPackage::GetEntryOffset(const ResTable_type* type_chunk, uint16_t entry_index) {
const size_t entry_count = dtohl(type_chunk->entryCount);
const size_t offsets_offset = dtohs(type_chunk->header.headerSize);
...
const uint32_t* entry_offsets = reinterpret_cast<const uint32_t*>(
reinterpret_cast<const uint8_t*>(type_chunk) + offsets_offset);
return dtohl(entry_offsets[entry_index]);
}
通过获取资源ResTable_type起始地址+ResTable_type的头部大小+头部起点地址,来找到entry偏移数组。偏移数组中的结果来确定当前的entry是否存在。
const ResTable_entry* LoadedPackage::GetEntryFromOffset(const ResTable_type* type_chunk,
uint32_t offset) {
...
return reinterpret_cast<const ResTable_entry*>(reinterpret_cast<const uint8_t*>(type_chunk) +
offset + dtohl(type_chunk->entriesStart));
}
type中记录对应entry中数据的偏移量,因此,可以通过简单的相加找到对应的地址。
总结
从上文我们可以知道:获取getResource()
默认的是通过Context中的Resource去获取的,Resource的真正实现类是ResourceImpl,但是RsoureceImpl其实是AssetManager的一个代理类而已。所有的获取字符串资源、drawable等资源都是通过AssetManager去获取。在native层面是通过上层传递的资源ID进行的查找逻辑,
通过解析资源Id得到PackageId、typeIndex、entryIndex,然后遍历PackageGroup查找目标资源或目标文件路径。最终获取到真正的资源或资源文件的引用,如果是文件路劲AssetManager去读取文件内容并通过xml解析器解析渲染。
资源ID:0xPPTTEEEE 。最高两位PP是指PackageID,一般编译之后,应用资源包是0x7f,系统资源包是0x01,而第三方资源包,则是从0x02开始逐个递增。接下来的两位TT,代表着当前资源类型id,如anim文件夹下的资源就是0x01,组合起来就是0x7f01.最后四位是指资源entryID,是指的资源每一项对应的id,可能是0000,一般是按照资源编译顺序递增,如果是0001,则当前资源完整就是0x7f010001.