美文网首页插件化
Android资源的插件化

Android资源的插件化

作者: taoyyyy | 来源:发表于2019-12-27 13:52 被阅读0次

    资源的查找过程

    在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方法的调用链

        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;
            }
        }
    

    结论:

    1. 通过id获取资源先后要经过ContextImpl->Resource->ResourceImpl->AssetManager将id传到native方法中,拿这个id通过arsc映射找到对应的资源信息,保存在TypedValue对象中返回。
    2. 通过AssetManager获取资源则是通过AssetManager的native方法直接去找assets目录下对应文件。

    Resource与AssetManager的生成时机

    通过上述分析,我们知道了所有的资源最终都要通过AssetManager到对应apk路径下去访问,那么 apk路径是如何添加到AssetManager中的? 我们不妨正向分析一波,找到Resource与AssetManager的生成时机。
    ContextImpl.java

    void setResources(Resources r) {
            if (r instanceof CompatResources) {
                ((CompatResources) r).setContext(this);
            }
            mResources = r;
        }
    

    找到调用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);
        }
    

    我们发现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)看名字像是一个取缓存的方法,最后我们发现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;
        }
    

    至此,我们发现了apk路径是通过assets.addAssetPath(key.mResDir)调用添加进来的

    Resource与AssetManager对象是否全局唯一以及与LoadedApk的联系

    根据上述分析,我们知道了每个Resource对象中包含一个唯一的AssetManager对象,因此Resource对象唯一,AssetManager对象便唯一。
    又Resource对象是ContextImpl对象的成员变量,而ContextImpl对象的数=Activity数+Service数+1个Application,所以Resource对象不唯一?我们不妨来分析下Application、Activity与Service在初始化的过程中对Resource是如何赋值的。

    Application与Context

    ActivityThread.java

        private void handleBindApplication(AppBindData data) {
               // ......
               // 获取应用信息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对象中的关键属性都是由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对象每次在调用android.app.ResourcesManager#getResources时都会生成。

    参考:https://segmentfault.com/a/1190000013048236?utm_medium=referral&utm_source=tuicool

    资源的插件化方案

    资源的插件化方案分为两种:一种是合并资源方案,将插件的所有资源添加到宿主的Resources中,这种插件方案可以访问宿主的资源。另一种是构建插件资源方案,为每个插件都构造出独立的Resources,这种方案不可以访问宿主资源。
    hook思路主要分两种:一种是在Application初始化前替换掉LoadedApk的资源路径,这种方式可以一劳永逸,以VirtualApp为代表;另一种是自己实现Contextmpl并重写getResources()方法,返回自己创建的Resources对象,再在每次Application或四大组件初始化的时候将自己的context对象替换进去,以VirtualApk为代表。

    插件化资源冲突的处理

    插件化资源冲突主要是指资源id的冲突,资源id由三部分组成,即PackageId+TypdId+EntryId,如0x7f0b0001代表的是layout类型的第二个资源。同一资源id可能对应了宿主和插件apk中两个不同的资源。解决这个问题就是要为不同的插件设置不同的PackageId。
    方案一: 修改AAPT,为每个插件指定不同的前缀,只要不是0x7f就行。
    方案二: 在aapt执行后,修改R.java和arsc文件,修改R.java中所有的资源id前缀,修改arsc文件中所有的资源id前缀。(gradle-small插件,hook了processReleaseResource task)

    相关文章

      网友评论

        本文标题:Android资源的插件化

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