美文网首页
Android换肤(二)源码分析App资源的加载流程

Android换肤(二)源码分析App资源的加载流程

作者: 程序员三千_ | 来源:发表于2020-07-21 11:03 被阅读0次

    大家思考一下,如果我们要去给我们的app换肤,肯定需要用到插件化的加载方式,
    去加载第三方的apk包,所以我们需要知道frameWork层是怎么去加载整个app的资源的。

    • 我们先看看把打包完成的一个apk解压缩出来会生成以下文件


      image.png
    • assets:资源文件
    • lib:so库,ndk开发的代码库
    • META-INF:一系列的签名文件,apk发布就需要签名文件来签名,不然发布不了、手机也安装不了
    • 大量的dex文件:java编译生成的java文件(包括R.java文件)。

    其中resources.arsc就是我们本文要将的重点,它的储存结构类似于数据库文件,主要用于表示整个app里所有资源的对应关系

    image.png

    例如我们要找一个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里面就可以了。
    用下面的关系图总结一下第三步流程中涉及的主要类的关系图


    image.png

    其实,我们平时加载一个color


    image.png
    image.png
    image.png

    加载一个Drawable


    image.png
    image.png
    image.png
    实际上最终加载资源都是在类AssetManager里,
    我们查看AssetManager里的一些主要方法
    image.png

    有根据资源名字获取资源id的,根据资源id换取资源包名的。这些就是我们换肤中要用到的一些主要方法。

    换肤思路:例如在我们的主app中有一个名字为bottom_item的控件,我们可以先根据名字获取这个控件的id,在插件app里有一个和主app相同名字的控件,但是他的颜色是不一样的,我们就可以根据名字获取插件里这个控件的id,把插件里这个控件的id赋值给主app,就实现了换肤的效果。

    到此,换肤的基础知识讲完了,下篇文章我们开始进入实战。

    相关文章

      网友评论

          本文标题:Android换肤(二)源码分析App资源的加载流程

          本文链接:https://www.haomeiwen.com/subject/yqnbkktx.html