Android的优势之一是它几乎能运行在任何尺寸的设备上,为了能让同一个apk在不同设备上正常运行,Android设计了一套资源管理系统来完成目标。

Android并不是简单地将UI布局和图片进行扩大和缩小来匹配不同配置的设备,而是通过复杂的资源定义方式来保证每种设备都可以有对应的资源文件,从而让用户体验最佳。

资源系统简介

Android 应用适应不同设备的方法是尽量为每种类型的设备提供一套资源。理论上虽然可以这样做,但实际上却行不通,我们只能为常见的几种设备类型提供完整的资源,否则应用的占用空间会膨胀到无法接受的程度

常用术语和单位

Android中常用的单位:

  • dpi:屏幕密度,即每英寸的像素点数。160 dpi表示屏幕每英寸包含160个像素点
  • px:像素,1 px表示一个物理的像素点。px不被推荐使用,但是如果需要通过像素点来控制UI,也可以使用。
  • dpdp是一个虚拟像素单位,1 dp约等于中密度屏幕(160dpi;“基准”密度)上的1像素。对于其他每个密度,Android会将此值转换为相应的实际像素数。
  • spsp多用于表示字体大小上。spdp概念相似。区别是Android在系统配置中定义一个scale值,spdp的换算关系是sp=dp*scale,通常scale值为1。(官方说法:默认情况下,sp单位与dp大小相同,但它会根据用户的首选文本大小来调整大小。)

Android把屏幕尺寸归为4类:

  • small:尺寸类似于低密度 VGA 屏幕的屏幕。小屏幕的最小布局尺寸约为 320x426 dp。例如,QVGA 低密度屏幕和 VGA 高密度屏幕。
  • normal:尺寸类似于中等密度 HVGA 屏幕的屏幕。标准屏幕的最小布局尺寸约为 320x470 dp。例如,WQVGA 低密度屏幕、HVGA 中等密度屏幕、WVGA 高密度屏幕。
  • large:尺寸类似于中等密度 VGA 屏幕的屏幕。大屏幕的最小布局尺寸约为 480x640 dp。例如,VGA 和 WVGA 中等密度屏幕。
  • xlarge:明显大于传统中等密度 HVGA 屏幕的屏幕。超大屏幕的最小布局尺寸约为 720x960 dp。在大多数情况下,屏幕超大的设备体积太大,不能放进口袋,最常见的是平板式设备。此项为 API 级别 9 中的新增配置。

Android把屏幕密度(dpi)分为:

  • ldpi:低密度屏幕;约为 120dpi。
  • mdpi:中等密度(传统 HVGA)屏幕;约为 160dpi。
  • hdpi:高密度屏幕;约为 240dpi。
  • xhdpi:超高密度屏幕;约为 320dpi。此项为 API 级别 8 中的新增配置
  • xxhdpi:绝高密度屏幕;约为 480dpi。此项为 API 级别 16 中的新增配置
  • xxxhdpi:极高密度屏幕使用(仅限启动器图标);约为 640dpi。此项为 API 级别 18 中的新增配置
  • nodpi:可用于您不希望为匹配设备密度而进行缩放的位图资源。
  • tvdpi:密度介于 mdpi 和 hdpi 之间的屏幕;约为 213dpi。此限定符并非指“基本”密度的屏幕。它主要用于电视,且大多数应用都不使用该密度 — 大多数应用只会使用 mdpi 和 hdpi 资源,而且系统将根据需要对这些资源进行缩放。
  • anydpi:此限定符适合所有屏幕密度,其优先级高于其他限定符。这非常适用于矢量可绘制对象。此项为 API 级别 21 中的新增配置

系统资源定义

对于下面的Layout资源定义:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
</LinearLayout>

上面的定义中有各种各样的符号,例如LinearLayoutlayout_widthorientation等,这些符号在哪里定义的呢?语法规则优势什么呢?

Android的资源系统并不想看上去那么简单,Android利用了xml定义了一套完整的资源语言,我们来具体看下

定义属性

Framework资源目录frameworks/base/core/res/res/values的目录下,有一个attrs.xml文件,在这个文件里,Android定义了资源的属性值,以LinearLayout为例,相关的定义如下:

<declare-styleable name="LinearLayout">
        <attr name="orientation" />
        <attr name="gravity" />
        <attr name="baselineAligned" format="boolean" />
        <attr name="baselineAlignedChildIndex" format="integer" min="0"/>
        <attr name="weightSum" format="float" />
        <attr name="measureWithLargestChild" format="boolean" />
        <attr name="divider" />
        <attr name="showDividers">
            <flag name="none" value="0" />
            <flag name="beginning" value="1" />
            <flag name="middle" value="2" />
            <flag name="end" value="4" />
        </attr>
        <attr name="dividerPadding" format="dimension" />
    </declare-styleable>

从文件中不难看出,LinearLayout中所使用的各种属性正是在这里定义的。不但定义了属性的名称,还有属性值的格式也做了定义

对于UI的基本元素widget而言,最重要的是获得预先定义好的各种属性值。于是在attrs.xml文件中,通过declare-styleable的方式定义了一套属性的集合。我们再看下LinearLayout类的构造方法:

public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        ......
        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);
        int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
        if (index >= 0) {
            setOrientation(index);
        }
        ......
        a.recycle();
    }
  • 在构造函数中,LinearLayout调用方法obtainStyledAttributes()来创建一个属性集合
  • a.getInt()用来获取属性
  • 第一个参数是com.android.internal.R.styleable.LinearLayout,正是attrs.xml文件中定义的属性集合名称
  • 还有一个参数是缺省值-1,如果在编写layout文件时必须给每个widget的所有属性都赋值,这会变得很繁琐,因此在读取每个属性时,代码中都会给出一个缺省值

给属性赋值

LinearLayout的构造方法中有一个defStyleAttrdefStyleRes用来指定使用的stylestyle就是一些预定义的属性值,例如:

<style name="Widget.Button">
        <item name="background">@drawable/btn_default</item>
        <item name="focusable">true</item>
        <item name="clickable">true</item>
        <item name="textAppearance">?attr/textAppearanceSmallInverse</item>
        <item name="textColor">@color/primary_text_light</item>
        <item name="gravity">center_vertical|center_horizontal</item>
    </style>

这段xml定义了一个名为Widget.Buttonstyle。使用style的方式是在layout定义中加入下面的语句:

style="@style/Widget.Button"

对于style的命名,为什么要用.分割呢?Android用这种方式表示一种继承关系。Widget.Button的含义是当前style继承了Widget的所有属性值

除了通过名字来表示继承关系外,还可以通过<style/>标签的parent属性来指定,如:

<style name="Widget.PopupMenu" parent="Widget.ListPopupWindow">
    </style>

主题Theme

theme就是所有UI属性的集合。作为基础定义的Theme本身是一个很庞大的style,而其他theme基本上都是通过Theme派生出来的,我们看下frameworks/base/core/res/res/values/themes.xml文件定义:

<style name="Theme">
        <item name="isLightTheme">false</item>
        <item name="colorForeground">@color/bright_foreground_dark</item>
        <item name="colorForegroundInverse">@color/bright_foreground_dark_inverse</item>
        ......
    </style>
    <!-- Variant of {@link #Theme} with no title bar -->
    <style name="Theme.NoTitleBar">
        <item name="windowNoTitle">true</item>
    </style>

资源类型

在Android应用的源码目录下,通常有两个和资源相关的目录:assetsres目录。这两个目录下的文件都会被打包进APK文件中

Android规定各类资源存放在res/目录,目录中支持的资源目录表如下:

目录

资源类型

animator

用于定义属性动画的 XML 文件。

anim

用于定义渐变动画的 XML 文件。(属性动画也可保存在此目录中,但为了区分这两种类型,属性动画首选 animator/ 目录。)

color

用于定义颜色状态列表的 XML 文件

drawable

位图文件(.png、.9.png、.jpg、.gif)或编译为以下可绘制对象资源子类型的 XML 文件:位图文件、九宫格(可调整大小的位图)、状态列表、形状、动画可绘制对象、其他可绘制对象

mipmap

适用于不同启动器图标密度的可绘制对象文件

layout

用于定义用户界面布局的 XML 文件

menu

用于定义应用菜单(如选项菜单、上下文菜单或子菜单)的 XML 文件

raw

需以原始形式保存的任意文件。如要使用原始 InputStream 打开这些资源,请使用资源 ID(即 R.raw.filename)调用 Resources.openRawResource()。但是,如需访问原始文件名和文件层次结构,则可以考虑将某些资源保存在 assets/ 目录(而非 res/raw/)下。assets/ 中的文件没有资源 ID,因此您只能使用 AssetManager 读取这些文件。

values

包含字符串、整型数和颜色等简单值的 XML 文件。

xml

可在运行时通过调用 Resources.getXML() 读取的任意 XML 文件。各种 XML 配置文件(如可搜索配置)都必须保存在此处。

font

带有扩展名的字体文件(如 .ttf.otf.ttc),或包含 <font-family> 元素的 XML 文件。

对于上面的animatoranimcolordrawablelayoutmenurawvaluesxml的9种目录下存放的是缺省资源,每种资源都可以有候选资源候选资源的存放格式类似<resources_name>-<qualifier>的目录下:

  • <resources_name>就是前面的这9种缺省资源的目录名
  • <qualifier>是一些限定符的组合,用来区分候选资源类型。

assets目录

assets目录下保存的文件不能通过存取资源的方式在代码中访问到。访问assets目录下文件的方式更像是打开一个文件,例如:

AssetManager am = Context.getAssets();
InputStream is = am.open(filePath);

assets目录下可以再建目录,没有限制,目录下存放的文件在编译过程中不会被改动,会被原封不动的打包进APK中。

raw目录

res目录下有个raw目录,放置在该目录下的文件也不会被Android改动,但是raw目录下不能在创建子目录。

访问raw目录下资源的方式和访问其他资源是一致的:

InputStream is = getResources().openRawResource(R.id.filename);

同时,raw目录下的资源也能像其它资源一样有备选资源,能够被overlay目录中的资源覆盖掉,这也是assets目录下的文件不具备的特性。

Android 资源管理的实现原理

上面我们了解到Android中的资源是如此的纷繁复杂,那么

  • Android是如何加载、管理资源的呢?
  • 我们知道Android应用可以使用的资源有3中来源:本apk、系统Framework、其他apk,Android如何统一管理控制呢?
  • 不同apk间的资源包如何共享呢?
  • 对于资源加载,反复加载会浪费内存吗?资源加载会有效率问题吗?

关于这几个问题的答案,我们需要从原理上来寻找!

Resources类的作用

Android资源的装载是通过Resources类来完成的。

还记的Zygote进程的预加载么?
就是通过Resources.getSystem().startPreloading();来实现的,我们来看看Resources这个类到底做了什么

我们先看下核心的成员变量:

class Resources{
    // Resources的实例对象
    static Resources mSystem = null;
    // 真正实现类ResourcesImpl的实例对象
    private ResourcesImpl mResourcesImpl;
}
class ResourcesImpl{
    // 预加载相关资源
    private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables;
    private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables
            = new LongSparseArray<>();
    private static final LongSparseArray<android.content.res.ConstantState<ComplexColor>>
            sPreloadedComplexColors = new LongSparseArray<>();
    // Drawable 和 ColorStateList 的缓存
    private final DrawableCache mDrawableCache = new DrawableCache();
    private final DrawableCache mColorDrawableCache = new DrawableCache();
    // XML文件的缓存 XML_BLOCK_CACHE_SIZE=4,写死的
    private int mLastCachedXmlBlockIndex = -1;
    private final int[] mCachedXmlBlockCookies = new int[XML_BLOCK_CACHE_SIZE];
    private final String[] mCachedXmlBlockFiles = new String[XML_BLOCK_CACHE_SIZE];
    private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[XML_BLOCK_CACHE_SIZE];
    // AssetManager实例的
    final AssetManager mAssets;
}

上面的代码中,Resources类有两个重要的变量:

  • Resources mSystem
  • Resources类的实例对象,用来管理系统资源
  • ResourcesImpl mResourcesImpl
  • 9.0版本的源码中,Resources类具体业务交给了ResourcesImpl来实现

我们下面具体来看下

mSystem的初始化

mSystem是一个引用Resources类自身实例的静态变量。我们看下mSystem变量是如何初始化的:

public static Resources getSystem() {
        synchronized (sSync) {
            Resources ret = mSystem;
            if (ret == null) {
                ret = new Resources();
                mSystem = ret;
            }
            return ret;
        }
    }

静态方法getSystemmSystem为空的情况下会创建一个Resources对象的实例。我们看下这个无参构造:

/**
     * class:Resources.java
     * Only for creating the System resources.
     */
    private Resources() {
        mClassLoader = ClassLoader.getSystemClassLoader();
        final DisplayMetrics metrics = new DisplayMetrics();
        metrics.setToDefaults();
        final Configuration config = new Configuration();
        config.setToDefaults();
        mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
                new DisplayAdjustments());
    }
    /**
     * class:ResourcesImpl.java
     */
    public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
            @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
        mAssets = assets;
        mMetrics.setToDefaults();
        mDisplayAdjustments = displayAdjustments;
        mConfiguration.setToDefaults();
        updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
    }

这个两个构造方法比较简单,我们需要重点关注的是

  • Resource类私有构造方法的注释:Only for creating the System resources.
  • 在创建ResourcesImpl实例时传入的AssetManager对象,是通过静态方法AssetManager.getSystem()来获取的
  • 这里暂不深究,后面会单独讲解AssetManager类哈

我们接下来就来找找Resource.getSystem()方法最早是在什么地方调用的?

大家在frameworks目录下grep一下就会发现,最早的函数调用其实也就是在Zygote进程初始化时:

/**
 * preloadResources 函数调用关系展示
 */
class ZygoteInit{
    public static void main(String argv[]) {
        preload();
    }
    static void preload(...) {
        preloadResources();
    }
    private static void preloadResources() {
        mResources = Resources.getSystem();
        mResources.startPreloading();
        ......
        preloadDrawables(ar);
        ......
        preloadColorStateLists(ar);
        ......
        mResources.finishPreloading();
    }
    private static int preloadDrawables() {
        ......
        mResources.getDrawable(id, null);
        ......
    }
    private static int preloadColorStateLists() {
        ......
        mResources.getColorStateList(id, null);
        ......
    }
}

由此,在Zygote进程初始化时mSystem就被赋值了。我们知道,所有的应用程序都是从Zygote进程fork而来的,这意味着所有进程中的mSystem引用的Resource实例在整个Android系统中是共享的,这部分其实也就是Framework的资源

getDrawable的过程

ZygotepreloadResources方法中得到系统的Resources实例后,会调用这个实例的getDrawablegetColorStateList方法去加载系统的Drawable资源和ColorStateList资源。

getDrawable(id,null)方法为例,我们来看下具体干了啥:

public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme){
        // 请注意此处传入的 density=0
        return getDrawableForDensity(id, 0, theme);
    }
    public Drawable getDrawableForDensity(...) {
        return mResourcesImpl.loadDrawable(this, value, id, density, theme);
    }

最后还是调用了具体实现类ResourcesImplloadDrawable方法,我们来看下整体流程:

Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
            int density, @Nullable Resources.Theme theme)
            throws NotFoundException {
        // getDrawable 传入的参数density = 0
        // 所以对于Zygote来说,useCache始终为true
        final boolean useCache = density == 0 || value.density == mMetrics.densityDpi;
        try {
            final boolean isColorDrawable;
            final DrawableCache caches;
            final long key;
            // 判断要加载的 Drawable 类型,普通资源还是Color
            if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                    && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
                isColorDrawable = true;
                caches = mColorDrawableCache;
                key = value.data;
            } else {
                isColorDrawable = false;
                caches = mDrawableCache;
                key = (((long) value.assetCookie) << 32) | value.data;
            }
            // 非预加载阶段才会走到这里
            // 检查要加载的资源是否已经存在于cache中
            // 有的话直接返回,没有找到继续检查预加载相关的cache
            if (!mPreloading && useCache) {
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    ......
                    return cachedDrawable;
                }
            }
            ......
            // 检查预加载相关的cache
            // ConstantState这是一个抽象类
            // 每个Drawable的子类都要实现它,可以用来生成对应的Drawable实例
            // 很经典的一个类
            final Drawable.ConstantState cs;
            if (isColorDrawable) {
                cs = sPreloadedColorDrawables.get(key);
            } else {
                cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
            }
            ......
            Drawable dr;
            boolean needsNewDrawableAfterCache = false;
            if (cs != null) {
                // 在预加载cache中找到资源
                // 生成对应的Drawable实例
                dr = cs.newDrawable(wrapper);
            } else if (isColorDrawable) {
                // 预加载cache中没有找到资源
                // 直接创建Color类的资源,不是我们这次分析的重点哈
                dr = new ColorDrawable(value.data);
            } else {
                // 预加载cache中没有找到资源 
                // 通过 loadDrawableForCookie 从XML中加载资源
                dr = loadDrawableForCookie(wrapper, value, id, density);
            }
            ......
            // 对Drawable实例进行简单的配置工作
            if (dr != null) {
                dr.setChangingConfigurations(value.changingConfigurations);
                if (useCache) {
                    // 通过 cacheDrawable 进行资源缓存
                    cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
                    ......
                }
            }
            return dr;
        } catch (Exception e) {
        }
    }

从上面代码看,当缓存中没有找到需要的Drawable资源,会通过loadDrawableForCookie来加载资源,最后根据需要把资源缓存到cache中。

先来看下loadDrawableForCookie的核心代码如下:

if (file.endsWith(".xml")) {
        // 加载XML类型的drawable资源
        final XmlResourceParser rp = loadXmlResourceParser(
            file, id, value.assetCookie, "drawable");
        dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
        rp.close();
    } else {
        // 加载图片类型的drawable资源
        final InputStream is = mAssets.openNonAsset(
            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
        AssetInputStream ais = (AssetInputStream) is;
        dr = decodeImageDrawable(ais, wrapper, value);
    }
    return dr;

资源装载分成了xmlimage两种:

  • xml:通过loadXmlResourceParser()来加载,流程主要分为两部分:
  • 检查缓存中是否已经存在:
// First see if this block is in our cache.
final int num = cachedXmlBlockFiles.length;
for (int i = 0; i < num; i++) {
    if (cachedXmlBlockCookies[i] == assetCookie 
    && cachedXmlBlockFiles[i] != null
    && cachedXmlBlockFiles[i].equals(file)) {
        return cachedXmlBlocks[i].newParser();
    }
}
  • 缓存中不存在,通过AssetManager来装载文件并缓存
// Not in the cache, create a new block and put it at
// the next slot in the cache.
final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
if (block != null) {
    final int pos = (mLastCachedXmlBlockIndex + 1) % num;
    mLastCachedXmlBlockIndex = pos;
    final XmlBlock oldBlock = cachedXmlBlocks[pos];
    if (oldBlock != null) {
        oldBlock.close();
    }
    cachedXmlBlockCookies[pos] = assetCookie;
    cachedXmlBlockFiles[pos] = file;
    cachedXmlBlocks[pos] = block;
    return block.newParser();
}
  • image:通过mAssets.openNonAsset来加载二进制流,并通过decodeImageDrawable()来转化为相应的Drawable对象

到这里,对于Resource类的作用我们就梳理的差不多了,主要的功能是缓存DrawableColorStateListXML文件等资源,而具体的加载细节,都和一个叫AssetManager的类有关系。

AssetManager等下再看,我们可以先总结一下:

  • 代表系统Framework资源的Resources实例系统只有一份,通过Resources类中的静态成员变量mSystem引用。
  • 系统中预加载的Drawable资源和ColorStateList资源也是通过Resources类中的静态成员变量引用。并且在整个系统中共享,不会浪费内存
  • 应用中装载的DrawableColorStateListXML资源在Resources中会被缓存,因此反复装载这些资源的操作不会真正的重复加载,不会浪费额外的内存

AssetManager类的作用

AssetManager类是Android中实际管理资源的模块,在Java层native层都有代码。我们先看Java层AssetManager

Java层的AssetManager类

我们先看下AssetManager类中的变量:

// Not private for LayoutLib's BridgeAssetManager.
    @UnsupportedAppUsage
    @GuardedBy("sSync") static AssetManager sSystem = null;
    @GuardedBy("sSync") private static ApkAssets[] sSystemApkAssets = new ApkAssets[0];
    @GuardedBy("sSync") private static ArraySet<ApkAssets> sSystemApkAssetsSet;
    
    // Pointer to native implementation, stuffed inside a long.
    @UnsupportedAppUsage
    @GuardedBy("this") private long mObject;
    // The loaded asset paths.
    @GuardedBy("this") private ApkAssets[] mApkAssets;
  • sSystem是和Resources类中mSystem对应的AssetManager对象,用来管理系统Framewrok的资源
  • 大家可以回顾上面的 mSystem的初始化 部分
  • sSystemApkAssetssSystemApkAssetsSet都是和系统资源相关的ApkAssets的集合,在Zygote初始化时就已经创建完成
  • 有没有很好奇ApkAssets到底是啥?
  • 官方说明如下:
/**
* The loaded, immutable, in-memory representation of an APK.
*
* The main implementation is native C++ and there is very little API surface exposed here. The APK
* is mainly accessed via {@link AssetManager}.
*
* Since the ApkAssets instance is immutable, it can be reused and shared across AssetManagers,
* making the creation of AssetManagers very cheap.
* @hide
*/
public final class ApkAssets {
        @GuardedBy("this") private final long mNativePtr;
        @GuardedBy("this") private final StringBlock mStringBlock;
}
private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
    throws IOException {
    mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
    mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
}
public static @NonNull ApkAssets loadFromPath(@NonNull String path) throws IOException {
    return new ApkAssets(path, false /*system*/, false /*forceSharedLib*/, false /*overlay*/);
}
  • ApkAssets主要是与native层进行联系,通过各种native调用加载相关资源
  • ApkAssets还有一个功能是把native层加载字符串资源保存到mStringBlock
  • sSystemApkAssets是一个ApkAssets的数组,通常一个ApkAssets管理的是一个资源包中的资源,用数组的话说明可能会管理多个资源包
  • mObject:用来存储在native层创建的AssetManager对象指针
  • mApkAssets:记录所有已经加载的asset资源,也包括sSystemApkAssets
  • 相关的实现过程可以参照AssetManagersetApkAssets()函数

还记得Resources初始化时调用的AssetManager.getSystem()函数么?我们来看下具体干了些啥?

注释比较详细:

private AssetManager(boolean sentinel) {
        // nativeCreate()的作用是创建一个native层的AssetManager实例,并返回对象指针
        // 从9.0的源码看,创建的是AssetManager2的实例
        mObject = nativeCreate();
    }
    public static AssetManager getSystem() {
        synchronized (sSync) {
            createSystemAssetsInZygoteLocked();
            return sSystem;
        }
    }
    private static void createSystemAssetsInZygoteLocked() {
        if (sSystem != null) {
            return;
        }
        final ArrayList<ApkAssets> apkAssets = new ArrayList<>();
        // 通过 ApkAssets 的 loadFromPath 来加载 Framework 资源
        // 最终是通过 nativeLoad() native函数来完成
        apkAssets.add(ApkAssets.loadFromPath("/system/framework/frmework-res.apk", true /*system*/));
        // 省略一些资源的额外操作
        ......
        // 分别赋值给 sSystemApkAssetsSet 和 sSystemApkAssets 集合
        sSystemApkAssetsSet = new ArraySet<>(apkAssets);
        sSystemApkAssets = apkAssets.toArray(newApkAssets[apkAssets.size()]);
        // 创建系统专用的AssetManager实例
        // 最终调用的是 nativeCreate 函数
        sSystem = new AssetManager(true /*sentinel*/);
        // 设置资源到已加载集合,并赋值给mApkAssets变量
        sSystem.setApkAssets(sSystemApkAssets, false /*invalidateCaches*/);
    }

关于AssetManager的创建我们比较清晰了,我们会发现大多数的过程都是通过native调用来完成的,这也是Java层AssetManager的主要作用:担当通过native层的桥梁

我们以ResourcesgetString()方法,来追踪下调用过程:

// Resources.java
    public String getString(@StringRes int id) throws NotFoundException {
        return getText(id).toString();
    }
    public CharSequence getText(@StringRes int id) throws NotFoundException {
        // 调用的AssetManager的getResourceText函数
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        ......
    }
    // AssetManager.java
    CharSequence getResourceText(@StringRes int resId) {
        synchronized (this) {
            // 通过 TypedValue 对象来接收转换数据
            final TypedValue outValue = mValue;
            if (getResourceValue(resId, 0, outValue, true)) {
                return outValue.coerceToString();
            }
            return null;
        }
    }
    boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
            boolean resolveRefs) {
        Preconditions.checkNotNull(outValue, "outValue");
        synchronized (this) {
            //通过 nativeGetResourceValue 函数查找resId资源对应ApkAssets的cookie值
            //并把相关数据更新到TypedValue对象中
            //资源cookie值映射的是Java层的已加载资源mApkAssets集合的index值
            final int cookie = nativeGetResourceValue(
                    mObject, resId, (short) densityDpi, outValue, resolveRefs);
            if (cookie <= 0) {
                return false;
            }
            ......
            if (outValue.type == TypedValue.TYPE_STRING) {
                // 当数值类型判断正确后
                // 通过ApkAssets对象的getStringFromPool读取数据
                // 需要注意的是outValue.data
                // 它表示字符串在对应ApkAssets对象中的mStringBlock数组的index值
                outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
            }
            return true;
        }
    }
    // ApkAssets.java
    CharSequence getStringFromPool(int idx) {
        synchronized (this) {
            // 从mStringBlock获取字符串数据
            return mStringBlock.get(idx);
        }
    }

调用过程很清晰了哈!

可以看出AssetManager的功能实现依赖了很多的native函数,接下来我们就看看Androidnative层是怎么设计的吧!

native层的AssetManager

从上面我们知道,native层的AssetManager才是真正装载资源的部分。

Android 资源管理这部分是Google爸爸折腾最多的地方,对比9.05.0的源码真滴是天差地别啊

5.0的书籍已经用不上了,差别太大。。。。。不过,从上面Java层的知识我们已经知道:

  • 装载资源(就是加载framwork-res.apk)的入口是ApkAssets.loadFromPath()
  • 最后将资源整合到体系中的函数是AssetManager.setApkAssets()

我们就先从这两个函数入手看看

通过ApkAssets装载资源

ApkAssets.loadFromPath调用的是ApkAssets的私有构造函数:

// ApkAssets.java
private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
    throws IOException {
    mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
    mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
}

ApkAssets的私有构造函数通过nativeLoad()本地调用来实现真正的资源加载

nativeLoad()函数调用示意:

// android_content_res_ApkAssets.cpp
static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system, jboolean force_shared_lib, jboolean overlay) {
    .......
    // 调用了 native层的 ApkAssets 的 Load 函数
    apk_assets = ApkAssets::Load(path.c_str(), system);
    ......
    return reinterpret_cast<jlong>(apk_assets.release());
}
// ApkAssets.cpp
std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path, bool system) {
    // 奇怪的C++智能指针
    // 真正进行加载的是 LoadImpl() 函数
    return LoadImpl({} /*fd*/, path, nullptr, nullptr, system, false /*load_as_shared_library*/);
}

nativeLoad()函数调用了nativeApkAssets类的LoadImpl函数。从这个函数开始,就开始对resources.arsc文件进行操作了,所以,我们先配上一幅结构图再一步一步的看函数实现

描述资源的resources.arsc文件

图片来源于大神LinJW博客,受益匪浅:

android px和dp的转换 安卓dp是什么意思_android

resources.arsc是以一个个Chunk块的形式组织的,Chunk的头部信息记录了这个Chunk的类型、长度等数据。从整体上来看,其结构为:资源索引表头部+字符串资源池+N个Package数据块

简单了解即可,让我们从LoadImpl函数开始跟踪吧

ApkAssets::LoadImpl()函数
std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl(......) {
    // 一个用来指向解压后的资源文件的指针
    ::ZipArchiveHandle unmanaged_handle;
    // 解压资源文件
    ::OpenArchive(path.c_str(), &unmanaged_handle);
    // 将解压后的资源文件包装成ApkAssets对象
    // Wrap the handle in a unique_ptr so it gets automatically closed.
    std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(unmanaged_handle, path));
    
    // resources.arsc 就是我们apk中的资源索引表
    ::ZipString entry_name("resources.arsc");
    ::ZipEntry entry;
    // 从解压后的apk资源中查找 resources.arsc
    result = ::FindEntry(loaded_apk->zip_handle_.get(), entry_name, &entry);
    if (result != 0) {
        // There is no resources.arsc, so create an empty LoadedArsc and return.
        loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty();
        // 对于一个Java coder,std::move真的是一个SAO操作
        return std::move(loaded_apk);
    }
    // Open the resource table via mmap unless it is compressed.
    // This logic is taken care of by Open.
    // 通过Open函数加载ARSC文件到resources_asset_,英文注释也就是方法的执行逻辑了
    loaded_apk->resources_asset_ = loaded_apk->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER);
    // Must retain ownership of the IDMAP Asset so that all pointers to its mmapped data remain valid.
    // 其实这行代码主要是给loadOverlay情况用的,其他情况下都相当于赋值为null
    // loadOverlay中已经包含了一个idmap,所以在这里需要保留下来原来的
    loaded_apk->idmap_asset_ = std::move(idmap_asset);
    // 将resources_asset_(加载完成的ASRC数据)打包成StringPiece对象
    const StringPiece data(reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)),loaded_apk->resources_asset_->getLength());
    // 调用 LoadedArsc 的 Load 函数解析转化为资源数据,我们等下重点看下
    loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library);
    ......
    // Need to force a move for mingw32.
    return std::move(loaded_apk);
}

上面的代码注释比较详细哈,不再赘述。接下来重点关注下LoadedArsc::Load()函数。

LoadedArsc::Load()函数解析resources.arsc
std::unique_ptr<const LoadedArsc> LoadedArsc::Load(const StringPiece& data,
                                                   const LoadedIdmap* loaded_idmap, bool system,
                                                   bool load_as_shared_library) {
  // Not using make_unique because the constructor is private.
  // 创建 LoadedArsc 实例
  std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc());
  loaded_arsc->system_ = system;
  // 遍历StringPiece对象(也就是加载好的arsc)
  ChunkIterator iter(data.data(), data.size());
  while (iter.HasNext()) {
    const Chunk chunk = iter.Next();
    switch (chunk.type()) {
        case RES_TABLE_TYPE:
            // 如果数据类型是 RES_TABLE_TYPE 
            // 通过 LoadTable 将对应资源加载到 table 中
            if (!loaded_arsc->LoadTable(chunk, loaded_idmap, load_as_shared_library)) {
                return {};
            }
        break;
    }
  }
  return std::move(loaded_arsc);
}

从上面可以看到,资源的加载处理调用到了LoadTable()函数,我们来看下

LoadedArsc::LoadTable()函数解析resources.arsc
bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap,
                           bool load_as_shared_library) {
  const ResTable_header* header = chunk.header<ResTable_header>();
  ......
  ChunkIterator iter(chunk.data_ptr(), chunk.data_size());
  while (iter.HasNext()) {
    const Chunk child_chunk = iter.Next();
    switch (child_chunk.type()) {
      case RES_STRING_POOL_TYPE:
        // Only use the first string pool. Ignore others.
        // 如果是字符常量池类型的资源
        // 数据保存到global_string_pool_中,这是全局字符串
        // Java层ApkAssets中的mStringBlock指向的就是这部分数据
        status_t err = global_string_pool_.setTo(child_chunk.header<ResStringPool_header>(), child_chunk.size());
        break;
      case RES_TABLE_PACKAGE_TYPE: {
        // 如果是pacakge类型的资源
        // 使用 LoadedPackage::Load() 进行加载
        ......
        std::unique_ptr<const LoadedPackage> loaded_package =
            LoadedPackage::Load(child_chunk, loaded_idmap, system_, load_as_shared_library);
        packages_.push_back(std::move(loaded_package));
      } break;
    }
  }
  ......
  return true;
}

从上面的代码看,字符串资源放到了global_string_pool_中,对于RES_TABLE_PACKAGE_TYPE类型的资源通过LoadedPackage::Load()函数

LoadedPackage::Load()函数解析resources.arsc
std::unique_ptr<const LoadedPackage> LoadedPackage::Load(......) {
  // 创建LoadedPackage实例
  std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage());
  ......
  // 设置是否为系统资源
  loaded_package->system_ = system;
  // 根据packageID 判断是否为共享资源
  loaded_package->package_id_ = dtohl(header->id);
  if (loaded_package->package_id_ == 0 ||
      (loaded_package->package_id_ == kAppPackageId && load_as_shared_library)) {
    // Package ID of 0 means this is a shared library.
    loaded_package->dynamic_ = true;
  }
  ......
  while (iter.HasNext()) {
    const Chunk child_chunk = iter.Next();
    switch (child_chunk.type()) {
        case RES_STRING_POOL_TYPE: 
            // 这里会解析两种字符串池类型
            // 一种是资源类型字符串池,比如:ainim、attr、bool、color等
            // 另一种是资源项名称字符串池,比如:app_name、activity_main等
            ......
        case RES_TABLE_TYPE_SPEC_TYPE: 
            ......
        case RES_TABLE_TYPE_TYPE: 
            ......
        case RES_TABLE_LIBRARY_TYPE: 
            const ResTable_lib_entry* const entry_end = entry_begin + dtohl(lib->count);
            for (auto entry_iter = entry_begin; entry_iter != entry_end; ++entry_iter) {
                ......
                // 添加package_name和packageId到loaded_package的dynamic_package_map_中
                loaded_package->dynamic_package_map_.emplace_back(std::move(package_name),dtohl(entry_iter->packageId));
            }
            ......
    }
  }
  ......
  return std::move(loaded_package);
}

这部分函数的核心就是把chunk按类型进行解析处理,然后返回解析完成的loaded_package指针。

到这里一个res.apk的装载过程基本就完成了,过程中忽略了很多细节,不要着急哈,让我们先搞清整体流程。

资源装载的整体流程

让我们从ApkAssets开始来梳理下流程:

  • 调用时序图:
  • 按照上图的调用流程走完
  • LoadedArsc对象中的global_string_poolpackage相关的资源已经完成加载
  • ApkAssets对象中的resource_asset_loaded_arsc_也加载完成
  • 关系类图:

这样,一个res.apk的装载过程我们清楚了,但是AssetManager是怎么组织它的呢?

我们来看下AssetManager.setApkAssets()函数

通过AssetManager.setApkAssets()添加资源

我们先看AssetManager.setApkAssets()函数:

public void setApkAssets(@NonNull ApkAssets[] apkAssets, boolean invalidateCaches) {
        ......
        ApkAssets[] newApkAssets = new ApkAssets[sSystemApkAssets.length + apkAssets.length];
        // Copy the system assets first.
        System.arraycopy(sSystemApkAssets, 0, newApkAssets, 0, sSystemApkAssets.length);
        // Copy the given ApkAssets if they are not already in the system list.
        int newLength = sSystemApkAssets.length;
        for (ApkAssets apkAsset : apkAssets) {
            if (!sSystemApkAssetsSet.contains(apkAsset)) {
                newApkAssets[newLength++] = apkAsset;
            }
        }
        ......
        mApkAssets = newApkAssets;
        nativeSetApkAssets(mObject, mApkAssets, invalidateCaches);
    }

Java部分比较简单:

  • setApkAssets()会先把sSystemApkAssets中的资源拷贝到一个全新数组newApkAssets
  • 然后检查sSystemApkAssets没有的资源,并添加到数组newApkAssets
  • 更新已加载资源集合mApkAssets
  • 调用nativeSetApkAssets处理native层的资源

我们看下NativeSetApkAssets函数做了啥:

static void NativeSetApkAssets(JNIEnv* env, jclass /*clazz*/, jlong ptr,
                               jobjectArray apk_assets_array, jboolean invalidate_caches) {
    // 根据Java层传递的数值长度,创建对应的ApkAssets集合
    const jsize apk_assets_len = env->GetArrayLength(apk_assets_array);
    std::vector<const ApkAssets*> apk_assets;
    apk_assets.reserve(apk_assets_len);
    // 将 Java 层保存的ApkAssets指针取出并添加到集合中
    for (jsize i = 0; i < apk_assets_len; i++) {
        jobject obj = env->GetObjectArrayElement(apk_assets_array, i);
        jlong apk_assets_native_ptr = env->GetLongField(obj, gApkAssetsFields.native_ptr);
        apk_assets.push_back(reinterpret_cast<const ApkAssets*>(apk_assets_native_ptr));
    }
    ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
    // 调用 AssetManager2 的 SetApkAssets 函数
    assetmanager->SetApkAssets(apk_assets, invalidate_caches);
}

NativeSetApkAssets()函数对Java层的数据进行简单处理后,调用了AssetManager2SetApkAssets函数,函数如下:

bool AssetManager2::SetApkAssets(const std::vector<const ApkAssets*>& apk_assets,
                                 bool invalidate_caches) {
  // 将ApkAssets赋值给apk_assets_
  apk_assets_ = apk_assets;
  // 构建动态资源引用表
  BuildDynamicRefTable();
  // 重新构建符合当前设备的资源,即 filtered_configs_
  RebuildFilterList();
  ......
  return true;
}

真的简洁。。。。。。核心函数也很突出,为了方便理解接下来的BuildDynamicRefTable()函数,我们先来看AssetManager2类的相关定义

AssetManager2的关键定义
class AssetManager2 {
    //用来存储该AssetManager2已经加载的所有APK包
    std::vector<const ApkAssets*> apk_assets_;
    /**
     * 用来将apk_assets_分组,主要还是用来处理Runtime Resources Overlay的
     */
    std::vector<PackageGroup> package_groups_;
    /**
     * 它的key表示APK包也就是ApkAssets的id
     * 它的value表示APK包也就是ApkAssets所在的PackageGroup在package_groups_中的索引
     */
    std::array<uint8_t, std::numeric_limits<uint8_t>::max() + 1> package_ids_;
    //表示设备当前的配置信息
    ResTable_config configuration_;
    /**
     * 用来缓存资源的Bag
     * 它的key表示一个资源的id,比如一个style,一个array
     * 它的value 表示已经从resources.arsc中解析出来了的,该资源的所有Bag
     */
    std::unordered_map<uint32_t, util::unique_cptr<ResolvedBag>> cached_bags_;
}
  // A collection of configurations and their associated ResTable_type that match the current
  // AssetManager configuration.
  struct FilteredConfigGroup {
    // 该ResTable_typeSpec中符合设备当前配置的所有的config
    std::vector<ResTable_config> configurations;
    // 该ResTable_typeSpec中符合设备当前配置的所有的ResTable_type
    std::vector<const ResTable_type*> types;
  };

  // Represents an single package.
  struct ConfiguredPackage {
    // A pointer to the immutable, loaded package info.
    const LoadedPackage* loaded_package_;
    // 我们在获取资源的时候,要根据设备的当前配置信息,去选择最合适的资源项
    // 这个过程要经过match、isBetterThan、isMoreSpecificThan 等比较的过程
    // 现在为了加快获取资源的速度,在加载完资源后,系统就会先选出匹配设备当前配置的资源存放在filtered_configs_中。
    // 当我们获取资源的时候,就可以节省筛选步骤
    // filtered_configs_中的每一项代表一个ResTable_typeSpec中符合设备当前配置的所有ResTable_type
    ByteBucketArray<FilteredConfigGroup> filtered_configs_;
  };
  
  using ApkAssetsCookie = int32_t;
  // Represents a logical package, which can be made up of many individual packages. Each package
  // in a PackageGroup shares the same package name and package ID.
  // 大家留意上面的注释
  struct PackageGroup {
    // 相同loaded_package name的集合,包括target package和overlay package
    // 如果一个Package没有overlay package,那么它应该独占一个PackageGroup
    std::vector<ConfiguredPackage> packages_;

    // The cookies associated with each package in the group. They share the same order as
    // packages_.
    // 表示 ConfiguredPackage 所代表的 ApkAssets 在 AssetManager2 的 apk_assets_ 集合中的位置
    // cookies_集合的存放顺序与packages_的顺序一一对应
    std::vector<ApkAssetsCookie> cookies_;

    // A library reference table that contains build-package ID to runtime-package ID mappings.
    // 一个用来描述资源共享库的编译时id和运行时id的映射关系表
    DynamicRefTable dynamic_ref_table;
  };

结合上面的定义,我们来看下BuildDynamicRefTable()干了啥

BuildDynamicRefTable()构建动态资源引用表
void AssetManager2::BuildDynamicRefTable() {
  // 简单的初始化操作
  package_groups_.clear();
  package_ids_.fill(0xff);

  // 0x01 is reserved for the android package.
  int next_package_id = 0x02;
  const size_t apk_assets_count = apk_assets_.size();
  // 遍历处理apk_assets_集合
  for (size_t i = 0; i < apk_assets_count; i++) {
    // 获取已经解析加载完成的arsc资源,即loaded_arsc
    const LoadedArsc* loaded_arsc = apk_assets_[i]->GetLoadedArsc();
    // 从loaded_arsc中取出解析完成的package资源,即loaded_package
    for (const std::unique_ptr<const LoadedPackage>& package : loaded_arsc->GetPackages()) {
      // Get the package ID or assign one if a shared library.
      int package_id;
      if (package->IsDynamic()) {
        // 如果是共享资源,分配一个特殊的package id
        package_id = next_package_id++;
      } else {
        // 不是共享资源,使用原有的package id
        package_id = package->GetPackageId();
      }
      // Add the mapping for package ID to index if not present.
      uint8_t idx = package_ids_[package_id];
      if (idx == 0xff) {
        // 0xff 说明package_ids还未记录
        package_ids_[package_id] = idx = static_cast<uint8_t>(package_groups_.size());
        // 先向package_groups_的尾部添加一个新的PackageGroup对象
        package_groups_.push_back({});
        // 然后获取对象动态资源引用表的指针
        DynamicRefTable& ref_table = package_groups_.back().dynamic_ref_table;
        // 赋值 package_id,干啥用的后面看看再说
        ref_table.mAssignedPackageId = package_id;
        // 设置当前是否为共享资源,常量值 0x7F 值得研究研究
        ref_table.mAppAsLib = package->IsDynamic() && package->GetPackageId() == 0x7f;
      }
      // 取出PackageGroup对象,开始进行数据填充
      PackageGroup* package_group = &package_groups_[idx];

      // Add the package and to the set of packages with the same ID.
      // 设置 packages_ 和 cookies_
      package_group->packages_.push_back(ConfiguredPackage{package.get(), {}});
      // 留意下这个变量i,其实就是当前ApkAsset在apk_assets_中的数组下标
      package_group->cookies_.push_back(static_cast<ApkAssetsCookie>(i));
      // Add the package name -> build time ID mappings.
      for (const DynamicPackageEntry& entry : package->GetDynamicPackageMap()) {
        // 解析loaded_package中的 dynamic_package_map_ 一个packageName:packageID格式的集合
        String16 package_name(entry.package_name.c_str(), entry.package_name.size());
        // 并设置到 package_group 中的动态资源引用表中
        package_group->dynamic_ref_table.mEntries.replaceValueFor(
            package_name, static_cast<uint8_t>(entry.package_id));
      }
    }
  }
  // 到这里,ApkAssets集合中的数据就被解析的差不多了
  // package_groups_和package_ids_集合中的数据也被填充的差不多了
  // Now assign the runtime IDs so that we have a build-time to runtime ID map.
  // 接下来的这部分是将资源的 Build ID 与 runtime ID 关联起来
  // 还记得前面生成的 mAssignedPackageId 么,在这里就起到作用了
  const auto package_groups_end = package_groups_.end();
  for (auto iter = package_groups_.begin(); iter != package_groups_end; ++iter) {
    const std::string& package_name = iter->packages_[0].loaded_package_->GetPackageName();
    for (auto iter2 = package_groups_.begin(); iter2 != package_groups_end; ++iter2) {
        // 跟踪addMapping函数就会发现,这部分的操作是:
        // 根据 package_name 查找的 build packageID
        // 然后把 build packageID 作为索引,关联runtimeID,也就是 mAssignedPackageId
      iter2->dynamic_ref_table.addMapping(package_name, iter->dynamic_ref_table.mAssignedPackageId);
    }
  }
}

BuildDynamicRefTable()函数基本上把AssetManager2定义的关键数据都填充完了,整体流程注释的比较详细,不再补充啦

这里还是要吐槽一下,Android资源相关的struct嵌套有点多,短时间理解起来还是有难度的。好在资源加载部分的业务逻辑应该很少遇到改动需求,庆幸一下下先

到这里整个framework资源的加载流程就差不多完成了,我们简单总结下

AssetManager加载过程的简单总结

资源管理部分还有比较重要的一个知识点是RRO,就不在这里介绍了,AssetManager2中的很多数据定义都是为了RRO而设计的,需要的话可以先从Google官网-RRO了解先

我们还是先通过调用时序图来看下资源加载过程,从AssetManager.getSystem()开始,流程如下:

android px和dp的转换 安卓dp是什么意思_android px和dp的转换_02

资源加载完后的整体结构,如下图:

android px和dp的转换 安卓dp是什么意思_AssetManager_03

看完这两张图后思路有木有更清晰了呢,哈哈哈

AssetManager的资源查找

了解了AssetManager的资源加载过程,查找过程就比较容易了,AssetManager的资源查找入口可以简单分为两种:

  • open方式:像open()openNonAsset()
  • getResource方式:像getResourceText()getResourceArray()

我们来简单看下

open方式

对于open类的接口,基本上处理的都是一些xml或者文件形式的数据,从函数调用来看,最后都走到了native层的OpenNonAsset函数,这部分处理比较简单,源码如下:

std::unique_ptr<Asset> AssetManager2::OpenNonAsset(const std::string& filename,
                                                   Asset::AccessMode mode,
                                                   ApkAssetsCookie* out_cookie) const {
  for (int32_t i = apk_assets_.size() - 1; i >= 0; i--) {
    std::unique_ptr<Asset> asset = apk_assets_[i]->Open(filename, mode);
    if (asset) {
      if (out_cookie != nullptr) {
        *out_cookie = i;
      }
      return asset;
    }
  }
  ......
}
std::unique_ptr<Asset> AssetManager2::OpenNonAsset(const std::string& filename,
                                                   ApkAssetsCookie cookie,
                                                   Asset::AccessMode mode) const {
    ......
    // 从这里就可以看出 ApkAssetsCookie 只是一个下标索引
    // 如果之前查找过对应的资源,就记录下来对应的数组下标
    // 下次获取时就不需要像上面一样用for循环去查找了
    return apk_assets_[cookie]->Open(filename, mode);
}

函数根据cookie需求做了一个简单的重载,但是最终都是调用到了ApkAssetsOpen()函数。

这个函数我们在资源加载时遇到过,作用就是加载资源,如果是压缩数据会进行相应的解压处理。

这里面还涉及到了Asset类,大家感兴趣可以深入了解下

getResource方式

跟踪getResource类型的函数调用,我们最终找到的native层接口是AssetManager2::FindEntry(),简要流程如下:

ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_override,
                                         bool /*stop_at_first_match*/,
                                         FindEntryResult* out_entry) const {
  ......
  // 根据resid解析出对应的pacakge信息 
  const uint32_t package_id = get_package_id(resid);
  const uint8_t type_idx = get_type_id(resid) - 1;
  const uint16_t entry_idx = get_entry_id(resid);
  const uint8_t package_idx = package_ids_[package_id];
  if (package_idx == 0xff) {
    // 资源对应的package为空,返回
    LOG(ERROR) << base::StringPrintf("No package ID %02x found for ID 0x%08x.", package_id, resid);
    return kInvalidCookie;
  }
  // 查找到资源对应的 PackageGroup 对象,PackageGroup 对象中包含着一个一个的loaded_pacakge
  const PackageGroup& package_group = package_groups_[package_idx];
  // 记录 PackageGroup 对象中的loaded_package_数量
  const size_t package_count = package_group.packages_.size();
  ......
  // 开始从资源对应的 PackageGroup 中查找最合适的资源
  for (size_t pi = 0; pi < package_count; pi++) {
    ......
    // If desired_config is the same as the set configuration, then we can use our filtered list
    // and we don't need to match the configurations, since they already matched.
    // 如果获取资源时的配置信息和资源load时的配置信息一致的话,可以使用快速查找逻辑
    const bool use_fast_path = desired_config == &configuration_;
    // 获取最适合当前配置的资源集合
    const FilteredConfigGroup& filtered_group = loaded_package_impl.filtered_configs_[type_idx];
    if (use_fast_path) {
      // 快速查找,直接使用filtered_configs_中的配置资源即可
    } else {
      // This is the slower path, which doesn't use the filtered list of configurations.
      // Here we must read the ResTable_config from the mmapped APK, convert it to host endianness
      // and fill in any new fields that did not exist when the APK was compiled.
      // Furthermore when selecting configurations we can't just record the pointer to the
      // ResTable_config, we must copy it.
      // 需要查找资源对应的所有类型,从中找出匹配的资源,过程中还会涉及到内存拷贝,比较缓慢
      ......
    }
  }
  ......
}

原函数还有很多细节没有展示,不过整体流程已经梳理出来了。再详细的部分就涉及到ARSC中的数据结构了,为了避免篇幅过大就先到这里啦!

感觉要想真正搞懂资源管理这部分还是要去熟悉arsc文件的格式,以及加载后的内存模型

学到这里算是找到了进入Android资源管理世界入口和钥匙了,后面再来深入学习研究吧(挖坑ING…)

结语

对于Android资源管理这部分的学习来说,进度比以往慢了很多,书中5.0的资源管理与当前系统差异着实不小,少了一个好的向导,学习起来迷茫了一些,好在对比着源码磕磕绊绊的理清了。

总结下来,还只是刚刚入门,像资源适配RRO等知识还未详细涉及,但为了不拖延大局,只能先到此为止了,等完成Android系统的整体学习后再来补充吧