安卓插件化课程-第四篇:加载apk中的资源

 序:

1.本文是安卓插件化课程的第四篇,完整课程链接参见下面链接:

安卓插件化课程-序章_分享+记录-CSDN博客前言:目前安卓领域,插件化十分的流行,本以为这一类的文章会有很多,但是百度一搜,基本上讲的插件化都是皮毛,没有涉及到核心。所以就想写一系列的文章来一步一步深入的讲解插件化,通过实现逐渐增加难度的需求,最终实现插件化方案。章节:插件化系列文章主要包含以下几个篇章:1.加载插件apk中的类2.启动插件apk中的activity3.使用插桩的方式启动apk中的activity4.使用插件apk中的资源5.使用插件apk中的so文件6.插件化加载移动apk。7.插件化目前所遇到的https://blog.csdn.net/rzleilei/article/details/122453035?spm=1001.2014.3001.5502https://blog.csdn.net/rzleilei/article/details/122453035?spm=1001.2014.3001.5502

2.主要内容

本篇主要讲的是如何在宿主APP中,使用插件中的资源文件,这是以使用插件中的一个string为例子。

一:原理简述

想知道如何加载插件中的资源,那么一定先要了解正常获取资源的流程。

我们先扩展一下,了解一下获取资源的流程,以及应用加载资源的时机和流程。


1.1.获取资源文件的全流程

1.resource.getString()

这里仍然以取string为例子,resource中getString,最终调用的是getText方法,

@NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        throw new NotFoundException("String resource ID #0x"
                + Integer.toHexString(id));
    }

最终调用的是assetManager的getResourceText方法。

2.getResourceText方法

   @Nullable CharSequence getResourceText(@StringRes int resId) {
        synchronized (this) {
            final TypedValue outValue = mValue;
            if (getResourceValue(resId, 0, outValue, true)) {
                return outValue.coerceToString();
            }
            return null;
        }
    }

交给了getResourceValue方法去处理,返回值是boolean告知是否查找最成功。如果成功则会给TypeValue赋值。

@UnsupportedAppUsage
    boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
            boolean resolveRefs) {
        Objects.requireNonNull(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) {
                if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
                    return false;
                }
            }
            return true;
        }
    }

这里可以看到,最终是交给了nativeGetResourceValue方法去处理的,JNI层。

3.nativeGetResourceValue方法

待补充

1.2应用何时加载资源

1.前面讲过了,应用的dex文件,其实是由虚拟机去加载的。那么资源文件是何时加载呢?讲到资源加载就不得不提一下一个应用启动的流程。具体流程这个可以参照我的另外一片文章:Android中APP启动的完整流程。

这里就简单的讲一下:

桌面点击图标->通知系统->系统创建APP进程并调用ActivityThread的main方法->APP调用attach方法通知系统已经启动->系统通知APP进行Application的创建,调用handleBindApplication方法来创建application。

这里主要包含四个关键步骤:

第一步: data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);

service传递过来ApplicationInfo,app这边来用来创建LoadedApk对象。这时候赋值记录资源包路径等信息。

第二步:ContextImpl.createAppContext(this, data.info);使用LoadedApk对象去创建Context对象。

我们activity中获取resource对象等操作,最终都是由ContextImpl来提供的。ContextImpl就是ContextWapper这个application和activity的父类中的mBase。

第三步:mInstrumentation = new Instrumentation();创建Instrumentation对象

第四部:app = data.info.makeApplication(data.restrictedBackupMode, null);创建最终的application对象

所以资源文件的加载,就发生在第二步中,资源都是通过ContextImpl来获取的。

不过实际上加载资源的流程,application和activity对应的assetManager其实是两个,但流程是一样的。

2.创建流程的调用逻辑如下图所示:

而最终添加的资源路径其实就是apk文件的地址,下面就是一个例子

ApkAssets{path=<empty> and /data/app/~~s9LfytxMb1TEz7xqWvupRA==/com.xt.appplugin-IwJNMlbgwYpZNTVrhYPk4w==/base.apk}

1.3如何加载外部资源

1.上面资源加载的流程和时机,因为我们一般来说最早的调用时机是application中的onCreate方法,但是在这之前资源对象就已经生成好了,所以生成时去hook就不太可能了。

2.在1.1中我们知道获取资源的流程是先通过resource对象,然后resource对象又通过assetManager去获取的。所以这里我们有两个方案,第一是替换掉resource对象,第二就是对assetManager做一些改造。

参照在assetManager.Builder的build方法中,最终生成assetManager的方法如下:

final AssetManager assetManager = new AssetManager(false /*sentinel*/);
            assetManager.mApkAssets = apkAssets;
            AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
                    false /*invalidateCaches*/);
            assetManager.mLoaders = mLoaders.isEmpty() ? null
                    : mLoaders.toArray(new ResourcesLoader[0]);

            return assetManager;

给mApkAssets赋值,然后通知native层刷新。我们发现assetManager中还有一个方法也提供了这样的功能:

   /**
     * @deprecated Use {@link #setApkAssets(ApkAssets[], boolean)}
     * @hide
     */
    @Deprecated
    @UnsupportedAppUsage
    public int addAssetPath(String path) {
        return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
    }

3.所以第一个想法是,获取到assetManager对象之后,通过反射调用这个方法,把外部APK中资源也加载进来。但是尝试了一把之后,失败了,虽然mApkAssets中的路径确实已经变了,但是我们获取资源的时候,仍然获取不到。

4.既然对assetManager改造不行,那我们就回过头来想想如何改造resource对象。好行也不行,因为Rosources中也是直接交给asset处理的。

 @NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        throw new NotFoundException("String resource ID #0x"
                + Integer.toHexString(id));
    }

5.既然hook不行,那我们能不能模拟系统的整个流程,也构造一个Resource出来呢?这条路试了下,是可以的,那我们就先按照这个方法去实现。

1.4如何在插件中使用插件中的资源

这个就是下一章要讲的内容了,本章就不扩展了。

二:代码编写


1.插件项目中资源项

<resources>
    <string name="app_name">AppPlugin</string>
    <string name="plugin_str1">这是plugin中资源文件中的字符串</string>
</resources>

2.编译项目,拷贝apk

我们可以看到已经string资源已经打包进apk了。


3.加载插件资源

我们直接生成一个AssetManager,然后调用addAssetPath的方法,最终生成Resources对象。

val apkPath = context.filesDir.absolutePath + File.separator + APK_NAME
                //反射加载资源包
                val newInstance = AssetManager::class.java.newInstance()
                val setApkAssetsMedthod = AssetManager::class.java.getDeclaredMethod(
                    "addAssetPath",
                    String::class.java
                )
                setApkAssetsMedthod.invoke(newInstance, apkPath)
                val resources = Resources(
                    newInstance,
                    context.resources.displayMetrics,
                    context.resources.configuration
                )

然后通过resource去获取插件中的资源。

 val string1 = resources.getString(0x7f040001)
                showResult(string1)

这里的ID是直接从插件APK中拷贝的。


4.测试验证

首先点击加载插件,然后点击使用插件中的资源。

这时候我们看到,插件中的字符串已经显示出来了。

三:要点总结

待补充

四。代码地址:


项目地址:

https://github.com/aa5279aa/android_all_demo

插件项目位置:https://github.com/aa5279aa/android_all_demo/tree/master/DemoClient/appplugin

调用类位置:https://github.com/aa5279aa/android_all_demo/blob/master/DemoClient/app/src/main/java/com/xt/client/fragment/DynamicFragment.kt

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失落夏天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值