Android換膚(二)源碼分析App資源的加載流程

大家思考一下,如果我們要去給我們的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,就實現了換膚的效果。

到此,換膚的基礎知識講完了,下篇文章我們開始進入實戰。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章