1. 概述
大部分控件我們都會使用,但是我們未必知道其資源加載的原理,目前換膚的架構比較多我們可以随随便便拿過來用,但早在幾年前這些資料是比較少的,如果想做一個換膚的架構那就隻能自己一點一點啃源碼。
如果說我們現在不去用第三方的開源架構,要做一個換膚的功能,擺在我們面前的其實隻有一個問題需要解決,那就是如何讀取另外一個皮膚apk中的資源。
所有分享大綱:2017Android進階之路與你同行
視訊講解位址:http://pan.baidu.com/s/1bC3lAQ

2. 資源加載源碼分析
2.1 我們先來看一下ImageView的scr屬性到底是怎麼加載圖檔資源的:
<ImageView
android:layout_width="wrap_content"
android:src="@drawable/app_icon"
android:layout_height="wrap_content" />
// ImageView.java 解析屬性
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
// 通過TypedArray擷取圖檔
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
// TypedArray.getDrawable() 方法
public Drawable getDrawable(@StyleableRes int index) {
// 省略部分代碼....
// 加載資源其實是通過mResources去擷取的
return mResources.loadDrawable(value, value.resourceId, mTheme);
}
2.2 Resource建立過程分析:
我們在Activity中也經常這樣使用context.getResources().getColor(R.id.title_color),那麼這個Resources執行個體是怎麼建立的呢?我們可以先從context的實作類ContextImpl入手
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);
if (resources != null) {
// 不會走此分支,因為6.0中還不支援多屏顯示,雖然已經有不少相關代碼了,7.0以及正式支援多屏操作了
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
......
}
}
......
mResources = resources;
}
// packageInfo.getResources 方法
public Resources getResources(ActivityThread mainThread) {
// 緩存機制,如果LoadedApk中的mResources已經初始化則直接傳回,
// 否則通過ActivityThread建立resources對象
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
}
return mResources;
}
最終會來到ResourcesManager的getResources方法
public @NonNull Resources getResources(@Nullable IBinder activityToken,
@Nullable String resDir, //app資源檔案夾路徑,實際上是apk檔案的路徑,如/data/app/包名/base.apk
@Nullable String[] splitResDirs, //針對一個app由多個apk組成(将原本一個apk切片為若幹apk)時,每個子apk中的資源檔案夾
@Nullable String[] overlayDirs,
@Nullable String[] libDirs, // app依賴的共享jar/apk路徑
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
try {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
// 以apk路徑為參數建立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) {
// .......
if (activityToken != null) {
// 根據key從緩存裡面找找 ResourcesImpl
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
// 如果 resourcesImpl 有 那麼根據resourcesImpl 和classLoader 從緩存找找 Resource
return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl);
}
// We will create the ResourcesImpl object outside of holding this lock.
} else {
// .......
}
}
// If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
// 這個比較重要 createResourcesImpl 通過 key
ResourcesImpl resourcesImpl = createResourcesImpl(key);
synchronized (this) {
// .......
final Resources resources;
if (activityToken != null) {
// 根據resourcesImpl和classLoader擷取Resources
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
}
return resources;
}
}
private @NonNull ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
// 建立AssetManager
final AssetManager assets = createAssetManager(key);
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
// 根據AssetManager 建立一個ResourcesImpl 其實找資源是: Resources -> ResourcesImpl -> AssetManager
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對象
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.
// 将app中的資源路徑都加入到AssetManager對象中
if (key.mResDir != null) {
// 這個方法很重要,待會我們就是用它去加載皮膚的apk
if (assets.addAssetPath(key.mResDir) == ) {
throw new Resources.NotFoundException("failed to add asset path " + key.mResDir);
}
}
if (key.mLibDirs != null) {
for (final String libDir : key.mLibDirs) {
// 僅僅選擇共享依賴中的apk,因為jar中不會有資源檔案
if (libDir.endsWith(".apk")) {
// Avoid opening files we know do not have resources,
// like code-only .jar files.
if (assets.addAssetPathAsSharedLibrary(libDir) == ) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}
return assets;
}
/**
* Gets an existing Resources object if the class loader and ResourcesImpl are the same,
* otherwise creates a new Resources object.
*/
private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
@NonNull ResourcesImpl impl) {
// Find an existing Resources that has this ResourcesImpl set.
final int refCount = mResourceReferences.size();
for (int i = ; i < refCount; i++) {
WeakReference<Resources> weakResourceRef = mResourceReferences.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;
}
}
// Create a new Resources reference and use the existing ResourcesImpl object.
// 建立一個Resources ,Resource有好幾個構造方法,每個版本之間有稍微的差别
// 有的版本是用的這一個構造方法 Resources(assets, dm, config, compatInfo)
Resources resources = new Resources(classLoader);
resources.setImpl(impl);
// 加入緩存
mResourceReferences.add(new WeakReference<>(resources));
if (DEBUG) {
Slog.d(TAG, "- creating new ref=" + resources);
Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
}
return resources;
}
【看了這麼多我們大緻可以總結一下Resources的建立流程了:】
- packageInfo.getResources(mainThread) -> mainThread.getTopLevelResources() -> mResourcesManager.getResources() -> getOrCreateResources() 這裡首先會找ResourcesImpl緩存如果有則會擷取Resource緩存傳回;
- 如果沒有ResourcesImpl緩存,那麼回去建立ResourcesImpl,ResourcesImpl的建立依賴于AssetManager ;
- AssetManager的建立是通過直接執行個體化對象調用了一個addAssetPath(path)方法把應用的apk路徑添加到AssetManager,addAssetPath()方法請看源碼解釋。
- 建立好ResourcesImpl之後會再去緩存中找Resource如果沒有,那麼則會建立Resource并将其緩存,建立我們看到的源碼是new Resources(classLoader),resources.setImpl(impl) 而不同的版本可能是 new Resources(assets, dm, config, compatInfo) 具體請看6.0源碼。
3. 加載皮膚資源
如果大緻知道了資源的加載流程以及Resource的建立過程,現在我們要去加載另外一個apk中的資源就好辦了,隻需要自己建立一個Resource對象,下面這段代碼網上找一大堆,如果分析過源碼相信你會有更深的認識:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
Resources superRes = getResources();
// 建立AssetManager,但是不能直接new是以隻能通過反射
AssetManager assetManager = AssetManager.class.newInstance();
// 反射擷取addAssetPath方法
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
// 皮膚包的路徑: 本地sdcard/plugin.skin
String skinPath = Environment.getExternalStorageDirectory().getAbsoluteFile()+ File.separator+"plugin.skin";
// 反射調用addAssetPath方法
addAssetPathMethod.invoke(assetManager, skinPath);
// 建立皮膚的Resources對象
Resources skinResources = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
// 通過資源名稱,類型,包名擷取Id
int bgId = skinResources.getIdentifier("main_bg","drawable","com.hc.skin");
Drawable bgDrawable = skinResources.getDrawable(bgId);
// 設定背景
findViewById(R.id.activity_main).setBackgroundDrawable(bgDrawable);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4. AssetManager建立過程分析
下面的分析希望不要有強迫症,看不懂其實也不打緊因為涉及到JNI。通過前面的分析可知,Android系統中實際對資源的管理是AssetManager類.每個Resources對象都會關聯一個AssetManager對象,Resources将對資源的操作大多數委托給了AssetManager。當然有些源碼還有一層 ResourcesImpl 剛剛我們也看到了。
另外還會存在一個native層的AssetManager對象與java層的這個AssetManager對象相對應,而這個native層AssetManager對象在記憶體的位址存儲在java層的AssetManager.mObject中。是以在java層AssetManager的jni方法中可以快速找到它對應的native層的AssetManager對象。
4.1 AssetManager的init()
/**
* 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 = ;
incRefsLocked(this.hashCode());
}
init(false);
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
ensureSystemAssets();
}
}
// ndk的源碼路徑
// frameworks/base/core/jni/android_util_AssetManager.cpp
// frameworks/base/libs/androidfw/AssetManager.cpp
private native final void init(boolean isSystem);
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);
// framework/framework-res.apk
// 初始化的時候會去加載系統的framework-res.apk資源
// 也就是說我們為什麼能加載系統的資源如顔色、圖檔、文字等等
path.appendPath(kSystemAssets);
return addAssetPath(path, NULL);
}
4.2 AssetManager的addAssetPath(String path)方法
bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
{
asset_path ap;
// 省略一些校驗代碼
// 判斷是否已經加載過了
for (size_t i=; i<mAssetPaths.size(); i++) {
if (mAssetPaths[i].path == ap.path) {
if (cookie) {
*cookie = static_cast<int32_t>(i+);
}
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 (cookie) {
*cookie = static_cast<int32_t>(mAssetPaths.size());
}
if (mResources != NULL) {
appendPathToResTable(ap);
}
return true;
}
bool AssetManager::appendPathToResTable(const asset_path& ap) const {
// skip those ap's that correspond to system overlays
if (ap.isSystemOverlay) {
return true;
}
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());
// 資源包路徑不是一個檔案夾,那就是一個apk檔案了
if (ap.type != kFileTypeDirectory) {
// 對于app來說,第一次執行時,肯定為0,因為mResources剛建立,還沒對其操作
// 下面的分支 指揮在參數是系統資源包路徑時,才執行,
// 而且系統資源包路徑是首次被解析的
// 第二次執行appendPathToResTable,nextEntryIdx就不會為0了
if (nextEntryIdx == ) {
// 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);
// 對于app自己的資源包來說,一般都會都下面的邏輯
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 == && 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());
// 建立ResTable對象,并把前面與resources.arsc關聯的Asset對象,加入到這個ResTabl中
sharedRes = new ResTable();
sharedRes->add(ass, idmap, nextEntryIdx + , 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 + , !shared);
}
onlyEmptyResources = false;
if (!shared) {
delete ass;
}
} else {
mResources->addEmpty(nextEntryIdx + );
}
if (idmap != NULL) {
delete idmap;
}
MY_TRACE_END();
return onlyEmptyResources;
}
大家應該之前了解過這個檔案resources.arsc, 如果沒了解過可以在網上找篇文章看一下。apk在打包的時候會生成它,我們解壓apk就應該能夠看到他。這裡面基本都是存放的資源的索引,之是以不同的分辨率可以加載不同的圖檔它可是個大功臣。
5. 資源的查找過程
現在我們回到最開始的loadDrawable()方法,drawable資源是有實際資源檔案的。這類資源索引的過程大體上分為兩個步驟,解析資源ID代表的資源的路徑;裝載資源檔案并緩存。
drawable是緩存到Resources.mDrawableCache中。加載drawable的時候,要先檢查下這個緩存中是否有,有的話,直接傳回,就不需要加載了。沒有緩存的話,說明還沒加載該資源檔案,是以要先加載加載之後在緩存到mDrawableCache中。而loadDrawable()方法中又是通過loadDrawableForCookie()來加載drawable的:
private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) {
// drawable資源項的值是一個字元串,代表檔案的路徑
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();
.
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(this, rp, theme);
rp.close();
} else {
// 如果drawable是圖檔檔案的話,打開它
// assetCookie-1就是圖檔所在的資源包路徑在native層AssetManager.mAssetPaths數組中的索引
// 下面這個方法就是打開這個檔案了
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
dr = Drawable.createFromResourceStream(this, 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;
}
到這裡為止就徹底搞清楚資源的查找與加載過程了:索引+加載+緩存。
所有分享大綱:2017Android進階之路與你同行
視訊講解位址:http://pan.baidu.com/s/1bC3lAQ