查看原文
其他

浅析Android 资源加载,一眼懂!

jaymzyang 郭霖
2024-12-27


/   今日科技快讯   /

比亚迪西安工业园已完成年产百万辆目标,全年产量首破百万。据介绍,位于西安高新区的比亚迪西安汽车工厂是比亚迪在全国最大的单一汽车工厂。目前,比亚迪西安工厂有四条总装线在同时生产,每天产量达到4000到4400辆。

/   作者简介   /

大家周一好,现在气温逐渐降低,大家注意做好防寒工作。

本篇文章转自jaymzyang的博客,文章主要分享了Android 资源加载的相关内容,相信会对大家有所帮助!

原文地址:
https://juejin.cn/post/7388441764874813459

/   Resources 资源介绍   /

构建 App 时将资源编译为二进制文件存放到APK文件,并通过 aapt 编译工具生成 resources.arsc 文件,该文件记录资源文件的映射关系(id/资源目录/资源名称/资源值等信息);

在使用一般资源时,我们通过 Context 上下文类获取 Resources 类,然后通过资源管理类(Resources)加载资源文件,在 Resources 类内部的 impl(ResourcesImpl)进行加载资源,而 ResourcesImpl 内部则通过 AssetManager 进行资源真正的加载操作;AssetManager 内部主要是 native 方法,所以资源的加载操作主要在 native 层进行的,下面我们先了解下资源的种类。

而与 style 相关的资源则是通过 Theme 进行加载,Theme 同样由 Context 上下文类获取,Theme 同时也是 Resources 的内部类,Theme 内部通过 ThemeImpl 进行 style 资源的操作,而 ThemeImpl 是 ResourcesImpl 的内部类,在 ThemeImpl 内引用着 ResourcesImpl 的 mAssets(AssetManager)对象,所以通过 Theme 加载 style 资源时,同样是通过 AssetManager 进行加载,并将加载的数据通过 TypedArray 数组进行返回;

资源文件类型如表


apk 内 resources.arsc 文件


/   资源核心类关系模型   /


在之前的文章中,我们知道 ContextImpl 实际是上下文的实现类,所以本篇介绍的 Resources 类是被 ContextImpl 所持有。

创建 Application Context 的 Resources

// ContextImpl.java 文件

// 静态方法,创建 Application 的上下文的实现类 ContextImpl 对象
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo ...) {
    if (packageInfo == nullthrow new IllegalArgumentException("packageInfo");
    // 创建 ContextImpl 对象
    ContextImpl context = new ContextImpl(null, mainThread, packageInfo,
        ContextParams.EMPTY, nullnullnullnullnull0null, opPackageName);
    // 创建 Resources 对象,并赋值给 ContextImpl
    context.setResources(packageInfo.getResources());
...
    return context;
}

// 将 resources 赋值给 ContextImpl 的 mResources 对象
void setResources(Resources r) {
    ...
    mResources = r;
}

// LoadedApk.java
public Resources getResources() {
    if (mResources == null) {
     ...
     // 创建 Resources
        mResources = ResourcesManager.getInstance().getResources(null, mResDir...);
    }
    return mResources;
}

// ResourcesManager.java
public Resources getResources(IBinder activityToken, String resDir...) {
    // 创建 key,用于缓存 resources 对象
    final ResourcesKey key = new ResourcesKey(resDir ...);
    ...
    // 创建 APK 加载资源的内存表示,同时防止GC回收,用于 C++ 交互
    final ApkAssetsSupplier assetsSupplier = createApkAssetsSupplierNotLocked(key);
    ...
    // 创建 Resources
    Resources resources = createResources(key, classLoader, assetsSupplier);
    return resources;
}

// 创建 Resources
private Resources createResources(ResourcesKey key, ClassLoader classLoader, ApkAssetsSupplier apkSupplier) {
    synchronized (mLock) {
     ...
     // 1、创建/获取 ResourcesImpl 对象
        ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key, apkSupplier);
        if (resourcesImpl == null) {
            return null;
        }

        // 2、创建/获取 Resources 对象
        return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
    }
}

/**=======================创建 Resources 分为两部分=========================**/

// 第一部分,创建 ResourceImpl 的流程
// 获取/创建 ResourceImpl
private ResourcesImpl findOrCreateResourcesImplForKeyLocked(ResourcesKey key, ApkAssetsSupplier apkSupplier) {
 // 1.1、从缓存加载 ResourcesImpl
    ResourcesImpl impl = findResourcesImplForKeyLocked(key);
    if (impl == null) {
     // 1.2、创建 ResourcesImpl
        impl = createResourcesImpl(key, apkSupplier);
        if (impl != null) {
         // 1.3、将创建的 ResourcesImpl 对象放入缓存
            mResourceImpls.put(key, new WeakReference<>(impl));
        }
    }
    return impl;
}

// 创建 ResourcesImpl
private ResourcesImpl createResourcesImpl(ResourcesKey key, ApkAssetsSupplier apkSupplier) {
    // 1.2.1、创建 AssetManager
    final AssetManager assets = createAssetManager(key, apkSupplier);
    if (assets == null) {
        return null;
    }
    // 1.2.2、创建 ResourceImpl
    final ResourcesImpl impl = new ResourcesImpl(assets, displayMetrics, config, daj);

    return impl;
}

// 第二部分,创建 Resources 的流程
private Resources createResourcesLocked(... ResourcesImpl impl,CompatibilityInfo compatInfo) {
    // 2.1、创建 Resources 对象
    Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
            : new Resources(classLoader);
    // 2.2、将 impl 设置到 Resources 内
    resources.setImpl(impl);
    // 2.3、将 resources 放入缓存内
    mResourceReferences.add(new WeakReference<>(resources, mResourcesReferencesQueue));
    ...
    return resources;
}

Resources 资源管理类创建流程图


Resources 资源管理对象的创建,主要分为7个步骤:

  1. 创建 ContextImpl 时,同步会创建相关的 Resources 对象,并将 Resources 赋值给 mResources 对象;
  2. 通过 ResourcesManager 的 getResources 方法创建 Resources 对象;
  3. 创建 ResourcesKey 对象,作为缓存key和记录 Resource 相关的信息;
  4. 创建 ApkAssetsSupplier 对象,内部维护了一个 Map 集合,用于缓存不同目录下的资源数据,Key 为 Appkey(目录path生成),Value 为 ApkAssets 的 Map;
  5. 创建 AssetManager,用于和 native 层进行交互;
  6. 创建 ResourceImpl,作为 Resources 的实现类;
  7. 创建 Resources,并将 resources 设置到 ContextImpl;

Resources 由 context 上下文负责持有,我们通过 context 对资源进行访问;采用组合的设计方式,将 resources 的职责放入上下文内,然后根据不同的组件上下文的业务需求,实现了对资源数据操作的装饰;

Resources 创建是通过 ResourceManager,ResourceManger 作为一个单例对象,方便对 Resouces 资源类的管理和维护;Resources 作为一个代理类存在,内部通过持有 ResoucesImpl 对象,实现对资源的操作;而 ResourcesImpl 作为实现类,内部则是通过 AssetManager 实现对资源的访问,AssetManager 主要负责与 Native 层进行交互,访问放在 APK 文件内的资源数据;

而 Theme 的设计与 Resouces 类似,Theme 同样由 context 上下文负责持有,通过 context 对 style 资源进行访问,Theme 的创建则是通过 Resources 负责,Theme 同样作为代理类存在,内部通过持有 ThemeImpl 对象,实现对资源的操作,而 ThemeImpl 作为 ResourcesImpl 的内部类,ThemeImpl 内的 AssetManager 对象,实际是直接持有了 ResourcesImpl 的 mAsset 对象,所以在资源加载上同样是通过 AssetManager 实现的;

存放资源的路径

mLegacyOverlayDirs:此应用程序使用的额外资源包(运行时覆盖)位置的完整路径。此字段仅在存在额外资源包时使用,否则为null,可用于存放换肤的资源包。
mOverlayPaths:包含resourceDirs的内容,以及可能是也可能不是APK包的覆盖的路径。
mResDir:应用程序的APK的完整路径。
splitSourceDirs:零个或多个拆分APK的完整路径,按与splitNames相同的顺序进行索引。

/   资源加载流程解析   /

文本资源的加载:

// Context.java
public final CharSequence getText(@StringRes int resId) {
    // 通过 resources 对资源进行加载
    return getResources().getText(resId);
}

// Resources.java
public CharSequence getText(@StringRes int id) throws NotFoundException {
    // 通过 ResourcesImpl 内的 AssetManager 对资源进行加载
    CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
    if (res != null) {
        return res;
    }
}

// AssetManager.java
CharSequence getResourceText(@StringRes int resId) {
    synchronized (this) {
        // java 对象,用于接收 native 层加载结果
        final TypedValue outValue = mValue;
        if (getResourceValue(resId, 0, outValue, true)) {
            return outValue.coerceToString();
        }
        return null;
    }
}

boolean getResourceValue(int resId, int densityDpi, TypedValue outValue ...) {
    synchronized (this) {
     // native 加载资源
        final int cookie = nativeGetResourceValue(
                mObject, resId, (short) densityDpi, outValue, resolveRefs);
        // 资源未找到
        if (cookie <= 0) {
            return false;
        }

        if (outValue.type == TypedValue.TYPE_STRING) {
         // 对 string 数据进行解析
            if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
                return false;
            }
        }
        return true;
    }
}

结合代码分析,通过id加载资源流程主要分为:

  1. 通过 Resources 提供的加载不同资源的方法,将id传入开始加载资源;
  2. ResourcesImpl 内则是通过 AssetManager 真正去加载资源;
  3. AssetManager 创建 java 层接收数据的 TypeValue 对象,并将其传入 native 层;
  4. 通过 native 方法 nativeGetResourceValue,开始加载id对应的资源数据,并存入 TypeValue 对象内;
  5. 解析 TypeValue 内的数据是否合法,并将数据转换为 java string;

以上是资源加载的基本流程,流程比较简单,至此我们完成了 Resources 资源管理类的创建和加载资源的分析。

/   应用场景   /

Resources 加载本质是通过 native 层进行的文件数据的解析和加载,在加载资源时,一般还会结合 configuration 信息,确定加载的资源路径等信息(colors.xml 和 colors-night.xml)。

换肤业务就是根据 conguration 主题的变换,然后触发资源的加载操作并渲染到屏幕,从而实现了换肤的业务;语言切换业务同理;

上述整理的资源数据类型,其实均可以实现动态替换,我们需要根据业务的需求,将动态加载资源的时机和需要加载的资源配置信息对应,即可实现动态替换资源的需求。

资源加载的设计思想较为清晰,根据业务的特点,采用组合设计将职责关联起来,这种低耦合的设计思想,更容易实现对业务的复用和扩展,在这里我们可以看到很多设计模式,比如:代理模式、单例模式、装饰器模式等。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
从零实现一个 KMP 插桩框架: EzHook
Android Studio 中的 Gemini 迎来自发布以来最大的功能更新

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注
继续滑动看下一个
郭霖
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存