大家思考一下,如果我們要去給我們的app換膚,肯定需要用到插件化的加載方式,
去加載第三方的apk包,所以我們需要知道frameWork層是怎麼去加載整個app的資源的。
-
我們先看看把打包完成的一個apk解壓縮出來會生成以下文件
- assets:資源文件
- lib:so庫,ndk開發的代碼庫
- META-INF:一系列的簽名文件,apk發佈就需要簽名文件來簽名,不然發佈不了、手機也安裝不了
- 大量的dex文件:java編譯生成的java文件(包括R.java文件)。
其中resources.arsc就是我們本文要將的重點,它的儲存結構類似於數據庫文件,主要用於表示整個app裏所有資源的對應關係
例如我們要找一個color資源,Name表示這個xml文件的名字,default表示這個xml文件的所在的位置,還有ID信息是不是很像我們的數據庫表信息。
實際上,當我們去加載一個apk包的時候,會有一個類去負責加載resources.arsc文件中的信息,所以我們本文的重點就是分析下,在源碼中resources.arsc文件中的信息是怎麼實現加載的。
和第一篇文章Android換膚(一)源碼分析View的創建流程一樣,我們直接從ActivityThread開始。
- 1、ActivityThread#handleBindApplication
瞭解過Activity創建、啓動流程源碼的小夥伴肯定知道,在application和activity的所有生命週期調用中,都會先調用instrumentation的相應方法。
例如:callActivityOnCreate,callApplicationOnCreate,newActivity,callActivityOnNewIntent
對於每一個android app來說,它的總入口都是ActivityThread#main. 每一個應用的進程都有一個ActivityThread對象,而每一個ActivityThread對象都有一個Instrumentation mInstrumentation;成員變量。mInstrumentation的初始化ActivityThread#handleBindApplication方法裏。
private void handleBindApplication(AppBindData data) {
。。。此處省略很多代碼。。。
mInstrumentation = new Instrumentation();
。。。此處省略很多代碼。。。
Application app;
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
app = data.info.makeApplication(data.restrictedBackupMode, null);
// Propagate autofill compat state
app.setAutofillOptions(data.autofillOptions);
// Propagate Content Capture options
app.setContentCaptureOptions(data.contentCaptureOptions);
mInitialApplication = app;
// don't bring up providers in restricted mode; they may depend on the
// app's custom Application class
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
installContentProviders(app, data.providers);
}
}
// Do this after providers, since instrumentation tests generally start their
// test thread at this point, and we don't want that racing.
try {
mInstrumentation.onCreate(data.instrumentationArgs);
}
catch (Exception e) {
throw new RuntimeException(
"Exception thrown in onCreate() of "
+ data.instrumentationName + ": " + e.toString(), e);
}
try {
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
} finally {
// If the app targets < O-MR1, or doesn't change the thread policy
// during startup, clobber the policy to maintain behavior of b/36951662
if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1
|| StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {
StrictMode.setThreadPolicy(savedPolicy);
}
}
}
我們看到了這裏主要是新建了一個mInstrumentation成員對象,然後又通過 app = data.info.makeApplication(data.restrictedBackupMode, null);
創建了一個Application,再調用mInstrumentation的callApplicationOnCreate(app)方法調用了Application的OnCreate方法。
所以我們先進入makeApplication看看Application是怎麼創建的,
- 2、LoadedApk#makeApplication
@UnsupportedAppUsage
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication");
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"initializeJavaContextClassLoader");
initializeJavaContextClassLoader();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
mActivityThread.mAllApplications.add(app);
mApplication = app;
if (instrumentation != null) {
try {
instrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!instrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}
// Rewrite the R 'constants' for all library apks.
SparseArray<String> packageIdentifiers = getAssets().getAssignedPackageIdentifiers();
final int N = packageIdentifiers.size();
for (int i = 0; i < N; i++) {
final int id = packageIdentifiers.keyAt(i);
if (id == 0x01 || id == 0x7f) {
continue;
}
rewriteRValues(getClassLoader(), packageIdentifiers.valueAt(i), id);
}
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
return app;
}
先通過這句代碼 ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);創建一個app的上下文appContext,所以我們再進入createAppContext看看是怎麼創建的```
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
String opPackageName) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
null, opPackageName);
context.setResources(packageInfo.getResources());
return context;
}
看到這句代碼context.setResources(packageInfo.getResources());,字面意思就是從包信息中獲取資源,並且設置給上下文context。看到這裏終於看到了我們本文的重點,所以我們再進去,看看到底是怎麼獲取資源的呢?進入getResources方法.
- 3、LoadedApk#getResources
@UnsupportedAppUsage
public Resources getResources() {
if (mResources == null) {
final String[] splitPaths;
try {
splitPaths = getSplitPaths(null);
} catch (NameNotFoundException e) {
// This should never fail.
throw new AssertionError("null split not found");
}
mResources = ResourcesManager.getInstance().getResources(null, mResDir,
splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
getClassLoader());
}
return mResources;
}
我們看到通過 mResources = ResourcesManager.getInstance().getResources進行mResources的初始化,所以再進入ResourcesManager#getResources方法
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);
}
}
再進入getOrCreateResources方法
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;
}
// 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 resourcesImpl =createResourcesImpl(key);
看看它的資源是怎麼創建的
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;
}
再進入 final AssetManager assets = createAssetManager(key);
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
final AssetManager.Builder builder = new AssetManager.Builder();
// 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) {
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) {
Log.e(TAG, "failed to add split asset path " + splitResDir);
return null;
}
}
}
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
try {
builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/,
true /*overlay*/));
} catch (IOException e) {
Log.w(TAG, "failed to add overlay path " + idmapPath);
// continue.
}
}
}
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) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
// continue.
}
}
}
}
return builder.build();
}
看到 builder.addApkAssets(loadApkAssets(key.mResDir, false /sharedLib/,
false /overlay/));這句代碼,
private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
throws IOException {
final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
ApkAssets apkAssets = null;
if (mLoadedApkAssets != null) {
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) {
if (mLoadedApkAssets != null) {
mLoadedApkAssets.put(newKey, apkAssets);
}
return apkAssets;
} else {
// Clean up the reference.
mCachedApkAssets.remove(newKey);
}
}
// We must load this from disk.
if (overlay) {
apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path),
false /*system*/);
} else {
apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib);
}
if (mLoadedApkAssets != null) {
mLoadedApkAssets.put(newKey, apkAssets);
}
mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
return apkAssets;
}
mLoadedApkAssets裏儲存的就是我們文章開頭講的資源文件的鍵-值關係,源碼裏使用LruCache的方式保存的,具體還想看細節的小夥伴可以再深入ApkAssets類來看看資源文件是怎麼加載進來,我這裏透露一點,其實就是通過底層native方法讀文件的流來實現的。我們這篇文章主要找到apk資源加載的點,以便我們實現換膚的流程,所以就適可而止了。源碼看到這裏我們應該有思路了,要實現換皮膚,就是要把我們插件apk的資源插入到我們宿主apk的mLoadedApkAssets裏面就可以了。
用下面的關係圖總結一下第三步流程中涉及的主要類的關係圖
其實,我們平時加載一個color
加載一個Drawable
實際上最終加載資源都是在類AssetManager裏,
我們查看AssetManager裏的一些主要方法
有根據資源名字獲取資源id的,根據資源id換取資源包名的。這些就是我們換膚中要用到的一些主要方法。
換膚思路:例如在我們的主app中有一個名字爲bottom_item的控件,我們可以先根據名字獲取這個控件的id,在插件app裏有一個和主app相同名字的控件,但是他的顏色是不一樣的,我們就可以根據名字獲取插件裏這個控件的id,把插件裏這個控件的id賦值給主app,就實現了換膚的效果。
到此,換膚的基礎知識講完了,下篇文章我們開始進入實戰。