資源的查找過程
在android中查找資源分為以下兩種方式:
- ContextImpl#getResource()#getxxx(R.xx.yy)
- AssetManager#open()
我們以android.content.res.Resources#getLayout為例
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
return impl.loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
} finally {
releaseTempTypedValue(value);
}
}
void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
if (found) {
return;
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}
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;
}
}
再看下AssetManager#open方法的調(diào)用鏈
public @NonNull InputStream open(@NonNull String fileName, int accessMode) throws IOException {
Preconditions.checkNotNull(fileName, "fileName");
synchronized (this) {
ensureOpenLocked();
final long asset = nativeOpenAsset(mObject, fileName, accessMode);
if (asset == 0) {
throw new FileNotFoundException("Asset file: " + fileName);
}
final AssetInputStream assetInputStream = new AssetInputStream(asset);
incRefsLocked(assetInputStream.hashCode());
return assetInputStream;
}
}
結(jié)論:
- 通過id獲取資源先后要經(jīng)過ContextImpl->Resource->ResourceImpl->AssetManager將id傳到native方法中囤耳,拿這個id通過arsc映射找到對應(yīng)的資源信息决摧,保存在TypedValue對象中返回种呐。
- 通過AssetManager獲取資源則是通過AssetManager的native方法直接去找assets目錄下對應(yīng)文件盗温。
Resource與AssetManager的生成時機
通過上述分析魄幕,我們知道了所有的資源最終都要通過AssetManager到對應(yīng)apk路徑下去訪問负间,那么 apk路徑是如何添加到AssetManager中的偶妖? 我們不妨正向分析一波,找到Resource與AssetManager的生成時機政溃。
ContextImpl.java
void setResources(Resources r) {
if (r instanceof CompatResources) {
((CompatResources) r).setContext(this);
}
mResources = r;
}
找到調(diào)用setResources方法的地方趾访,如
private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
final String[] splitResDirs;
final ClassLoader classLoader;
try {
splitResDirs = pi.getSplitPaths(splitName);
classLoader = pi.getSplitClassLoader(splitName);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
return ResourcesManager.getInstance().getResources(activityToken,
pi.getResDir(),
splitResDirs,
pi.getOverlayDirs(),
pi.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfig,
compatInfo,
classLoader);
}
我們發(fā)現(xiàn)Resource對象最后都是通過ResourcesManager.getInstance().getResources方法生成的。
ResourcesManager.java
public @Nullable 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");
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);
}
}
ResourcesManager.java
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
if (DEBUG) {
Throwable here = new Throwable();
here.fillInStackTrace();
Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
}
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.
if (key.hasOverrideConfiguration()
&& !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
final Configuration temp = new Configuration(activityResources.overrideConfig);
temp.updateFrom(key.mOverrideConfiguration);
key.mOverrideConfiguration.setTo(temp);
}
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
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) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
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;
}
synchronized (this) {
ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
if (existingResourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- got beat! existing impl=" + existingResourcesImpl
+ " new impl=" + resourcesImpl);
}
resourcesImpl.getAssets().close();
resourcesImpl = existingResourcesImpl;
} else {
// 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對象賦值的地方董虱,findResourcesImplForKeyLocked(key)看名字像是一個取緩存的方法扼鞋,最后我們發(fā)現(xiàn)ResourcesImpl對象是通過createResourcesImpl方法生成的。
ResourcesManager.java
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
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對象的地方
final AssetManager assets = createAssetManager(key);
ResourcesManager.java
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
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.
if (key.mResDir != null) {
if (assets.addAssetPath(key.mResDir) == 0) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
}
if (key.mSplitResDirs != null) {
for (final String splitResDir : key.mSplitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
Log.e(TAG, "failed to add split asset path " + splitResDir);
return null;
}
}
}
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
assets.addOverlayPath(idmapPath);
}
}
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.
if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}
return assets;
}
至此愤诱,我們發(fā)現(xiàn)了apk路徑是通過assets.addAssetPath(key.mResDir)調(diào)用添加進來的云头。
Resource與AssetManager對象是否全局唯一以及與LoadedApk的聯(lián)系
根據(jù)上述分析,我們知道了每個Resource對象中包含一個唯一的AssetManager對象淫半,因此Resource對象唯一溃槐,AssetManager對象便唯一。
又Resource對象是ContextImpl對象的成員變量科吭,而ContextImpl對象的數(shù)=Activity數(shù)+Service數(shù)+1個Application昏滴,所以Resource對象不唯一?我們不妨來分析下Application对人、Activity與Service在初始化的過程中對Resource是如何賦值的谣殊。
Application與Context
ActivityThread.java
private void handleBindApplication(AppBindData data) {
// ......
// 獲取應(yīng)用信息LoadedApk
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
// 實例化Application
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
}
Activity與Context
ActivityThread.java
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//...
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
//......
Activity activity = null;
//......
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
//......
//createBaseContextForActivity返回了ContextImpl實例
Context appContext = createBaseContextForActivity(r, activity);
//......
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);
//......
return activity;
}
Service與Context
ActivityThread.java
private void handleCreateService(CreateServiceData data) {
//...
LoadedApk packageInfo = getPackageInfoNoCheck(
data.info.applicationInfo, data.compatInfo);
//......
service = (Service) cl.loadClass(data.info.name).newInstance();
//......
ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
context.setOuterContext(service);
Application app = packageInfo.makeApplication(false, mInstrumentation);
service.attach(context, this, data.info.name, data.token, app,
ActivityManagerNative.getDefault());
//......
}
由上述分析可知,Resource對象中的關(guān)鍵屬性都是由LoadedApk對象中傳遞的牺弄,因此只要LoadedApk對象唯一姻几,Resource對象便唯一。
而LoadedApk對象幾乎都是從ActivityThread#getPackageInfoNoCheck方法中獲取的势告。
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
if (mSystemThread && "android".equals(aInfo.packageName)) {
packageInfo.installSystemApplicationInfo(aInfo,
getSystemContext().mPackageInfo.getClassLoader());
}
if (differentUser) {
// Caching not supported across users
} else if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}
LoadedApk對象以包名為鍵值緩存在一個ArrayMap中蛇捌。因此,LoadedApk對象全局唯一咱台,修改了LoadedApk中的資源路徑豁陆,也便修改了Resource對象中的資源路徑。
又Resource對象實際查找資源的能力是在ResourceImpl對象中吵护,ResourceImpl對象是全局唯一的盒音,而Resource對象每次在調(diào)用android.app.ResourcesManager#getResources時都會生成表鳍。
參考:https://segmentfault.com/a/1190000013048236?utm_medium=referral&utm_source=tuicool
資源的插件化方案
資源的插件化方案分為兩種:一種是合并資源方案,將插件的所有資源添加到宿主的Resources中祥诽,這種插件方案可以訪問宿主的資源譬圣。另一種是構(gòu)建插件資源方案,為每個插件都構(gòu)造出獨立的Resources雄坪,這種方案不可以訪問宿主資源厘熟。
hook思路主要分兩種:一種是在Application初始化前替換掉LoadedApk的資源路徑,這種方式可以一勞永逸维哈,以VirtualApp為代表绳姨;另一種是自己實現(xiàn)Contextmpl并重寫getResources()方法,返回自己創(chuàng)建的Resources對象阔挠,再在每次Application或四大組件初始化的時候?qū)⒆约旱腸ontext對象替換進去飘庄,以VirtualApk為代表。
插件化資源沖突的處理
插件化資源沖突主要是指資源id的沖突购撼,資源id由三部分組成跪削,即PackageId+TypdId+EntryId,如0x7f0b0001代表的是layout類型的第二個資源迂求。同一資源id可能對應(yīng)了宿主和插件apk中兩個不同的資源碾盐。解決這個問題就是要為不同的插件設(shè)置不同的PackageId。
方案一: 修改AAPT揩局,為每個插件指定不同的前綴毫玖,只要不是0x7f就行。
方案二: 在aapt執(zhí)行后凌盯,修改R.java和arsc文件付枫,修改R.java中所有的資源id前綴,修改arsc文件中所有的資源id前綴十气。(gradle-small插件,hook了processReleaseResource task)