通常情況下,當我們需要使用資源的時候,都是通過api直接呼叫:
getResources().getDrawable(R.mipmap.ic_launcher);
複製程式碼
通過getResources()的眾多方法可以獲取到整個apk包裡面的資源,那麼我們是如何獲取到資源的?這些資源又是如何被載入到記憶體中的? 今天我們來一起分析一下app是如何載入資原始檔的。
資源載入過程
首先我們通過getResources()
來看一下Resource是在哪裡被建立出來的。
// AppCompatActivity.java
@Override
public Resources getResources() {
if (mResources == null && VectorEnabledTintResources.shouldBeUsed()) {
mResources = new VectorEnabledTintResources(this, super.getResources());
}
return mResources == null ? super.getResources() : mResources;
}
// Context.java
public abstract Resources getResources();
複製程式碼
一路點選過去發現,最終又到了Context這個上下文中,而Context是一個抽象類,所以我們需要找到其實現類ContextImpl.java
。
// ContextImpl.java
@Override
public Resources getResources() {
return mResources;
}
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
Resources resources = packageInfo.getResources(mainThread);
mResources = resources;
}
// LoadedApk.java
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
}
return mResources;
}
// ResourcesManager.java
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());
}
複製程式碼
發現在ContextImpl.java
中Resources是被ResourcesManager的getTopLevelResources
方法返回的。ResourcesManager是資源的管理類,我們著重看一下它。
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");
// 生成資源key物件
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);
}
}
private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
// 從快取中獲取Resources
if (activityToken != null) {
final ActivityResources activityResources =
getOrCreateActivityResourcesStructLocked(activityToken);
// 通過資源key物件獲取快取Resources
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl);
}
} else {
// 通過資源key物件獲取快取Resources
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesLocked(classLoader, resourcesImpl);
}
}
}
// 沒有快取建立ResourcesImpl
ResourcesImpl resourcesImpl = createResourcesImpl(key);
synchronized (this) {
// 通過資源key物件獲取快取Resources
ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
if (existingResourcesImpl != null) {
resourcesImpl.getAssets().close();
resourcesImpl = existingResourcesImpl;
} else {
// 如果快取中沒有,將建立好的ResourceImpl加入到快取中
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
}
final Resources resources;
if (activityToken != null) {
// 通過資源key物件獲取快取Resources
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl);
} else {
// 通過資源key物件獲取快取Resources
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
}
return resources;
}
}
複製程式碼
生成資源Resources物件的步驟:
- 首先生成資源Key物件,用於將資源物件新增到快取map中或者從快取map中查詢資源物件
- 通過activityToken判斷是否關聯Activity,並從快取中通過key獲取快取的ResourcesImpl物件,如果有直接返回
- 如果沒有快取的ResourcesImpl物件,則直接建立新的快取ResourcesImpl物件,通過
createResourcesImpl(key)
來建立。 - 建立完之後在將其加入快取Map中,並最終返回
Resources.setImpl(impl)
方法,建立出Resources物件並返回。
所以我們需要著重看下createResourcesImpl
方法,這個方法才是建立ResourcesImpl的核心。
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;
}
複製程式碼
可以發現這兒方法建立了AssetManager
資產管理類:
@VisibleForTesting
protected @NonNull AssetManager createAssetManager(@NonNull final ResourcesKey key) {
AssetManager assets = new AssetManager();
if (key.mResDir != null) {
if (assets.addAssetPath(key.mResDir) == 0) {
throw new Resources.NotFoundException("failed to add asset path " + key.mResDir);
}
}
return assets;
}
// LoadedApk.java
private void setApplicationInfo(ApplicationInfo aInfo) {
final int myUid = Process.myUid();
aInfo = adjustNativeLibraryPaths(aInfo);
mApplicationInfo = aInfo;
mAppDir = aInfo.sourceDir;
mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
}
複製程式碼
AssetManager是用來載入制定路徑下的資原始檔的,所以我們想要獲取的資原始檔都是在這個類中找到的。而key.mResDir
就是app的資源路徑。
所以AssetManager才是整個資源載入的關鍵所在。
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 native final void init(boolean isSystem);
private native final int loadResourceValue(int ident, short density, TypedValue outValue,
boolean resolve);
複製程式碼
在建立AssetManager的時候,它首先會去呼叫native層中的init方法
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem)
{
if (isSystem) {
verifySystemIdmaps();
}
// AssetManager.cpp
AssetManager* am = new AssetManager();
if (am == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", "");
return;
}
am->addDefaultAssets();
ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast<jlong>(am));
}
bool AssetManager::addDefaultAssets()
{
const char* root = getenv("ANDROID_ROOT");
LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");
String8 path(root);
// kSystemAssets -> framework/framework-res.apk
// 載入系統的資源如顏色、圖片、文字
path.appendPath(kSystemAssets);
return addAssetPath(path, NULL);
}
bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
{
asset_path ap;
// 判斷是否已經載入過了
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;
}
}
// 檢查路徑是否有一個androidmanifest.xml
Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
kAndroidManifest, Asset::ACCESS_BUFFER, ap);
if (manifestAsset == NULL) {
// 如果不包含任何資源
delete manifestAsset;
return false;
}
delete manifestAsset;
// 新增
mAssetPaths.add(ap);
if (mResources != NULL) {
appendPathToResTable(ap);
}
return true;
}
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) {
// mAssetPaths中儲存的第一個資源包路徑是系統資源的路徑,
// 即framework-res.apk的路徑,它在zygote啟動時已經載入了
// 可以通過mZipSet.getZipResourceTable獲得其ResTable物件
sharedRes = const_cast<AssetManager*>(this)->
mZipSet.getZipResourceTable(ap.path);
// 對於APP來說,肯定不為NULL
if (sharedRes != NULL) {
// 得到系統資源包路徑中resources.arsc個數
nextEntryIdx = sharedRes->getTableCount();
}
}
// 當引數是mAssetPaths中除第一個以外的其他資源資源包路徑,
// 比如app自己的資源包路徑時,走下面的邏輯
if (sharedRes == NULL) {
// 檢查該資源包是否被其他程式載入了,這與ZipSet資料結構有關,後面在詳細介紹
ass = const_cast<AssetManager*>(this)->
mZipSet.getZipResourceTableAsset(ap.path);
if (ass == NULL) {
ALOGV("loading resource table %s\n", ap.path.string());
// 建立Asset物件,就是開啟resources.arsc
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);
}
}
// 只有在zygote啟動時,才會執行下面的邏輯
// 為系統資源建立 ResTable,並加入到mZipSet裡。
if (nextEntryIdx == 0 && ass != NULL) {
ALOGV("Creating shared resources for %s", ap.path.string());
// 建立ResTable物件,並把前面與resources.arsc關聯的Asset物件,加入到這個ResTabl中
sharedRes = new ResTable();
sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
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 {
// 非系統資源包時,將與resources.arsc關聯的Asset物件加入到Restable中
// 此過程會解析resources.arsc檔案。
ALOGV("Parsing resources for %s", ap.path.string());
mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
}
onlyEmptyResources = false;
if (!shared) {
delete ass;
}
} else {
mResources->addEmpty(nextEntryIdx + 1);
}
if (idmap != NULL) {
delete idmap;
}
MY_TRACE_END();
return onlyEmptyResources;
}
複製程式碼
獲取系統路徑並解析resources.arsc
這個檔案對映表,其中存放的就是資源的索引。
至此,系統資源載入就完成了。
獲取資源
現在所有資源的索引已經載入到記憶體中了,我們就需要獲取資源並載入了。
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/image"/>
複製程式碼
我們看一下ImageView的src屬性圖片是如何載入的,通常情況下,src屬性和我們自定義屬性很想,都是自定義的屬性,只不過一個是系統定義的,一個是我們定義的,所有我們直接看ImageView的建構函式:
public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
}
@Nullable
public Drawable getDrawable(@StyleableRes int index) {
final TypedValue value = mValue;
if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value);
}
return mResources.loadDrawable(value, value.resourceId, mTheme);
}
return null;
}
複製程式碼
最後也是通過Resources的loadDrawable(value, value.resourceId, mTheme)
方法來獲取到資源:
// Resources.java
@NonNull
Drawable loadDrawable(@NonNull TypedValue value, int id, @Nullable Theme theme)
throws NotFoundException {
return mResourcesImpl.loadDrawable(this, value, id, theme, true);
}
// ResourcesImpl.java
@Nullable
Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
boolean useCache) throws NotFoundException {
try {
Drawable dr;
if (cs != null) {
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(wrapper, value, id, null);
}
return dr;
} catch (Exception e) {
throw nfe;
}
}
複製程式碼
載入drawable的時候,首先會現在快取中獲取,如果有就直接返回,如果沒有的話,說明沒有載入過這個資原始檔,就需要先載入並快取到caches中,所有我們看loadDrawableForCookie
方法:
private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id,
Resources.Theme theme) {
final String file = value.string.toString();
final Drawable dr;
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
try {
if (file.endsWith(".xml")) {
// 類似於shape的xml檔案
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXml(wrapper, rp, theme);
rp.close();
} else {
// 不是xml檔案,就是圖片檔案,就將其載入到記憶體中
// value.assetCookie這個cookie就是圖片在native層中的mAssetPaths中的索引
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;
}
複製程式碼
loadDrawableForCookie方法會首先判斷是否是.xml
檔案,如果是xml檔案,則直接使用xml解析並建立Drawable物件返回,如果不是xml檔案,則認為是圖片的資原始檔,使用流的方式將資源讀取到快取中。
至此為止,資源是如何載入的和如何從記憶體中獲取資源都完成了。