Android插件化原理(三)——加载插件资源


系列帖子:

Android插件化原理(一)—— 插件类加载、类加载原理、(双亲委托机制)
Android插件化原理(二)—启动插件Activity

一点感慨

一开始打算整个系列在2-3周之内完成的,但是一拖再拖,期间工作也一直变动,从小鹏到上汽,现在到阿维塔,工作内容也一直在变,刚开始坐的传统互联网,后来做车载地图,
不得不说车企真的太卷了,一直都没有太多的精力去完成,直到现在我开始做平台化,成为革命的砖,哪里需要去哪里,
从一开始踽踽独行到现在独当一面,从初出茅庐一直喊别人大佬大哥到现在也会经产听到别人喊自己大佬,一路的成长虽然心酸但也有一丝丝小小的成就感;

本来不打算更新这个系列了,而且作为整个系列,加载插件的资源是个很简单的小技术,但始终放在心里是个疙瘩,好像task没有完成一样,做事还得有始有终不是嘛,所以今天打算继续完成。

应用

单作为一个技术点来讲,其实没有什么意义,还是要围绕需求展开的,加载插件资源最常见的应用就是插件化换肤了。我们直到,apk绝大部分体积都是资源,如果多套主题皮肤,那么包体积就会增加很多,
所以插件化换肤就很实用。可以实现多样化主题,个性化定制,提高了应用的灵活性和扩展性。

应用加载资源的原理

要看加载资源的原理,我们先看一下,在实际运行中如何获取资源的

如何获取资源

Activity

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getResources().getDrawable();
        getDrawable(0);
    }

Context

    @Nullable
    public final Drawable getDrawable(@DrawableRes int id) {
        return getResources().getDrawable(id, getTheme());
    }

实际上,不论是直接调用getDrawable或者Resource().getDrawable(),亦或者getContext().getDrawable()最终调用的都是Re
Resources的方法

Resources


    public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
            throws NotFoundException {
        return getDrawableForDensity(id, 0, theme);
    }

    @Nullable
    public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValueForDensity(id, density, value, true);
            return impl.loadDrawable(this, value, id, density, theme);
        } finally {
            releaseTempTypedValue(value);
        }
    }

    

ResourcesImpl

    void getValueForDensity(@AnyRes int id, int density, TypedValue outValue,
            boolean resolveRefs) throws NotFoundException {
        boolean found = mAssets.getResourceValue(id, density, outValue, resolveRefs);
        if (found) {
            return;
        }
        throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
    }

其中,最重要的就是 mAssets.getResourceValue(id, density, outValue, resolveRefs),而mAssets就是android.content.res.AssetManager,

AssetManager


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

    private static native int nativeGetResourceValue(long ptr, @AnyRes int resId, short density,
                                                     @NonNull TypedValue outValue, boolean resolveReferences);

最终呢,调用到了nativeGetResourceValue

总结:查找资源从Context开始到Context内部的Resources,再到Resources的成员ResourcesImpl,再到ResourcesImpl的成员AssetManager,最终呢,到大native层,到这一步,我们可以说资源实际上是由AssetManager管理的。
那么,AssetManager又是如何来的呢?

继续往下看应用资源加载原理

android资源加载原理

我们从Activity的创建开始,一步步看他的资源是如何加载出来的

ActivityThread.handleLaunchActivity

    @Override
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {

        //...

        final Activity a = performLaunchActivity(r, customIntent);
        //...

    }


    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        //...    
        ContextImpl appContext = createBaseContextForActivity(r);
        //...
    }

    private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
        final int displayId;
        try {
            displayId = ActivityManager.getService().getActivityDisplayId(r.token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    
        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
    
       //...
        return appContext;
    }   
    

上述代码是Activity启动时,为Activity创建了ContextImpl,下面是如何创建的

ContextImpl

    static ContextImpl createActivityContext(ActivityThread mainThread,
            LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
            Configuration overrideConfiguration) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");

        String[] splitDirs = packageInfo.getSplitResDirs();
        //获取ClassLoader
        ClassLoader classLoader = packageInfo.getClassLoader();

        if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "SplitDependencies");
            try {
                classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName);
                splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
            } catch (NameNotFoundException e) {
                // Nothing above us can handle a NameNotFoundException, better crash.
                throw new RuntimeException(e);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
        }
        //创建实例
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
                activityToken, null, 0, classLoader);

        // Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
        displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY;

        final CompatibilityInfo compatInfo = (displayId == Display.DEFAULT_DISPLAY)
                ? packageInfo.getCompatibilityInfo()
                : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;

        final ResourcesManager resourcesManager = ResourcesManager.getInstance();

        // Create the base resources for which all configuration contexts for this Activity
        // will be rebased upon.
        //注入资源
        context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));
        context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
                context.getResources());
        return context;
    }

这里主要有3步骤:

  • 找到当前的ClassLoader
  • 构造ContextImpl实例
  • 通过ResourcesManager获取资源Resources,然后注入ContextImpl

那么ResourcesManager的Resources从何而来呢,我们继续往下看

ResourcesManager

    public @Nullable Resources createBaseActivityResources(@NonNull 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 {
            
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
     
            synchronized (this) {
                // Force the creation of an ActivityResourcesStruct.
                getOrCreateActivityResourcesStructLocked(activityToken);
            }

            // Update any existing Activity Resources references.
            updateResourcesForActivity(activityToken, overrideConfig, displayId,
                    false /* movedToDifferentDisplay */);

            // Now request an actual Resources object.
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
                                                 @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
    //......

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

这里省略大量代码,主要看createResourcesImpl:


    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

    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;
            }
        }
        //省略类似代码

        return builder.build();
    }

这里我们先看loadApkAssets


    private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
            throws IOException {
        final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
        ApkAssets 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) {
                mLoadedApkAssets.put(newKey, apkAssets);
                return apkAssets;
            } else {
                // Clean up the reference.
                mCachedApkAssets.remove(newKey);
            }
        }
        //如果上面检查缓存没有,就直接按照path加载

        // We must load this from disk.
        if (overlay) {
            apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path),
                    false /*system*/);
        } else {
            apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib);
        }
        mLoadedApkAssets.put(newKey, apkAssets);
        mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
        return apkAssets;
    }

这段代码,首先会从缓存里面检查是否已经加载过了,如果没有,就按照path加载,核心代码就是apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib);后面的代码到Native了,没有必要往下看了。

再回到 builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/, false /*overlay*/));

    public static class Builder {
        private ArrayList<ApkAssets> mUserApkAssets = new ArrayList<>();

        public Builder addApkAssets(ApkAssets apkAssets) {
            mUserApkAssets.add(apkAssets);
            return this;
        }

        public AssetManager build() {
            // Retrieving the system ApkAssets forces their creation as well.
            final ApkAssets[] systemApkAssets = getSystem().getApkAssets();

            final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
            final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];

            System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);

            final int userApkAssetCount = mUserApkAssets.size();
            for (int i = 0; i < userApkAssetCount; i++) {
                apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
            }

            // Calling this constructor prevents creation of system ApkAssets, which we took care
            // of in this Builder.
            final AssetManager assetManager = new AssetManager(false /*sentinel*/);
            assetManager.mApkAssets = apkAssets;
            AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
                    false /*invalidateCaches*/);
            return assetManager;
        }
    }

ok,我们梳理一下流程:

![流程图][1]

资源最终是在AssetManager被加载进来的,而内部是通过ApkAssets.loadFromPath跟局path加载为ApkAssets,然后添加到AssetManager,AssetManager在ResourcesImpl中,ResourcesImpl是Resources的成员…


ok,了解了系统是如何为Activity(Context)加载资源的,我们接下来开始正题——加载插件资源

加载插件资源

在上一节我们了解了系统加载资源的方法,核心就是

  • 1.调用ApkAssets.loadFromPath加载ApkAssets
  • 2.将ApkAssets添加到AssetManager.Builder
  • 3.调Build.build得到AssetManager

但实际上不论AssetManager还是AssetManager.Builder都对开发者不可见的,我们有更简单的办法,查看AssetManager源码发现一个方法:

    public int addAssetPath(String path) {
        return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
    }

    private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
        Preconditions.checkNotNull(path, "path");
        synchronized (this) {
            ensureOpenLocked();
            final int count = mApkAssets.length;
    
            // See if we already have it loaded.
            for (int i = 0; i < count; i++) {
                if (mApkAssets[i].getAssetPath().equals(path)) {
                    return i + 1;
                }
            }
    
            final ApkAssets assets;
            try {
                if (overlay) {
                    // TODO(b/70343104): This hardcoded path will be removed once
                    // addAssetPathInternal is deleted.
                    final String idmapPath = "/data/resource-cache/"
                            + path.substring(1).replace('/', '@')
                            + "@idmap";
                    assets = ApkAssets.loadOverlayFromPath(idmapPath, false /*system*/);
                } else {
                    assets = ApkAssets.loadFromPath(path, false /*system*/, appAsLib);
                }
            } catch (IOException e) {
                return 0;
            }
    
            mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
            mApkAssets[count] = assets;
            nativeSetApkAssets(mObject, mApkAssets, true);
            invalidateCachesLocked(-1);
            return count + 1;
        }
}

addAssetPathInternal是不是感觉很熟悉,其实我们只需要反射构造AssetManager实例,然后反射addAssetPath即可。

加载插件资源的核心代码:


    /**
     * 加载插件的Resources
     *
     * @param context
     * @return
     */
    public static Resources loadResources(Context context, String pluginAbsolutePath) {
        try {
            //创建加载插件的AssetManager
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            //加载插件的资源
            addAssetPathMethod.invoke(assetManager, pluginAbsolutePath);
            //获取宿主的资源,获取其资源configuration等信息
            Resources resources = context.getResources();
            return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());

        } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
            Log.e(TAG, "loadResources: ", e);
        }

        return null;
    }

这段代码是不是很直观

  • 通过反射获取AssetManager实例
  • 反射调用addAssetPath方法把插件apk的资源加载到其实例
  • 根据宿主app的屏幕配置、主题配置等信息构造属于插件的Resources实例

是不是很简单,就是这么简单。

关于插件化换肤的一些看法

我看有的人会把插件的资源加载进来后像是我们加载插件Activity一样,合并到宿主中,但是我不建议这么做,主要问题如下:

  • 一来是还需要进行一系列的反射(尽管上面我特意说到资源相关类的从属关系,这就是反射的关键)
  • 二来还会碰到资源冲突的问题,一通操作下来我觉得浪费时间得不偿失
  • 另外我觉得不如将插件资源包装到个人的资源管理类,全局用资源管理类加载资源,而不是用默认的context加载,这样配合换肤管理类使用更加灵活方便。

好了,插件化这一系列终于完成了,🤦‍♂️🤦‍♂️🤦‍♂️

附:Github所有代码

  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值