Android插件化资源的使用及动态加载 附demo

Posted 刘镓旗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android插件化资源的使用及动态加载 附demo相关的知识,希望对你有一定的参考价值。

上一篇我们已经完成了一个真正可运行的插件化demo,而且demo中也解决了插件中不可以使用资源的问题,但是由于篇幅的问题我们并没有对原理讲解,所以这一篇是对上一篇的一个收尾,如果没有看过上一篇建议先看Android插件化完美实现代码资源加载及原理讲解 附可运行demo.

demo地址 : https://github.com/ljqloveyou123/LiujiaqiAndroid

我们的宿主应用调用一个未安装的插件apk,正常的情况下是不能访问插件中的资源的,例如R.,因为我们宿主中根本就不存在这个资源id,所以就会崩溃。还有另一种情况,基于我们上一篇的demo中,我们使用了占坑的方式加载了插件中apk中的Activity,上一篇我们也分析了创建Activity的时候需要Classloader,这个Classloader是通过 r.packageInfo.getClassloader()来获取的,而 r.packageInfo是一个LoadedApk类型的对象,这个对象是一个apk在内存中的标示, Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等四大组件的信息我们都可以通过此对象获取。我们再看一下这个LoadedApk对象怎么创建

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) 
    synchronized (mResourcesManager) 
        WeakReference<LoadedApk> ref;
        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);

          。。。
        
        return packageInfo;
    

我们看一下构造参数的第二个参数,aInfo,这是一个ApplicationInfo类型的对象,我们上一篇并没有构造我们自己的LoadedApk对象,而只是将我们的dex和宿主的合并了而已,那么也就是说了,这个LoadedApk里传入的ApplicationInfo其实是我们宿主的,并不是插件apk中的。那么也就是说如果我们在插件apk中直接使用资源,等到插件apk被宿主调用器后,使用的是宿主的资源库,而宿主的资源中并没有我们插件apk中的资源,所有一运行的时候就会报错。那么要解决这个问题我们就得想办法,让插件apk运行的时候使用自己的资源才行,下面我们分析。

我们在代码中使用资源的时候都是通过R.,或者是Context.getResources(),这两种方式,其实R.也是通过Context.getResources()查找对应id的。那么我们直接分析Context.getResources()就好了,那么Context的实现类是ContextImpl,我们去看看。
platform_frameworks_base-master\\core\\java\\android\\app\\ContextImpl.java

 @Override
public Resources getResources() 
    return mResources;

我们再看一下这个mResources怎么创建的,5.1源码,在这里说一下

private ContextImpl(ContextImpl container, ActivityThread mainThread,
        LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
        Display display, Configuration overrideConfiguration, int createDisplayWithId) 

  ...

    mPackageInfo = packageInfo;

    //这里拿到了一个ResourcesManager,单例的,说明我们应用当中使用的都是同一套资源
    mResourcesManager = ResourcesManager.getInstance();

   ...

    //LoadedApk对象中得到Resources对象
    Resources resources = packageInfo.getResources(mainThread);
     Resources resources = packageInfo.getResources(mainThread);
    if (resources != null) 
        if (activityToken != null
                || displayId != Display.DEFAULT_DISPLAY
                || overrideConfiguration != null
                || (compatInfo != null && compatInfo.applicationScale
                        != resources.getCompatibilityInfo().applicationScale)) 
            //给resource赋值
            resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                    packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                    packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                    overrideConfiguration, compatInfo, activityToken);
        
    
    //给mResources赋值
    mResources = resources;

    ...


ResourcesManager.getInstance()是单例的这样保证了我们每个Context获取的都是同样的资源,resources通过getTopLevelResources方法赋值,我们看看getTopLevelResources方法干了什么

public Resources getTopLevelResources(String resDir, String[] splitResDirs,
            String[] overlayDirs, String[] libDirs, int displayId,
            Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) 
        final float scale = compatInfo.applicationScale;
        ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
        Resources r;
        synchronized (this) 
            // Resources is app scale dependent.
            if (false) 
                Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
            
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
            if (r != null && r.getAssets().isUpToDate()) 
                if (false) 
                    Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                            + ": appScale=" + r.getCompatibilityInfo().applicationScale);
                
                return r;
            
        

        //if (r != null) 
        //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "
        //            + r + " " + resDir);
        //

        //创建一个AssetManager对象
        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 (resDir != null) '
            //添加资源路径
            if (assets.addAssetPath(resDir) == 0) 
                return null;
            
        

        if (splitResDirs != null) 
            for (String splitResDir : splitResDirs) 
                if (assets.addAssetPath(splitResDir) == 0) 
                    return null;
                
            
        

        if (overlayDirs != null) 
            for (String idmapPath : overlayDirs) 
                assets.addOverlayPath(idmapPath);
            
        

        if (libDirs != null) 
            for (String libDir : libDirs) 
                if (assets.addAssetPath(libDir) == 0) 
                    Slog.w(TAG, "Asset path '" + libDir +
                            "' does not exist or contains no resources.");
                
            
        

        //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);
        Configuration config;
        boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
        final boolean hasOverrideConfig = key.hasOverrideConfiguration();
        if (!isDefaultDisplay || hasOverrideConfig) 
            config = new Configuration(getConfiguration());
            if (!isDefaultDisplay) 
                applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
            
            if (hasOverrideConfig) 
                config.updateFrom(key.mOverrideConfiguration);
            
         else 
            config = getConfiguration();
        '
        //创建一个Resource对象
        r = new Resources(assets, dm, config, compatInfo, token);
        if (false) 
            Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
                    + r.getConfiguration() + " appScale="
                    + r.getCompatibilityInfo().applicationScale);
        

        synchronized (this) 
            WeakReference<Resources> wr = mActiveResources.get(key);
            Resources existing = wr != null ? wr.get() : null;
            if (existing != null && existing.getAssets().isUpToDate()) 
                // Someone else already created the resources while we were
                // unlocked; go ahead and use theirs.
                r.getAssets().close();
                return existing;
            

            // XXX need to remove entries when weak references go away
            mActiveResources.put(key, new WeakReference<Resources>(r));
            return r;
        
    

方法有点长,但其实主要就3步,这里说明一下,每个版本Resource的赋值过程可能不太一样,但是最终都是通过一下三步来创建的Resource

1.创建AssetManager对象

2.通过addAssetPath方法将资源路径添加给AssetManager,这个方法是hint的,我们要通过反射调用

  /**
 * Add an additional set of assets to the asset manager.  This can be
 * either a directory or ZIP file.  Not for use by applications.  Returns
 * the cookie of the added asset, or 0 on failure.
 * @hide
 */
public final int addAssetPath(String path) 
    synchronized (this) 
        int res = addAssetPathNative(path);
        makeStringBlocks(mStringBlocks);
        return res;
    

3.通过AssetManager对象创建一个Resource对象,我们选择一个参数比较少的,参数说明第一个是AssetManager,后后面两个是和设备相关的配置参数,我们可以直接使用宿主的

 /**
 * Create a new Resources object on top of an existing set of assets in an
 * AssetManager.
 * 
 * @param assets Previously created AssetManager. 
 * @param metrics Current display metrics to consider when 
 *                selecting/computing resource values.
 * @param config Desired device configuration to consider when 
 *               selecting/computing resource values (optional).
 */
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) 
    this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);

那么我们通过上面的分析后,知道了Resource怎么创建的,那么我们就可以给插件apk创建一个属于自己的Resource对象,这样他就可以自由的使用资源了。但是还要留意一点就是在我们的插件apk默认拿的是宿主的Resource对象,如果想让插件apk可以自由的使用资源,那么我们就必须要在宿主中提供一个返回插件apk自己资源的方法,然后在插件apk中我们要重写Context的getResource方法,这样才算真正的完成

1.在宿主程序中,一定要在调用插件apk前执行下面代码,demo中在Application中执行

        //创建我们自己的Resource
        String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/chajian_demo.apk";

        //创建AssetManager
        assetManager = AssetManager.class.newInstance();
        Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
        addAssetPathMethod.setAccessible(true);

        addAssetPathMethod.invoke(assetManager, apkPath);


        Method ensureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
        ensureStringBlocks.setAccessible(true);
        ensureStringBlocks.invoke(assetManager);

        Resources supResource = getResources();
        Log.e("Main", "supResource = " + supResource);
        newResource = new Resources(assetManager, supResource.getDisplayMetrics(), supResource.getConfiguration());

        mTheme = newResource.newTheme();
        mTheme.setTo(super.getTheme());

2.在宿主中提供给插件apk返回自己Resource对象的方法,demo中为了简单直观,直接在Application中重写了getResources和getAssets方法,这个可以根据自己的需求来定,只是提供一种思路

     @Override
public AssetManager getAssets() 
    return assetManager == null ? super.getAssets() : assetManager;


@Override
public Resources getResources() 
    return newResource == null ? super.getResources() : newResource;

3.在插件apk中使用资源的Activity中一定要重新getResources和getAssets方法,因为我们的插件apk默认使用的是宿主的Resource,而宿主中并没有我们插件中的资源id,一定要拿自己的Resource才可以使用。这里说的使用资源包括R.和Context.getResource()。

 @Override
public AssetManager getAssets() 
    if(getApplication() != null && getApplication().getAssets() != null)
        return getApplication().getAssets();
    
    return super.getAssets();


@Override
public Resources.Theme getTheme() 
    if(getApplication() != null && getApplication().getTheme() != null)
        return getApplication().getTheme();
    
    return super.getTheme();

ok了,到这里可以愉快的在插件apk中使用资源了,以上demo代码为了简单明了,所有很多地方需要自己根据需求改进,这里只给出了实现的思路和方法。到这里插件化所有的东西全部说完了,研究这些东西也花了不少的时间,拿出来和大家分享,如果觉得不错的话,还请给点个赞,项目上给个star,

demo地址 : https://github.com/ljqloveyou123/LiujiaqiAndroid

以上是关于Android插件化资源的使用及动态加载 附demo的主要内容,如果未能解决你的问题,请参考以下文章

Android插件化完美实现代码资源加载及原理讲解 附可运行demo

Android 插件化Hook 插件化框架 ( Hook Activity 启动流程 | AMS 启动前使用动态代理替换掉插件 Activity 类 )

插件化中Activity的加载

插件化中Activity的加载

Android 插件化Hook 插件化框架 ( 加载插件包资源 )

Android 插件化Hook 插件化框架 ( 从源码角度分析加载资源流程 | Hook 点选择 | 资源冲突解决方案 )