前言
如果遇到問題歡迎在這個地址下留言:http://www.lxweimin.com/p/817a787910f2
上一篇文章和大家聊了聊Android是如何進行View的實例化。但是還是遺留了一個問題,Android是怎么讀取資源文件的,本文將會和大家來聊聊關于Resources類是怎么讀取到底層資源的。
其實這部分,有部分內容在我寫插件化的文章里面有聊到過,也畫過一張簡單的時序圖,不過這只是Resources類在Java層怎么初始化,怎么獲取對應資源的。本文將會更加重點的,系統的探索,native的初始化以及讀取數據。
不過,這僅僅只是一個很粗略的時序圖。實際上每個Resources都會被ResourcesManager管理著。但是Resources相當于一個代理類,實際上真正的操作都是由ResourcesImpl去完成。
ResourcesImpl管理著什么?它一般管理著一個apk中各種資源文件,其中它有一個很核心的類AssetManager,這才是真正連通native層進行解析。別看AssetManager的名字上好像指管理著asset文件夾,但是它的管理范圍一般是ApkAsset這個資源抽象對象。
有了大致的印象,我們來看看整個Android系統是如何管理ApksAsset以及AssetManager;就大致上弄明白了Android系統的資源管理體系。
正文
從上面時序圖中,我們可以主要的關注一下ContextImpl是怎么初始化的?怎么獲取到資源訪問的能力的,把目光放在createActivityContext方法上。
文件:/frameworks/base/core/java/android/app/ContextImpl.java
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
Configuration overrideConfiguration) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
String[] splitDirs = packageInfo.getSplitResDirs();
ClassLoader classLoader = packageInfo.getClassLoader();
if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
try {
classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName);
splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
} catch (NameNotFoundException e) {
// Nothing above us can handle a NameNotFoundException, better crash.
throw new RuntimeException(e);
} finally {
}
}
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
activityToken, null, 0, classLoader);
// Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY;
final CompatibilityInfo compatInfo = (displayId == Display.DEFAULT_DISPLAY)
? packageInfo.getCompatibilityInfo()
: CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
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;
}
這里面我們能夠看到之前在插件話分析一文熟悉的LoadApk對象,這個對象代表這apk包在Android中的內存對象。關于這個對象,將會在PMS中和大家解析解析。
我們先不去細究這個對象是怎么來的。但是我們可以方法名字大致上知道做了如下幾個事情:
- 1.讀取保存在LoadApk中的資源文件夾
- 2.讀取LoadApk中的classLoader,作為當前應用的主要ClassLoader。
- 3.實例化ContextImpl,這個ContextImpl在絕大部分情況下就是我們應用開發常用的Context。
- 4.初始化ResourceManager,資源管理器
- 把資源管理設置到Context中,之后Context才具有訪問資源的能力。
本文主要關注資源的加載,因此我們只需要研究ResourceManager,就能明白資源加載的原理了,關注下面這兩行代碼:
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));
因此,資源的初始化放在這個函數上:
- createBaseActivityResources 打開資源映射,初步解析Resource中的資源
createBaseActivityResources 打開資源映射
文件:/frameworks/base/core/java/android/app/ResourcesManager.java
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 {
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) {
// Force the creation of an ActivityResourcesStruct.
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 {
}
}
- 1.基于所有的資源目錄,顯示屏id,配置生成一個ResourcesKey。
- getOrCreateActivityResourcesStructLocked 生成ActivityResources并且保存在緩存到map。
- 根據ResourcesKey,生成一個資源的實際操作者ResourceImpl。
緩存ActivityResources到list中
private ActivityResources getOrCreateActivityResourcesStructLocked(
@NonNull IBinder activityToken) {
ActivityResources activityResources = mActivityResourceReferences.get(activityToken);
if (activityResources == null) {
activityResources = new ActivityResources();
mActivityResourceReferences.put(activityToken, activityResources);
}
return activityResources;
}
private static class ActivityResources {
public final Configuration overrideConfig = new Configuration();
public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>();
}
這個緩存對象本質上控制著所有在Apk中所有的Resources資源對象,有了緩存之后,之后讀取資源就不需要重新打開資源目錄這些耗時操作。本質上和View的實例化一節中聊過的一樣,為了減少反射的次數,會把已經反射過的View的構造函數保存下來,等待下次使用。
根據ResourcesKey,生成ResourceImpl
updateResourcesForActivity
先來看看updateResourcesForActivity方法,更新Resource的配置。
public void updateResourcesForActivity(@NonNull IBinder activityToken,
@Nullable Configuration overrideConfig, int displayId,
boolean movedToDifferentDisplay) {
try {
synchronized (this) {
final ActivityResources activityResources =
getOrCreateActivityResourcesStructLocked(activityToken);
...
// Rebase each Resources associated with this Activity.
final int refCount = activityResources.activityResources.size();
for (int i = 0; i < refCount; i++) {
WeakReference<Resources> weakResRef = activityResources.activityResources.get(
i);
Resources resources = weakResRef.get();
if (resources == null) {
continue;
}
// Extract the ResourcesKey that was last used to create the Resources for this
// activity.
final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl());
if (oldKey == null) {
continue;
}
// Build the new override configuration for this ResourcesKey.
final Configuration rebasedOverrideConfig = new Configuration();
if (overrideConfig != null) {
rebasedOverrideConfig.setTo(overrideConfig);
}
if (activityHasOverrideConfig && oldKey.hasOverrideConfiguration()) {
// Generate a delta between the old base Activity override configuration and
// the actual final override configuration that was used to figure out the
// real delta this Resources object wanted.
Configuration overrideOverrideConfig = Configuration.generateDelta(
oldConfig, oldKey.mOverrideConfiguration);
rebasedOverrideConfig.updateFrom(overrideOverrideConfig);
}
// Create the new ResourcesKey with the rebased override config.
final ResourcesKey newKey = new ResourcesKey(oldKey.mResDir,
oldKey.mSplitResDirs,
oldKey.mOverlayDirs, oldKey.mLibDirs, displayId,
rebasedOverrideConfig, oldKey.mCompatInfo);
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(newKey);
if (resourcesImpl == null) {
resourcesImpl = createResourcesImpl(newKey);
if (resourcesImpl != null) {
mResourceImpls.put(newKey, new WeakReference<>(resourcesImpl));
}
}
if (resourcesImpl != null && resourcesImpl != resources.getImpl()) {
// Set the ResourcesImpl, updating it for all users of this Resources
// object.
resources.setImpl(resourcesImpl);
}
}
}
} finally {
}
}
這個方法本質上是更新保存在activityResources的Resource實例。能看到每一次都會嘗試通過組合出來的ResourceKey來尋找之前是否存在ResourcesKey老的ResourceKey。
沒有則不會繼續下去,有則會根據原來的老ResourceKey重新生成新的ResourceKey,中間改變的是配置,接著根據新的ResourceKey尋找ResourceImpl,不存在則創建一個,并且把key和ResourceImpl設置mResourceImpls。
能看到中間有2個核心的方法:
- findResourcesImplForKeyLocked 查找對應ResourcesImpl
- createResourcesImpl 創建一個ResourcesImpl
先暫停在這里,我們先去看看getOrCreateResources,再回頭看看這兩個方法。
getOrCreateResources
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
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.
...
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
}
// 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) {
return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
// 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);
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;
}
}
這里分為兩種情況:
- 1.存在activityToken 是指開發應用層的應用
- 不存在activityToken 是指系統應用
當activityToken存在的時候
當activityToken存在的時候,這里是指應用啟動的時候要做的事情。
- 1.首先會嘗試去查找ResourcesImpl是否緩存起來,如下:
private ResourcesImpl findResourcesImplForKeyLocked(@NonNull ResourcesKey key) {
WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.get(key);
ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
if (impl != null && impl.getAssets().isUpToDate()) {
return impl;
}
return null;
}
能看到每一個ResourcesImpl將會保存到mResourceImpls這個ArrayMap中。
當ResourceImpl存在會調用getOrCreateResourcesLocked,當通過ResourcesImpl反過來查找Resource代理類,沒有找到,則會重新生成一個新的Resource,添加到mResourceReferences弱引用緩存中。
ResourcesImpl的創建
當ResourcesImpl不存在的時候,就需要創建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;
}
能看到在創建ResourcesImpl的同時會創建AssetManager。而這個AssetManager就是管理apk包中asset資源的管理者,我們只要需要訪問資源就一定和它打交道。
AssetManager的創建準備
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
final AssetManager.Builder builder = new AssetManager.Builder();
if (key.mResDir != null) {
try {
builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
}
if (key.mSplitResDirs != null) {
for (final String splitResDir : key.mSplitResDirs) {
try {
builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
...
return null;
}
}
}
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
try {
builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/,
true /*overlay*/));
} catch (IOException e) {
...
}
}
}
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.
try {
builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
....
}
}
}
}
return builder.build();
}
看到這里,我們稍微回憶一下我寫的插件化框架一文,其中有一段就是需要加載插件中的資源,在Android 9.0中需要用到一個核心方法addApkAssets;而在老版本中這里面的方法是assets.addAssetPath代替。為什么我們知道這樣加載資源,是因為資源正是使用這種方式把資源加載到AssetManager。
這里的步驟可以分為2個步驟:
- loadApkAssets 讀取資源目錄的資源生成ApkAsset對象
- addApkAssets把所有的對象都添加到AssetManager建造者中,最后生成AssetManager對象
那么這里就有三個核心方法,一個是通過建造模式創建AssetManager,一個addApkAssets,一個loadApkAssets讀取目錄下的資源接下來,接下來一次看看這些完成什么?
loadApkAssets 讀取目錄的資源,生成ApkAsset對象
private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
throws IOException {
final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
ApkAssets apkAssets = mLoadedApkAssets.get(newKey);
if (apkAssets != null) {
return apkAssets;
}
// Optimistically check if this ApkAssets exists somewhere else.
final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(newKey);
if (apkAssetsRef != null) {
apkAssets = apkAssetsRef.get();
if (apkAssets != null) {
mLoadedApkAssets.put(newKey, apkAssets);
return apkAssets;
} else {
// Clean up the reference.
mCachedApkAssets.remove(newKey);
}
}
if (overlay) {
apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path),
false /*system*/);
} else {
apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib);
}
mLoadedApkAssets.put(newKey, apkAssets);
mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
return apkAssets;
}
資源緩存思路
我們能看到所有的資源目錄路徑下都會生成一個ApkAssets對象,并且緩存起來,做了二級緩存。
- 第一級緩存:mLoadedApkAssets保存這所有已經加載的了ApkAssets的強引用。
- 第二級緩存:mCachedApkAssets保存這所有加載過的ApkAssets的弱引用。
首先先從mLoadedApkAssets查找是否已經存在已經加載的資源,找不到則嘗試著從mCachedApkAssets中查找,如果找到了,則從mCachedApkAssets中移除,并且添加到mLoadedApkAssets中。
實際上這種思路在Glide中有體現,我們可以把這種緩存看作內存緩存,把緩存拆分兩部分,活躍緩存以及非活躍緩存。活躍緩存持有強引用避免GC銷毀,而非活躍活躍緩存則持有弱引用,就算GC銷毀了也不會有什么問題。
當什么都找不到,只好從磁盤中讀取資源。
創建ApkAssets資源對象
ApkAssets可以通過兩種方式創建:
- ApkAssets.loadOverlayFromPath 當apk使用到了額外重疊的資源目錄對應的ApkAsset
- ApkAssets.loadFromPath 當apk使用一般的資源,比如的value資源,第三方資源庫等創建對應的ApkAsset。
文件:/frameworks/base/core/java/android/content/res/ApkAssets.java
public static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system)
throws IOException {
return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/);
}
public static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system,
boolean forceSharedLibrary) throws IOException {
return new ApkAssets(path, system, forceSharedLibrary, false /*overlay*/);
}
public static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system)
throws IOException {
return new ApkAssets(path, system, false /*forceSharedLib*/, false /*overlay*/);
}
private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
throws IOException {
mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
}
可以看到每一個靜態方法,最后都會通過構造函數的nativeLoad在native生成一個對應的地址指針,以及創建一個StringBlock。這里面究竟做了什么呢?讓我們先來看看nativeLoad。
ApkAssets的nativeLoad 創建native對象
文件:/frameworks/base/core/jni/android_content_res_ApkAssets.cpp
static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system,
jboolean force_shared_lib, jboolean overlay) {
ScopedUtfChars path(env, java_path);
...
std::unique_ptr<const ApkAssets> apk_assets;
if (overlay) {
apk_assets = ApkAssets::LoadOverlay(path.c_str(), system);
} else if (force_shared_lib) {
apk_assets = ApkAssets::LoadAsSharedLibrary(path.c_str(), system);
} else {
apk_assets = ApkAssets::Load(path.c_str(), system);
}
if (apk_assets == nullptr) {
...
return 0;
}
return reinterpret_cast<jlong>(apk_assets.release());
}
能看到,在這個native方法中一樣分成三種情況去讀取資源數據,生成ApkAssets native對象返回給java層。
- LoadOverlay 加載重疊資源
- LoadAsSharedLibrary 加載第三方庫資源
- Load 加載一般的資源
什么是重疊資源,引用羅生陽的解釋?
假設我們正在編譯的是Package-1,這時候我們可以設置另外一個Package-2,用來告訴aapt,如果Package-2定義有和Package-1一樣的資源,那么就用定義在Package-2的資源來替換掉定義在Package-1的資源。通過這種Overlay機制,我們就可以對資源進行定制,而又不失一般性。
舉一個例子,當我們下載某個主題并替換的時候,將會把整個Android相關的資源全部替換掉。此時會在overlay的文件夾中包含這個apk,這個apk只有資源,沒有dex,并且把相關能替換的id寫在某個文件。此時在初始化AssetManager會根據這個id替換掉所有的資源。和換膚框架相比,這是framework層面上的替換。
我們首先來看看加載一般資源的邏輯,Load。
ApkAssets::Load 讀取磁盤的資源
文件:/frameworks/base/libs/androidfw/ApkAssets.cpp
static const std::string kResourcesArsc("resources.arsc");
std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path, bool system) {
return LoadImpl({} /*fd*/, path, nullptr, nullptr, system, false /*load_as_shared_library*/);
}
std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl(
unique_fd fd, const std::string& path, std::unique_ptr<Asset> idmap_asset,
std::unique_ptr<const LoadedIdmap> loaded_idmap, bool system, bool load_as_shared_library) {
::ZipArchiveHandle unmanaged_handle;
int32_t result;
if (fd >= 0) {
result =
::OpenArchiveFd(fd.release(), path.c_str(), &unmanaged_handle, true /*assume_ownership*/);
} else {
result = ::OpenArchive(path.c_str(), &unmanaged_handle);
}
...
std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(unmanaged_handle, path));
// Find the resource table.
::ZipString entry_name(kResourcesArsc.c_str());
::ZipEntry entry;
result = ::FindEntry(loaded_apk->zip_handle_.get(), entry_name, &entry);
if (result != 0) {
...
loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty();
return std::move(loaded_apk);
}
if (entry.method == kCompressDeflated) {
...
}
loaded_apk->resources_asset_ = loaded_apk->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER);
if (loaded_apk->resources_asset_ == nullptr) {
...
return {};
}
loaded_apk->idmap_asset_ = std::move(idmap_asset);
const StringPiece data(
reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)),
loaded_apk->resources_asset_->getLength());
loaded_apk->loaded_arsc_ =
LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library);
if (loaded_apk->loaded_arsc_ == nullptr) {
...
return {};
}
return std::move(loaded_apk);
}
在LoadImpl中可以看見,資源的壓縮算法是zip算法,因此我們看到在這個核心方法中,大致上把資源讀取分為如下4個步驟:
- 1.OpenArchive 打開zip文件,并且生成ApkAssets對象
-2.通過FindEntry,尋找apk包中的resource.arsc文件 - 3.讀取apk包中的resource.arsc文件,讀取里面包含的id相關的map,以及資源asset文件夾中。
- 4.生成StringPiece對象,接著通過LoadedArsc::Load讀取其中的數據。
關于zip有一篇寫的比較好的文章:https://www.cnblogs.com/xumaojun/p/8544127.html
zip算法本質上是一種無損壓縮,通過短語式壓縮,編碼壓縮(哈夫曼編碼)進行壓縮。同時我們看到Android中使用的是libziparchive,而這個內置的系統庫不支持ZIP64,也就限制了包壓縮的大小(必須小于32位字節就是4G),這根本原因是系統限制了大小,而不是單單因為市場自己限制了apk大小。換句說,就算你強制生成一個過大apk包,android系統也會拒絕解析,核心檢測在這里:
if (file_length > static_cast<off64_t>(0xffffffff)) {
....
}
如果熟悉這一塊流程的哥們就一定知道resource.arsc是作為ResTable(資源表)的核心文件,等下我們在LoadedArsc::Load看看究竟做了什么。
現在我們只需要關心ApkAssets生成之后,Open方法讀取了什么東西,StringPiece又是指代什么。
ApkAssets.Open
std::unique_ptr<Asset> ApkAssets::Open(const std::string& path, Asset::AccessMode mode) const {
CHECK(zip_handle_ != nullptr);
::ZipString name(path.c_str());
::ZipEntry entry;
int32_t result = ::FindEntry(zip_handle_.get(), name, &entry);
if (result != 0) {
return {};
}
if (entry.method == kCompressDeflated) {
std::unique_ptr<FileMap> map = util::make_unique<FileMap>();
if (!map->create(path_.c_str(), ::GetFileDescriptor(zip_handle_.get()), entry.offset,
entry.compressed_length, true /*readOnly*/)) {
...
return {};
}
std::unique_ptr<Asset> asset =
Asset::createFromCompressedMap(std::move(map), entry.uncompressed_length, mode);
if (asset == nullptr) {
...
return {};
}
return asset;
} else {
std::unique_ptr<FileMap> map = util::make_unique<FileMap>();
if (!map->create(path_.c_str(), ::GetFileDescriptor(zip_handle_.get()), entry.offset,
entry.uncompressed_length, true /*readOnly*/)) {
....
return {};
}
std::unique_ptr<Asset> asset = Asset::createFromUncompressedMap(std::move(map), mode);
if (asset == nullptr) {
...
return {};
}
return asset;
}
}
open方法的意思是,判斷當前傳進來的Zip的Entry,判斷當前的entry是否是經過壓縮。
如果是經過壓縮的模塊,先通過FileMap把ZipEntry通過mmap映射到虛擬內存中(詳細可以看看Binder的mmap映射原理一文),接著通過Asset::createFromCompressedMap通過_CompressedAsset::openChunk拿到StreamingZipInflater,返回_CompressedAsset對象。
如果是沒有壓縮的模塊,通過FileMap把ZipEntry通過mmap映射到虛擬內存中,最后Asset::createFromUncompressedMap,獲取FileAsset對象.
在這里,resource.arsc并沒有在apk中沒有壓縮,因此走的下面,直接返回對應的FileAsset。
由此,可以得知,ApkAsset將會管理由ZipEntry映射出來的FileMap的Asset對象。
resource.arsc存儲內容
這個方法就是解析整個Android資源表的方法,只要了解這個方法,就能明白,Android是怎么找到id資源的。可能光看源碼很難有直觀的了解其中的數據結構,先來看看apk包中resource.arsc究竟有什么東西。我們借助AS的解析器看看內部:
從這個表中能看到左邊是資源的類型,右邊是資源id以及資源具體的路徑(或者具體的資源內容)。通常的,我們把resource.arsc中保存的資源映射表稱為ResTable(資源表)。當然如果是類似String,id后面對應將會是字符串內容:
當我們使用apk內部資源的時候,一般會使用如R.id.xxx的方式引入,本質上R.id就是對應在這個的int類型。在打包的時候,會把對應的id打包到resource.arsc中,在運行階段會解析這個文件,通過這個映射id,找到對應的路徑,才能正確的找到我們需要資源。
之前在插件化基礎框架一文中,曾經粗略的聊過每一個資源id的組成結構,這里就詳細聊聊。
一旦提到resource.arsc文件中的數據結構,就一定會提到下面這幅圖
Android資源打包過程
在聊resource.arsc之前,我先聊聊Android中這個目錄下的資源打包工具/frameworks/base/tools/aapt/
aapt是我們開發中中經常打交道,但是從來沒有注意過的工具。這個工具主要為我們apk收集打包資源文件,并且生成resource.arsc文件。在這個過程中,打包所有的xml資源文件的時候,會從文本格式轉化為二進制格式。
這么做原因有兩個:
- 1.二進制的xml文件占用的空間更加小,所有的字符串都會被收集到字符串字典中(也叫字符串資源池),對所有的字符串進行去重,重復的字符串都有個索引,本質上和zip的壓縮很相似。
- 2.二進制的讀取解析速度比文本速度快,因為字符串去重,需要讀取的數據就小很多。
在整個apk包中擁有這如下幾種資源:
- 1.二進制xml文件
- 2.resource.arsc文件
- 3.沒有經過壓縮的asset文件以及so庫
那么整個apk資源打包必然包含這幾個過程。大致上可以分為如下三個大步驟:
- 1.收集資源
- 2.收集Xml資源,壓平Xml文件,轉化為二進制Xml
- 3.收集資源生成resource.arsc文件
整個打包大致上分為如下幾個步驟:
收集資源:
- 1.解析AndroidManifest.xml,根據package標簽創建ResourcesTable
- 2.添加被引用資源包。如系統的layout_width,如應用自己定義的資源,這些引用的資源包都會被添加進來。
- 3.收集資源文件
- 4.將收集到的資源文件添加到資源表
- 5.編譯value類資源,在這個時候會為每一個資源的type添加一個資源的entry,每一個entry會根據配置生成不同的config。就如上圖String資源,每一項字符串都稱為entry,而字符串根據不同的語言映射著不同的真正字符串,這些稱為config(配置)
- 6.給Bag資源分配ID。類型為values的資源除了是string之外,還有其它很多類型的資源,其中有一些比較特殊,如bag、style、plurals和array類的資源。這些資源會給自己定義一些專用的值,這些帶有專用值的資源就統稱為Bag資源
收集Xml資源
- 7.編譯Xml資源文件: 解析Xml文件,生成XMLNode
-
8.編譯Xml資源文件:賦予屬性名稱資源ID,每一個Xml文件都是從根節點開始給屬性名稱賦予資源ID,然后再給遞歸給每一個子節點的屬性名稱賦予資源ID,直到每一個節點的屬性名稱都獲得了資源ID為止。如下
image.png - 9.編譯Xml資源文件:解析屬性值; 上一步是對Xml元素的屬性的名稱進行解析,這一步是對Xml元素的屬性的值進行解析。通過上一步的資源id來查找bag中的對應的字符串,這就作為解析的結果。("@+id/XXX"+符號的意思是如果沒有對應的資源id就創建一個)
壓平Xml資源
準備好Xml解析的資源就開始壓平Xml文件,把文本的文件轉化為二進制文件.
10.收集具有資源id屬性名稱和字符串; 這一步除了收集那些具有資源ID的Xml元素屬性的名稱字符串之外,還會將對應的資源ID收集起來放在一個數組中。這里收集到的屬性名稱字符串保存在一個字符串資源池中,它們與收集到的資源ID數組是一一對應的。
11.收集其它字符串,如控件名稱,命名空間等等
12.寫入Xml文件頭。包含了代表頭部的type(RES_XML_TYPE),頭部大小,整個xml文件大小.最終編譯出來的Xml二進制文件是一系列的chunk組成的,每一個chunk都有一個頭部,用來描述chunk的元信息。同時,整個Xml二進制文件又可以看成一塊總的chunk,它有一個類型為ResXMLTree_header的頭部。
13.寫入字符串資源池,此時把10步驟和11步驟的字符串嚴格按照順序寫入字符串池子。此時寫入頭部大小以及type為RES_STRING_POOL_TYPE
14.寫入資源ID,在第10步驟中收集到的ID,將會按照順序作為一個單獨的chunk寫入到xml文件中,這個chunk位于字符串池子后面。
15.壓平Xml文件,把所有的字符串替換成字符串池子中的索引
生成resource.arsc資源表
從第一大步驟中,收集了大量的關于資源的數據,并且保存在資源表中(此時在內存),此時需要真正的生成一個文件。
- 16.收集類型字符串 如layout,id等
- 17.收集資源項名稱字符串 獲取類型字符串中每一項名稱
- 18.收集資源項值字符串 獲取每一項資源中具體的值。
- 14.寫入Package資源項元信息數據塊頭部,寫入type RES_TABLE_PACKAGE_TYPE
- 15.寫入類型字符串資源池,指代的是(layout,menu,strings等xml文件名稱)
- 16.寫入資源項名稱字符串資源池,指代的是每個資源類型中的數據項名稱(如layout中有一個main.xml的文件名)
- 17.寫入類型規范數據塊,type為RES_TABLE_TYPE_SPEC_TYPE;類型規范指代就是這些(文件夾layout,menu中各種數據)。
- 寫入類型資源項數據塊,type為RES_TABLE_TYPE_TYPE,用來描述一個類型資源項頭部。每一個資源項數據塊都會指向一個資源entry,里面有著當前當前資源項在各種情況的真實數據,如mipmap,drawable在不同分辨率文件夾下具體文件路徑。
- 寫入資源索引表頭部,type為RES_TABLE_TYPE,此時size就是指resource.arsc大小
- 20.寫入資源項的值字符串資源池
- 21.寫入Package數據塊
到這里就完成了resource.arsc文件的生成。
最后還需要幾個額外的步驟,完善apk還沒有打包的資源。
- 1.AndroidMainfest.xml轉化二進制文件
- 2.生成R.java文件
- 3.把assets目錄,resources.arsc,二進制Xml文件打包到apk。
至此,這就是Android打包的大致流程。
resource.arsc文件數據結構剖析
根據上圖以及上一節的打包流程,來分析resource.arsc文件。
在整個表的頂部保存著RES_TABLE_TYPE的標示位來標示著整個資源映射表是從哪里開始解析。后面接著這個頭部的大小,整個文件的大小,保存著多少package的資源。
在整個資源映射表中,第一個chunk是字符串池子。type是RES_STRING_POOL_TYPE。在整個生成文件過程所有的資源值字符串都會經過收集,放到這個池子中,變成索引。
最后這一大塊,就是生成resource.arsc文件最后寫入的Package數據塊。
packge數據大致分為如下幾大塊:
- 1.Package的頭部,type為RES_TABLE_PACKAGE_TYPE。
- 2.Package的類型規范名稱字符串,資源類型值名稱字符串資源池
- 3.Package的RES_TABLE_TYPE_SPEC_TYPE類型規范的頭部
- 4.Package RES_TABLE_TYPE_TYPE 類型資源項的頭部,里面有指向entry的指針。
大致上了解整個resource.arsc文件后,看看LoadedArsc::Load是如何解析的。
LoadedArsc::Load
std::unique_ptr<const LoadedArsc> LoadedArsc::Load(const StringPiece& data,
const LoadedIdmap* loaded_idmap, bool system,
bool load_as_shared_library) {
std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc());
loaded_arsc->system_ = system;
ChunkIterator iter(data.data(), data.size());
while (iter.HasNext()) {
const Chunk chunk = iter.Next();
switch (chunk.type()) {
case RES_TABLE_TYPE:
if (!loaded_arsc->LoadTable(chunk, loaded_idmap, load_as_shared_library)) {
return {};
}
break;
default:
...
break;
}
}
...
}
進來第一件事情就是把所有zip的chunk解析出來后,迭代尋找resource.arsc文件的標志頭RES_TABLE_TYPE。找到之后,開始讀取這個數據,尋找的是上面結構的如下結構:
LoadedArsc::LoadTable
bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap,
bool load_as_shared_library) {
const ResTable_header* header = chunk.header<ResTable_header>();
...
const size_t package_count = dtohl(header->packageCount);
size_t packages_seen = 0;
packages_.reserve(package_count);
ChunkIterator iter(chunk.data_ptr(), chunk.data_size());
while (iter.HasNext()) {
const Chunk child_chunk = iter.Next();
switch (child_chunk.type()) {
case RES_STRING_POOL_TYPE:
if (global_string_pool_.getError() == NO_INIT) {
status_t err = global_string_pool_.setTo(child_chunk.header<ResStringPool_header>(),
child_chunk.size());
} else {
...
}
break;
case RES_TABLE_PACKAGE_TYPE: {
if (packages_seen + 1 > package_count) {
....
return false;
}
packages_seen++;
std::unique_ptr<const LoadedPackage> loaded_package =
LoadedPackage::Load(child_chunk, loaded_idmap, system_, load_as_shared_library);
if (!loaded_package) {
return false;
}
packages_.push_back(std::move(loaded_package));
} break;
default:
...
break;
}
}
...
}
在LoadPackage方法中,分別加載兩個大區域的數據:
-
1.RES_STRING_POOL_TYPE 象征著資源中所有字符串,style的資源池(不包括資源類型名稱,以及資源數據項名稱)。解析的是如下部分:
image.png
比如:string.xml,某個R.string.xxx 中的值,比如drawable文件夾中,某個文件的具體路徑
-
2.RES_TABLE_PACKAGE_TYPE 象征著整個Package數據塊,解析的是如下這部分:
image.png
ResStringPool的解析過程
我們先來看看加載到內存中的Xml字符串資源池的結構體:
struct ResStringPool_header
{
struct ResChunk_header header;
// Number of strings in this pool (number of uint32_t indices that follow
// in the data).
uint32_t stringCount;
// Number of style span arrays in the pool (number of uint32_t indices
// follow the string indices).
uint32_t styleCount;
// Flags.
enum {
// If set, the string index is sorted by the string values (based
// on strcmp16()).
SORTED_FLAG = 1<<0,
// String pool is encoded in UTF-8
UTF8_FLAG = 1<<8
};
uint32_t flags;
// Index from header of the string data.
uint32_t stringsStart;
// Index from header of the style data.
uint32_t stylesStart;
};
這個數據結構實際上是字符串資源池的這一部分:
我們可以從該頭部解析到整個資源池的大小,字符串個數,style個數,標記,以及字符串池子起始位置偏移量和style池子的起始位置偏移量。
計算原理實際上就很簡單:
字符串池子的起點位置 = header地址+stringsStart
style 資源池的起點位置 = header地址+stylesStart
值得注意的是字符串/style的個數并是指寫入字符串/style的條數。為在setTo方法中會通過偏移量去計算整個資源StringPool/StylePool占用多少char。
我們還有一處值得注意的是,在整個字符串資源池中,還有兩個比較重要的Entrys還沒聊,這兩個entry(偏移數組)的位置在圖中如下,在header的后方:
這兩個偏移數組做的事情比較重要,當我們嘗試著通過index去查找String的內容,就要訪問這個偏移數組,來找到對應字符串的在整個池子中的位置。計算方法如下:
字符串偏移數組起點 = header + header.size
style偏移數組起點位置 = 字符串偏移數組起點 + 字符串大小
因為資源的寫入是嚴格按照順序寫入的,那么通過index互相查找資源成為了可能,我們來看看string8At查找字符串的方法看看:
文件:/frameworks/base/libs/androidfw/ResourceTypes.cpp
const char* ResStringPool::string8At(size_t idx, size_t* outLen) const
{
if (mError == NO_ERROR && idx < mHeader->stringCount) {
if ((mHeader->flags&ResStringPool_header::UTF8_FLAG) == 0) {
return NULL;
}
const uint32_t off = mEntries[idx]/sizeof(char);
if (off < (mStringPoolSize-1)) {
const uint8_t* strings = (uint8_t*)mStrings;
const uint8_t* str = strings+off;
decodeLength(&str);
const size_t encLen = decodeLength(&str);
*outLen = encLen;
if ((uint32_t)(str+encLen-strings) < mStringPoolSize) {
return stringDecodeAt(idx, str, encLen, outLen);
} else {
...
}
} else {
...
}
}
return NULL;
}
在Android底層有一層緩存mCache,里面存放著已經解析過的長度為uint_16較長的資源字符串。
解析String的算法如下:
首先通過index,找到對應Entry中的數組中對應的元素off. uint32_t off = Entries[index]
當偏移元素 off 最高兩位沒有設置,說明這就是當前字符串的距離資源池起點的偏移量,如果設置了最高兩位,則清除掉當前的最高位置,把當前的len和下個字符的加到一起。這樣就靈活合并了字符串。
對應字符串string 起點地址(單位uint8_t) = mString(字符串資源池起點地址) + off
最后調用下面這個方法解析資源池中的字符串:
const char* ResStringPool::stringDecodeAt(size_t idx, const uint8_t* str,
const size_t encLen, size_t* outLen) const {
const uint8_t* strings = (uint8_t*)mStrings;
size_t i = 0, end = encLen;
while ((uint32_t)(str+end-strings) < mStringPoolSize) {
if (str[end] == 0x00) {
if (i != 0) {
...
}
*outLen = end;
return (const char*)str;
}
end = (++i << (sizeof(uint8_t) * 8 * 2 - 1)) | encLen;
}
// Reject malformed (non null-terminated) strings
...
return NULL;
}
能看到這里面這里面的算法如下:
在String資源池的大小限制下,unit_8長度下,不斷的寫入字符串,并且整個數據不斷向左移動15位,遇到了0x00就停止解析,并且把結果設置到outLen中。一般的outLen是指向encLen的指針,encLen是內容,而encLen是通過解析str的內容來的,因此這個方法本質上就是寫入到str中。
確實很繞,不過沒有這么難懂。
最后再把這個資源池,設置到全局資源global_string_pool_中,方便后面的查找。
Package數據塊解析,LoadedPackage::Load
std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk,
const LoadedIdmap* loaded_idmap,
bool system, bool load_as_shared_library) {
ATRACE_NAME("LoadedPackage::Load");
std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage());
// typeIdOffset was added at some point, but we still must recognize apps built before this
// was added.
constexpr size_t kMinPackageSize =
sizeof(ResTable_package) - sizeof(ResTable_package::typeIdOffset);
const ResTable_package* header = chunk.header<ResTable_package, kMinPackageSize>();
if (header == nullptr) {
...
return {};
}
loaded_package->system_ = system;
loaded_package->package_id_ = dtohl(header->id);
if (loaded_package->package_id_ == 0 ||
(loaded_package->package_id_ == kAppPackageId && load_as_shared_library)) {
// Package ID of 0 means this is a shared library.
loaded_package->dynamic_ = true;
}
if (loaded_idmap != nullptr) {
...
loaded_package->package_id_ = loaded_idmap->TargetPackageId();
loaded_package->overlay_ = true;
}
if (header->header.headerSize >= sizeof(ResTable_package)) {
uint32_t type_id_offset = dtohl(header->typeIdOffset);
if (type_id_offset > std::numeric_limits<uint8_t>::max()) {
...
return {};
}
loaded_package->type_id_offset_ = static_cast<int>(type_id_offset);
}
util::ReadUtf16StringFromDevice(header->name, arraysize(header->name),
&loaded_package->package_name_);
std::unordered_map<int, std::unique_ptr<TypeSpecPtrBuilder>> type_builder_map;
ChunkIterator iter(chunk.data_ptr(), chunk.data_size());
while (iter.HasNext()) {
const Chunk child_chunk = iter.Next();
switch (child_chunk.type()) {
case RES_STRING_POOL_TYPE: {
break;
case RES_TABLE_TYPE_SPEC_TYPE: {
...
break;
case RES_TABLE_TYPE_TYPE:
...
break;
case RES_TABLE_LIBRARY_TYPE:
...
break;
default:
...
break;
}
}
...
// Flatten and construct the TypeSpecs.
for (auto& entry : type_builder_map) {
uint8_t type_idx = static_cast<uint8_t>(entry.first);
TypeSpecPtr type_spec_ptr = entry.second->Build();
...
// We only add the type to the package if there is no IDMAP, or if the type is
// overlaying something.
if (loaded_idmap == nullptr || type_spec_ptr->idmap_entries != nullptr) {
// If this is an overlay, insert it at the target type ID.
if (type_spec_ptr->idmap_entries != nullptr) {
type_idx = dtohs(type_spec_ptr->idmap_entries->target_type_id) - 1;
}
loaded_package->type_specs_.editItemAt(type_idx) = std::move(type_spec_ptr);
}
}
return std::move(loaded_package);
}
根據type,我們就能區分如下幾種類型:
-
1.RES_TABLE_PACKAGE_TYPE 解析頭部,解析如下部分的數據:
image.png -
2.RES_STRING_POOL_TYPE 從資源類型字符串池子和資源項名稱字符串池子解析所有資源類型名稱,資源數據項名稱中的字符串
image.png -
3.RES_TABLE_TYPE_SPEC_TYPE 解析所有的資源類型規范
image.png -
4.RES_TABLE_TYPE_TYPE 解析所有的資源類型
image.png - 5.RES_TABLE_LIBRARY_TYPE 解析所有的第三方庫資源,這里的圖片沒有顯示。
小結
限于文章的長度,本文剖析到這里,下一篇將會剖析資源類型規范,資源數據項,AssetManager的核心原理。在這里面,本文講述了如下內容:
Resource 是由ResourcesImpl控制的。ApkAssets是每個資源文件夾在內存中的對象。AssetManager伴隨著ResourcesImpl初始化而存在,其目的是為了更好的管理每一個ApkAssets。
在整個Android 資源體系的Java層中有四重緩存:
- 1.activityResources 一個面向Resources弱引用的ArrayList
- 2.以ResourcesKey為key,ResourcesImpl的弱引用為value的Map緩存。
- 3.ApkAssets在內存中也有一層緩存,緩存拆成兩部分,mLoadedApkAssets已經加載的活躍ApkAssets,mCacheApkAssets已經加載了但是不活躍的ApkAssets
- 4.native加載磁盤資源(加載磁盤資源過程中還有一些緩存)
對于Android系統來說,resources.arsc文件尤為重要,它充當了Android系統解析資源的向導,沒有了它,Android中的應用無法正常解析數據。
該文件大致分為如下幾個部分,注意一下雖然存在多個字符串資源池但是存放的數據不一樣:
- 1.resources.arsc頭部信息,type為RES_TABLE_TYPE
- 2.解析資源中所有的字符串,style字符串,type為RES_STRING_POOL_TYPE
- 3.剩下全部為Package數據塊,type為RES_TABLE_PACKAGE_TYPE
- RES_TABLE_PACKAGE_TYPE 代表這Package數據塊的頭部
- 5.在Package數據塊中同樣存在著資源池,不過這個資源池存放的是資源類型規范字符串以及資源數據字符串。type為RES_STRING_POOL_TYPE
- RES_TABLE_TYPE_SPEC_TYPE 代表這所有資源類型規范數據塊(chunk)
- 7.RES_TABLE_TYPE_TYPE 代表著所有資源數據數據塊
- 8.RES_TABLE_LIBRARY_TYPE代表所有的第三方資源庫。
三個不同的字符串資源池,就以layout文件夾為例子:
- 下標1最左側指代的是資源類型名稱,也就是位于package數據塊中,typeString偏移數組以及類型字符串資源池的數據,RES_TABLE_TYPE_SPEC_TYPE 也是從這里找到正確的名稱
- 下標2 指代的是的是資源數據項名稱,也就是位于package數據塊中,String偏移數組以及資源數據項字符串資源池,RES_TABLE_TYPE_TYPE 也是從這里找到正確的名稱。
- 下標3,指代的是資源字符串,位于package數據塊之外,最大的字符串資源池。
通過這幾個資源池,加上資源數據項中指向的config數據項中的數據,就能正確的從resource.arsc文件中復原資源出來。
Android為了加速資源的加載速度,并不是直接通過File讀寫操作讀取資源信息。而是通過FileMap的方式,也就是mmap把文件地址映射到虛擬內存中,時刻準備讀寫。這么做的好處,就是mmap回返回文件的地址,可以對文件進行操作,節省系統調用的開銷,壞處就是mmap會映射到虛擬內存中,是的虛擬內存增大。更加詳細的討論,在Binder的mmap映射原理一文中。