Android基础_资源访问机制

一、定义资源

Aandroid 中的资源从类型的角度来看包括:drawable、layout、字符串、颜色值、menu、animation 等。

资源的定义可以分为两类:

  1. 属性的定义
  2. 值的定义

二、储存资源

Android 和资源有关的“属性”包括:

  • attr
  • styleable
  • style
  • theme

2.1、styleable 和 attr

attr 表示属性,风格样式的最小单元

styleable 一般和 attr 联合使用,用于定义一些属性,理论上可以不使用 styleable 而只是使用 attr,示例代码如下:

<element
    attr name="flower_name" format="string"
    attr name="flower_color" format="color|reference"
    attr name="flower_types" format="string"
    ...
</element>

对于以上定义,当解析 XML 时,会返回一个 AtrributeSet 对象,该对象包括了 element 中包含的所有属性和值。通过使用 R.id.flower_name 作为参数可以得到相应的取值,但是这种方式并不在能体现以上几个属性联合在一起的意义,于是引入了 styleable 的概念,示例代码如下:

<declare-styleable name="Flower">
    <attr name="flower_name" format="string"/>
    <attr name="flower_color" format="color|reference"/>
    <attr name="flower_types" format="string">
        <enum name="rose" value="Rose"/>
        <enum name="tulips" value="Tulips"/>
    </attr>
</declare-styleable>

这样从 AtrributeSet 中获取这三个属性值时,就可以把 R.styleale.Flower 作为参数,该参数实际会被 aapt 编译成为一个 int[] 数组,数组的内容就是所包含的 attr 的 id。

2.2、Style

Style 直译为“风格”,它是一系列 Attr 的集合用来定义一个 View 的样式,比如 height、width、padding 等,可以理解为是 Attr 属性集合的具体组合实现,示例代码如下:

<style name="Netherlands">
    <item name="flower_name">yahoo</item>
    <item name="flower_color">@color/Amber</item>
    <item name="flower_types">Rose</item>
</style>

以上 XML 文件就实现了 Flower 中定义的属性,并将这些具体的属性这定义为一个 Style。

2.3、theme

Theme 直译为“主题”,而主题本身是一个抽象化的概念,比如 Window 系统中的主题包括了桌面背景、系统图标、字体大小以及按钮风格等,在 Android 中,从代码角度来看,主题包括三个方面,其出处和用法如下:

  1. 在 frameworks/base/core/res/res/values/attrs.xml 中定义了一个名称为 Theme 的 styleable:

    <declare-styleable name="Theme">
    

其中包括了几十项属性,包括窗口样式、字体颜色、大小、按钮样式、进度条等。这些属性大部分只能用在 AndroidManifest 文件中的 Application 和 Activity 中,而不能用于普通的 View/ViewGroup 中

原因很简单,attrs 已经为 View/ViewGroup 定义了可用的属性,而 styleable 只是为了方便访问一些特定的 attr 集合,而把这些 attr 放在一个 styleable 中,而 theme 只是把适用于 Application 和 Activity 的属性单独放到了一起

  1. 在 frameworks/base/core/res/res/values/attrs_manifest.xml 中定义了一个名称为 theme 的属性:

    <attr name="theme" format="reference"/>
    

该属性只有定义到 AndroidManifest 中才有意义。进一步讲,只有定义到 Application 和 Activity 元素内部才是有意义的,因为只有在这两个类中才能读取 theme 属性的值。theme 的属性赋值是一个 reference,其引用类型是 style,即 theme 应该赋值到某个 style,而 该 style 中所包含的属性应该是 Application 和 Activity 中可以识别的属性值,也就是名称为 theme 的 styleable 中定义的属性。

  1. 在 frameworks/base/core/res/res/values/themes.xml 中定义了一系列名称为 theme 的 style:

     <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>
       <item name="colorBackground">@color/background_dark</item>
       ...
     </Style>
    

这些 style 将作为 Application 和 Activity 中 theme 中的值,而这些 theme 中所包含的属性属性名称全部取自与 styleable name = “Theme” 处。

三、AttributeSet 与 TypedArray

3.1、AttributeSet

AttributeSet 类的位置在 android.util 包中,从该类的位置看,该类与 Framework 内核没有任何直接的联系,而纯粹是一个辅助类。该类仅仅是为了解析 XML 文件用的。AttributeSet 类的代码如下(只列出部分方法):

public interface AttributeSet {

    public int getAttributeCount();

    public String getAttributeName(int index);

    public String getAttributeValue(int index);

    ...

    public String getClassAttribute();

    public int getIdAttributeResourceValue(int defaultValue);

    public int getStyleAttribute();
}

一般 XML 文件有以下形式:

<ElementName
  attr_name1="value1"
  attr_name2="value2"
  attr_name3="value3"
  ...
</ElementName>

如果使用一般的 XML 解析工具,则可以通过类似 getElementById() 等方法获得属性的名称和属性值,然而却没有在属性名称和 attrs.xml 定义的属性名称建立任何联系。而 AttributeSet 类正是在这之间建立了某中联系,并提供了一些新的 API 接口,从而可以方便根据 attrs.xml 已有的名称获得相应的属性值

在开发中,通常并不关心 AttributeSet 接口如何实现,只需要通过该该对象获取相应的属性值即可,比如在 TextView 的构造函数中:

public TextView(Context context, AttributeSet attrs) {
  super((Context)null, (AttributeSet)null, 0, 0);
  throw new RuntimeException("Stub!");
}

AttributeSet 中的 API 可按功能划分为以下几类,假定该 XML 的格式为:

<View class="android.widget.TextView"
  android:layout_width="@dimem/general_width"
  android:layout_height="wrap_content"
  android:text="@string/title"
  android:id="@+id/output"
  style="@style/Test"
/>
  • 第一类,操作特定属性,包括以下几类:

    • public String getIdAttribute(): 获取 id 属性对应的字符串,返回值为“@+id/output”。
    • public String getClassAttribute(): 获取 class 对应的字符串,返回值为“android.widget.TextView”。
    • public String getStyleAttribute(): 获取 style 对应的字符串,返回值“@style/Test”。
    • public int getIdAttributeResourceValue(int defaultValue): 返回 id 属性对应的 int 值,此处应该是 R.id.output 的值。
  • 第二类,操作通用属性,包括以下几类:

    • public int getAttributeCount(): 获取属性的数目。

    • public String getAttributeName(int index): 根据属性所在的位置返回相应的属性名称。本例中,相应的属性位置如下:

      class=0
      layout_width=1
      layout_height=2
      text=3
      id=4
      style=5
      

      如果 index 为 1,则返回 android:layout_width。

    • public String getAttributeValue(int index): 根据位置返回属性值。本例中,如果 index 为 1,则返回“@dimem/general_width”。

    • public String getAttributeValue(String nameSpase, String name):返回值定命名空间、指定名称的属性值,该函数说明 AttributeSet 允许给一个 XML Element 的属性中添加多个命名空间的属性值。

    • public int getAttributeNameResource(int index):返回指定位置的属性 id 值。本例中,如果 index 为 1,则返回 R.id.layout_width,系统为每一个属性(attr)分配了惟一的 id 值。

  • 第三类,获取特定类型的值。

    • public XXXType getAttributeXXXTypeValue(int index,XXXType defaultValue);

      其中 XXXType 包括 int、unsigned int、boolean、以及 float 类型,使用该方法时,必须明确知道某个位置(index)对应的数据类型。而且该方法仅适用于特定的类型,如果某个属性的值为 style 类型,或是一个 layout 类型,那么返回值将无效。

3.2、TypedArray

TypedArray 是对 AttributeSet 的某种抽象

在上面的例子中,对于 android:layout_width="@dimem/general_width",如果使用 AttributeSet 只能获取“@dimem/general_width”字符串,而实际上该字符串对应了一个 dimem 类型的数据,因此还要去解析 id 为 general_width 对应的具体的 dimen 的值。TypedArray 正是免去了这个过程,可以将 AttributeSet 作为参数来构造 TypedArray,TypedArray 提供更加方便的方法来直接获取该 dimen 的值。

从一个 AttributeSet 构造 TypedArray 对象方法代码如下:

TypedArray a = context.obtainStyleAttribute(attrs, com.android.internal.R.styleable.XXX, defStyle, 0);

函数 obtainStyleAttribute 的第一个参数为一个 AttributeSet 对象,它包含了一个 XML 元素中所定义的所有属性。第二个参数就是前面定义的 styleable,
aapt 会把 TypedArray 编译为一个 int[] 数组,该数组所包含的内容正是通过遍历 AttributeSet 中的每一个属性,然后把值和属性经过重定位,返回一个 TypedArray 对象。

下面分析 TypedArray 类的内部接口和重要成员变量。

该类的重要成员变量包括:

  • int[] mData;

  • /package/ TypedValue mValue = new TypedValue(): TypedValue 是一个数据类,其意义是为了保存一个属性值,比如 layout_width、textSize、textColor 等,该类中有四个重要成员变量:

    1. int type:类型包括 int、boolean、float、String、reference 等;
    2. int data:如果 type 是一个 int、boolean、float、类型,则 data 包含了具体的数据;
    3. int referenceId:如果 type 是一个 reference 类型,那么该值为对应的 reference id;
    4. CharSequence string:如果 type 是一个 String 类型,则该值为具体的 String。

mValue 起到了一个内部缓存的作用。mData 则包含了指定 styleable 中的所有属性值,mData 的长度为 styleable 中属性的的个数 × AssetManager.STYLE_NUM_ENTRIES
(该值为 6)。也就是说需要 6 个 int 来表示一个属性值,以下为 AssetManager 中这 6 个值的定义:

/*package*/ static final int STYLE_NUM_ENTRIES = 6;
/*package*/ static final int STYLE_TYPE = 0;
/*package*/ static final int STYLE_DATA = 1;
/*package*/ static final int STYLE_ASSET_COOKIE = 2;
/*package*/ static final int STYLE_RESOURCE_ID = 3;

/* Offset within typed data array for native changingConfigurations. */
static final int STYLE_CHANGING_CONFIGURATIONS = 4;

/*package*/ static final int STYLE_DENSITY = 5;

在以上常用值中,常用的包含了三个:

  • STYLE_TYPE(0):包含了值得类型;
  • STYLE_DATA(1):包含了特定的值;
  • STYLE_RESOURCE_ID(3):如果 STYLE_TYPE 为一个 reference 类型,该值对应了相应的 resource id;

下面来看 TypedArray 如何使用以上几个成员变量。当在 XML 中引用某个资源时,比如:

android:background=“@drawable/bkg”

该引用对应的元素一般是某个 View/ViewGroup,View/ViewGroup 的构造函数中一般会通过函数 obtainStyleAttributes() 方法返回一个 TypedArray 对象,然后再调用该对象中的相应 getDrawable() 方法。

下面以 getString() 和 getDrawable() 为例说明 TypedArray 内部接口的工作原理。getString() 代码如下:

@Nullable
public String getString(@StyleableRes int index) {
    if (mRecycled) {
        throw new RuntimeException("Cannot make calls to a recycled instance!");
    }

    index *= AssetManager.STYLE_NUM_ENTRIES;
    final int[] data = mData;
    final int type = data[index+AssetManager.STYLE_TYPE];
    if (type == TypedValue.TYPE_NULL) {
        return null;
    } else if (type == TypedValue.TYPE_STRING) {
        return loadStringValueAt(index).toString();
    }

    final TypedValue v = mValue;
    if (getValueAt(index, v)) {
        final CharSequence cs = v.coerceToString();
        return cs != null ? cs.toString() : null;
    }

    // We already checked for TYPE_NULL. This should never happen.
    throw new RuntimeException("getString of bad type: 0x" + Integer.toHexString(type));
}

首先传递进来的 index 必须先乘以 6,因为每一个属性值都站用连续的 6 个 int 值。每一个 styleable 都将被 aapt 编译为一个 int[] 数组,数组中的内容为 styleable 所包含的每一个属性(attr)对应的 id 的值,在调用 getString() 时,其参数 index 是该属性在 styleable 中的位置。当定义了一个 styleable 时, aapt 同时生成了 attr 在 styleable 中的位置,比如 TextView 是一个 styleable,其中包含的 attr 有 textSize,aapt 会自动生成一个 TextView_textSize 常量。该常量的名称格式是固定的,其形式为“styleable 名称 _attr 名称”。

接着从 mData 中取出值得类型,即 index+AssetManager.STYLE_TYPE 处,然后判断类型,如果为 STYLE_NULL,说明无该属性值,返回 null,如果类型为 STYLE_STRING,则调用 loadStringValueAt() 方法找到 String 并返回。

接着看 loadStringValueAt(),代码如下:

private CharSequence loadStringValueAt(int index) {
    final int[] data = mData;
    final int cookie = data[index+AssetManager.STYLE_ASSET_COOKIE];
    if (cookie < 0) {
        if (mXml != null) {
            return mXml.getPooledString(
                data[index+AssetManager.STYLE_DATA]);
        }
        return null;
    }
    return mAssets.getPooledStringForCookie(cookie, data[index+AssetManager.STYLE_DATA]);
}

该方法先从 mData 中取出 cookie,如果 cookie 小于 0 并且 mXml 存在,则会从 mXml(用于解析 XML 文件) 中得到 String 的值。mXml 内部有一个 String 池,通过 mData 的 STYLE_DATA 为索引可以得到哦相应的 String 值。如果 cookie 大于 0,那么 cookie 将作为 mResource.mAssets的内部方法 getPooledString() 的参数。cookie 将作为 mAssets 内部 mXml 的索引,而 Data 的 STYLE_DATA 将作为字符串索引。

下面看 getDrawable() 的流程:

@Nullable
public Drawable getDrawable(@StyleableRes int index) {
    return getDrawableForDensity(index, 0);
}

/**
 * Version of {@link #getDrawable(int)} that accepts an override density.
 * @hide
 */
@Nullable
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
    if (mRecycled) {
        throw new RuntimeException("Cannot make calls to a recycled instance!");
    }

    final TypedValue value = mValue;
    if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
        if (value.type == TypedValue.TYPE_ATTRIBUTE) {
            throw new UnsupportedOperationException(
                    "Failed to resolve attribute at index " + index + ": " + value);
        }

        if (density > 0) {
            // If the density is overridden, the value in the TypedArray will not reflect this.
            // Do a separate lookup of the resourceId with the density override.
            mResources.getValueForDensity(value.resourceId, density, value, true);
        }
        return mResources.loadDrawable(value, value.resourceId, density, mTheme);
    }
    return null;
}

四、获取 Resources 的过程

获取 Resources 有两种方式,一是通过 Context,而是通过 PackageManager。

4.1、通过 Context 获取

在开发中,通常是通过 getResources().getXXX() 方法来获取 XML 中的指定资源,比如 getDrawable()、getString()、getBoolean() 等。

首先看 getResources() 方法,该方法是 Context 的成员函数,一般在 Activity 或是 Service 中调用,因为 Activity 和 Service 本质上是一个 Context,而真正实现 Context 接口的是 ContextImpl 类。

ContextImpl 是在 ActivityThread 中创建的,它的 getResources() 方法就是返回其内部的 mResources 变量,对该变量的赋值是在创建 ContextImpl 对象时进行初始化的,代码如下(API 27):

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

  ...

  mPackageInfo = packageInfo;
  mResourcesManager = ResourcesManager.getInstance();

  ...

  Resources resources = packageInfo.getResources(mainThread);
  if (resources != null) {
      if (displayId != Display.DEFAULT_DISPLAY
              || overrideConfiguration != null
              || (compatInfo != null && compatInfo.applicationScale
                      != resources.getCompatibilityInfo().applicationScale)) {

          if (container != null) {
              // This is a nested Context, so it can't be a base Activity context.
              // Just create a regular Resources object associated with the Activity.
              resources = mResourcesManager.getResources(
                      activityToken,
                      packageInfo.getResDir(),
                      packageInfo.getSplitResDirs(),
                      packageInfo.getOverlayDirs(),
                      packageInfo.getApplicationInfo().sharedLibraryFiles,
                      displayId,
                      overrideConfiguration,
                      compatInfo,
                      packageInfo.getClassLoader());
          } else {
              // This is not a nested Context, so it must be the root Activity context.
              // All other nested Contexts will inherit the configuration set here.
              resources = mResourcesManager.createBaseActivityResources(
                      activityToken,
                      packageInfo.getResDir(),
                      packageInfo.getSplitResDirs(),
                      packageInfo.getOverlayDirs(),
                      packageInfo.getApplicationInfo().sharedLibraryFiles,
                      displayId,
                      overrideConfiguration,
                      compatInfo,
                      packageInfo.getClassLoader());
          }
      }
  }
  mResources = resources;

  ...

可以看出 mResources 是调用 mPackageInfo 的 getResources() 方法进行赋值的。一个用应用中的多个 ContextImpl 对象实际上共享了同一个 PackageInfo 对象,这就意味着,多个 ContextImpl 对象中的 mResources 变量实际上是同一个

packageInfo(类型为:LoadedApk)的 getResources() 方法如下:

public Resources getResources(ActivityThread mainThread) {
    if(this.mResources == null) {
        this.mResources = mainThread.getTopLevelResources(this.mResDir, this.mSplitResDirs, this.mOverlayDirs, this.mApplicationInfo.sharedLibraryFiles, 0, (Configuration)null, this);
    }
    return this.mResources;
}

ActivityThread 的 getTopLevelResources 的逻辑是得到本应用的对应的资源对象。代码如下:

Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, LoadedApk pkgInfo) {
    return this.mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), (IBinder)null);

// ResourcesManager#getTopLevelResources
public Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
    float scale = compatInfo.applicationScale;
    ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
    Resources r;
    synchronized(this) {
        WeakReference<Resources> wr = (WeakReference)this.mActiveResources.get(key);
        r = wr != null?(Resources)wr.get():null;
        if(r != null && r.getAssets().isUpToDate()) {
            return r;
        }
    }

    AssetManager assets = new AssetManager();
    if(resDir != null && assets.addAssetPath(resDir) == 0) {
        return null;
    } else {
        int len$;
        int i$;
        String libDir;
        String[] arr$;
        if(splitResDirs != null) {
            arr$ = splitResDirs;
            len$ = splitResDirs.length;

            for(i$ = 0; i$ < len$; ++i$) {
                libDir = arr$[i$];
                if(assets.addAssetPath(libDir) == 0) {
                    return null;
                }
            }
        }

        if(overlayDirs != null) {
            arr$ = overlayDirs;
            len$ = overlayDirs.length;

            for(i$ = 0; i$ < len$; ++i$) {
                libDir = arr$[i$];
                assets.addOverlayPath(libDir);
            }
        }

        if(libDirs != null) {
            arr$ = libDirs;
            len$ = libDirs.length;

            for(i$ = 0; i$ < len$; ++i$) {
                libDir = arr$[i$];
                if(assets.addAssetPath(libDir) == 0) {
                    Slog.w("ResourcesManager", "Asset path '" + libDir + "' does not exist or contains no resources.");
                }
            }
        }

        DisplayMetrics dm = this.getDisplayMetricsLocked(displayId);
        boolean isDefaultDisplay = displayId == 0;
        boolean hasOverrideConfig = key.hasOverrideConfiguration();
        Configuration config;
        if(isDefaultDisplay && !hasOverrideConfig) {
            config = this.getConfiguration();
        } else {
            config = new Configuration(this.getConfiguration());
            if(!isDefaultDisplay) {
                this.applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
            }

            if(hasOverrideConfig) {
                config.updateFrom(key.mOverrideConfiguration);
            }
        }
        // 重点,使用构造方法创建 Resources
        r = new Resources(assets, dm, config, compatInfo, token);
        synchronized(this) {
            WeakReference<Resources> wr = (WeakReference)this.mActiveResources.get(key);
            Resources existing = wr != null?(Resources)wr.get():null;
            if(existing != null && existing.getAssets().isUpToDate()) {
                r.getAssets().close();
                return existing;
            } else {
                this.mActiveResources.put(key, new WeakReference(r));
                return r;
            }
        }
    }
}

 // Resources
 @Deprecated
 public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
     this(null);
     mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
 }

 // ResourcesImpl
 public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
        @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
    mAssets = assets;
    mMetrics.setToDefaults();
    mDisplayAdjustments = displayAdjustments;
    updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
    mAssets.ensureStringBlocks();
}

变量 mActiveResources 对象内部保存了该应用所使用到的所有 Resources 对象,其类型为:

ArrayMap<ResourcesKey, WeakReference<Resources>>

参数 ResourcesKey 是一个数据类,其构造方式如下:

ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);

重点看第一个参数,resDir 变量为源文件路径,实际就是 APK 程序所在路径,比如可以是:/data/app/com.serah.android.Kaiyan.apk,该 APK 会对应 /data/dalvik-cache 目录下的:data@app@com.serah.android.Kaiyan.apk@classes.dex 文件。

所以,如果一个应用没有访问该程序外的其他资源,那么 mActiveResources 中只含有一个 Resources 对象。如果 mActiveResources 中没有包含所要的 Resources 那么,就重新建立一个 Resources 并添加到 mActiveResources 中。

可以发现 Resources 构造函数需要一个 AssetManager 对象,AssetManager 负责访问 res 下的所有资源(不只是 res/assets 目录下资源),AssetManager 中几个关键函数都是 native 的。以上代码 assets.addAssetPath(resDir) 函数非常关键,它为所创建的 AssetManager 对象添加资源路径,剩下的事就 AssetManager 内部完成,内部会从指定的路径下加载资源文件,AssetManager 构造函数如下:

 public AssetManager() {
    synchronized(this) {
        this.init(false);
        ensureSystemAssets();
    }
}

构造方法中的来两个关键函数 init() 和 ensureSystemAssets() 都是 native 实现的。

init() 用于初始化 AssetManager 内部的环境变量,初始化过程的一个关键任务就是把 Framework 中的资源路径添加到 AssetManager 中,该 native 代码如下(android_content_AssetManager_init.cpp):

static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem)
{
    if (isSystem) {
        verifySystemIdmaps();
    }
    AssetManager* am = new AssetManager();
    if (am == NULL) {
        jniThrowException(env, "java/lang/OutOfMemoryError", "");
        return;
    }

    am->addDefaultAssets();

    ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
    env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast<jlong>(am));
}

以上代码首先创建一个 AssetManager 类,这是一个 C++ 类,然后调用 am->addDefaultAssets() 将 Framework 的资源文件添加到这个 AssetManager 对象的路径中。最后调用 SetLongField() 方法将 C++ 创建的 AssetManager 对象引用保存到 java 端的 mObject 变量中,该变量可以在 java 端的 AssetManager 类中找到, 其类型为 int。

addDefaultAssets() 代码如下:

bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

    String8 path(root);
    path.appendPath(kSystemAssets);

    return addAssetPath(path, NULL, false /* appAsLib */, true /* isSystemAsset */);
}

该函数首先获取 Android 根目录, getenv() 是一个 Linux 系统调用,用户同样可以使用以下终端命令来获取:

keyid:tools keyid$ ./adb shell
root@android:/ # echo $ANDROID_ROOT
/system
root@android:/ #

获取根目录后,在与 kSystemAssets 路径进行组合,该变量定义如下:

static const char* kSystemAssets = "framework/framework-res.apk";

所以最中获得的文件路径为:/system/framework/framework-res.apk, 这正是 Framework 对应的资源文件。

分析完 init() 后,接着看 ensureSystemAssets() 方法,该方法实际在 Framework 启动时调用,因为 mSystem 是一个 static 变量,该变量在 Zygote 启动时已经被赋值。

private static void ensureSystemAssets() {
    Object var0 = sSync;
    synchronized(sSync) {
        if(sSystem == null) {
            AssetManager system = new AssetManager(true);
            system.makeStringBlocks((StringBlock[])null);
            sSystem = system;
        }
    }
}

因为应用程序中 Resources 对象内部的 AssetManager 对象除了包含应用程序本身的资源文件路径外,还包含了 Framework 的资源路径,这就是为什么仅使用本地 Resources 就能访问系统的资源的原因。

在 AssetManager.cpp 文件中,当使用 getXXX(int id) 访问资源时,如果 id 小于 0x1000 0000 时,AssetManager 会认为是访问系统资源。因为 aapt 在对系统资源进行编译时,所有资源 id 都被编译为小于该值的一个 int 值, 而当访问应用程序资源时,id 值都会大于 0x7000 0000。

创建好 Resources 对象后,就把该对象缓存到 mActiveResources 中,方便以后继续使用。以上就是访问 Resources 的整个流程。

4.2、通过 PackageManager 获取

该方法用于访问其他程序中的资源,使用 PackageManager 获取资源的代码如下:

    try {
        PackageManager pm = mContext.getPackageManager();
        pm.getResourcesForApplication("com.serah.android.Kaiyan");
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }

和其他 Manager 一样,PackageManager 负责和远程 PackageManagerService 进行通信,获取PackageManager 代码如下(ContextImpl中实现):

@Override
public PackageManager getPackageManager() {
    if (mPackageManager != null) {
        return mPackageManager;
    }

    IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));
    }

    return null;
}

PackageManager 本身是一个抽象类,其实现类是 ApplicationPackageManager,该类的构造函数包含了远程服务的一个引用,即 IPackageManager,该对象是通过 getPackageManager() 静态方法的到的,这种获取远程服务的方法和大多数获取远程服务的方法类似,代码如下(ActivityThread#getPackageManager()):

public static IPackageManager getPackageManager() {
    if (sPackageManager != null) {
        //Slog.v("PackageManager", "returning cur default = " + sPackageManager);
        return sPackageManager;
    }
    IBinder b = ServiceManager.getService("package");
    //Slog.v("PackageManager", "default service binder = " + b);
    sPackageManager = IPackageManager.Stub.asInterface(b);
    //Slog.v("PackageManager", "default service = " + sPackageManager);
    return sPackageManager;
}

即得到一个本地代理,获得代理后调用 getResourcesForApplication() 方法,该方法代码在 ApplicationPackageManager 中实现,代码如下:

@Override
public Resources getResourcesForApplication(@NonNull ApplicationInfo app)
        throws NameNotFoundException {
    if (app.packageName.equals("system")) {
        return mContext.mMainThread.getSystemUiContext().getResources();
    }
    final boolean sameUid = (app.uid == Process.myUid());
    final Resources r = mContext.mMainThread.getTopLevelResources(
                sameUid ? app.sourceDir : app.publicSourceDir,
                sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
                app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
                mContext.mPackageInfo);
    if (r != null) {
        return r;
    }
    throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
}

以上代码调用了 mMainThread.getTopLevelResources,这和从 Contex 中获取 Resource 过程一致。

需要注意的是这里的参数,其含义是:如果目标资源程序和当前程序是同一个 uid 那么就使用目标程序的 sourceDir 作为路径,否则就使用目标程序的publicSourceDir 目录,该目录可以在 AndroidManifest.xml 中指定。在多数情况下,目标程序和当前程序都不属于一个 uid,因此,多为 publicSourceDir,而该值在默认情况下和 sourceDir 的值相同。

当进入 mMainThread.getTopLevelResources() 方法后,全局 ActivityThread 对象就会在 mActiveResources 中保存一个新的 Resources 对象,其键值对应目标应用程序的包名。

五、Framework 资源

了解了 Resources 的获取流程后,本节将介绍系统资源的加载、读取、添加过程,至于系统资源是如何被编译的,将在后续文章中进行分析(aapt 编译
framework/base/core/res/res 目录下资源,并生成 framework-res.apk)。

5.1、加载和读取

系统资源是在 Zygote 进程启动时被加载的,并且只有当加载了系统资源之后才开始启动其他应用进程,从而实现其他应用进程共享系统资的目标。该过程源码在 com/android/internal/os/ZygoteInit.java 的 main() 函数,核心代码如下:

public static void main(String argv[]) {

    ZygoteHooks.startZygoteNoThreadCreation();

    try {

        ...

        // 加载系统资源
        preload();

        ...

        // 启动系统进程
        if (startSystemServer) {
            startSystemServer(abiList, socketName);
        }

        runSelectLoop(abiList);

        closeServerSocket();
    } catch (MethodAndArgsCaller caller) {
        caller.run();
    } catch (Throwable ex) {
        Log.e(TAG, "Zygote died with exception", ex);
        closeServerSocket();
        throw ex;
    }
}

以上函数内部过程可以分为三步:

  1. 加载系统资源;
  2. 调用 startSystemServer() 启动系统进程;
  3. 调用 runSelectLoop() 开始监听 Socket,并启动指定的应用进程。

本文主要分析第一步,即加载系统资源,该过程具体是通过 preloadResources() 实现的,该函数代码如下:

private static void preloadResources() {
    final VMRuntime runtime = VMRuntime.getRuntime();

    try {
        mResources = Resources.getSystem();
        mResources.startPreloading();
        if (PRELOAD_RESOURCES) {
            Log.i(TAG, "Preloading resources...");

            long startTime = SystemClock.uptimeMillis();
            TypedArray ar = mResources.obtainTypedArray(
                    com.android.internal.R.array.preloaded_drawables);
            int N = preloadDrawables(ar);
            ar.recycle();
            Log.i(TAG, "...preloaded " + N + " resources in "
                    + (SystemClock.uptimeMillis()-startTime) + "ms.");

            startTime = SystemClock.uptimeMillis();
            ar = mResources.obtainTypedArray(
                    com.android.internal.R.array.preloaded_color_state_lists);
            N = preloadColorStateLists(ar);
            ar.recycle();
            Log.i(TAG, "...preloaded " + N + " resources in "
                    + (SystemClock.uptimeMillis()-startTime) + "ms.");

            if (mResources.getBoolean(
                    com.android.internal.R.bool.config_freeformWindowManagement)) {
                startTime = SystemClock.uptimeMillis();
                ar = mResources.obtainTypedArray(
                        com.android.internal.R.array.preloaded_freeform_multi_window_drawables);
                N = preloadDrawables(ar);
                ar.recycle();
                Log.i(TAG, "...preloaded " + N + " resource in "
                        + (SystemClock.uptimeMillis() - startTime) + "ms.");
            }
        }
        mResources.finishPreloading();
    } catch (RuntimeException e) {
        Log.w(TAG, "Failure preloading resources", e);
    }
}

以上代码有两个关键点:

  1. 第一点,创建 Resources 对象 mResources 是通过 Resources.getSystem() 函数。该函数返回的 Resources 对象只能访问 Framework 中定义的系统资源。

getSystem() 函数内部会调用一个 private 类型的 Resources 构造函数,该函数内部调用 AssetManager 类的静态方法 AssetManager.getSystem() 为变量 mAssets 赋值,从而保证了 Resources 类内部 mSystem 变量对应为系统资源,mSystem 是 static 类型的。

从这里可以看出,zygote 中创建 Resources 对象和普通应用程序的不同,前者使用静态的 getSystem()方法,而后者使用带有参数的 Resources 构造函数创建,参数间接包含了应用程序资源文件的路径信息

  1. 第二点,有了包含系统资源的 Resources 后,接下来调用两个重要函数:preloadDrawables 和 preloadColorStateLists,装载需要的“预装载”资源。

首先看 preloadDrawables():

    private static int preloadDrawables(TypedArray ar) {
        int N = ar.length();
        for (int i=0; i<N; i++) {
            int id = ar.getResourceId(i, 0);
            if (false) {
                Log.v(TAG, "Preloading resource #" + Integer.toHexString(id));
            }
            if (id != 0) {
                if (mResources.getDrawable(id, null) == null) {
                    throw new IllegalArgumentException(
                            "Unable to find preloaded drawable resource #0x"
                            + Integer.toHexString(id)
                            + " (" + ar.getString(i) + ")");
                }
            }
        }
        return N;
    }

该函数的参数是一个 TypedArray 对象,其来源是 res/values/arrays.xml 中定义的一个 array 数组资源,名称为 preloaded_drawable,以下是该资源的代码片段:

    <array name="preloaded_drawables">
      <item>@drawable/ab_share_pack_material</item>
      <item>@drawable/ab_solid_shadow_material</item>
      <item>@drawable/action_bar_item_background_material</item>
      <item>@drawable/activated_background_material</item>
      ......
    </array>

因此,需要想要让所有的应用进程共享预装的资源,则需要在该文件中生命资源的名称。

接下来看 preloadColorStateLists:

    private static int preloadColorStateLists(TypedArray ar) {
        int N = ar.length();
        for (int i=0; i<N; i++) {
            int id = ar.getResourceId(i, 0);
            if (false) {
                Log.v(TAG, "Preloading resource #" + Integer.toHexString(id));
            }
            if (id != 0) {
                if (mResources.getColorStateList(id, null) == null) {
                    throw new IllegalArgumentException(
                            "Unable to find preloaded color resource #0x"
                            + Integer.toHexString(id)
                            + " (" + ar.getString(i) + ")");
                }
            }
        }
        return N;
    }

该函数的参数同样是一个 TypedArray, 来源同样是来自 res/values/arrays.xml 中定义的一个数组资源,名称为 preloaded_color_state_lists,该资源代码片段如下所示:

    <array name="preloaded_color_state_lists">
      <item>@color/primary_text_dark</item>
      <item>@color/primary_text_dark_disable_only</item>
      <item>@color/primary_text_dark_nodisable</item>
      .....
    </array>

以上便是加载资源的来源,接着,在 Resources 类中相关资源读取函数中则需要将读到的资源缓存起来,为了这个目地,Resources 中定义了四个静态变量,
如下所示:

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<>();
private static boolean sPreloaded;

前三个都是类表变量,并且是 static 的,正是由于变量类型为 static 所以导致 Resources 类在被应用进程创建新的对象时,保存了 zygote 进程中所预装载的资源。

由于 zygote 进程在从 framework-res.apk 中装载资源的实现方式和普通进程基本相同,可以通过 sPreloaded 变量区分是从 zygote 中还是普通进程调用,该变量在 startPreloading() 中被设置为 true,在 finishPreloading() 中被设置为 false,该过程参见 ZygoteInit 的 preloadResources() 函数。

5.2、添加

以上加载的资源仅仅是加载 res/values/arrays.xml 中的资源,而 Framework 中的资源远不止于此,以设计者角度来看,对于那些非“预装载”的系统资源不会被添加到静态列表中,在这种情况下,多个应用进程如果需要一个非预装载的资源,则会在各自的进程中保持一个资源的缓冲。

至于是否是“预装载”的,仅仅取决于该资源始否在 res/values/arrays.xml中。

系统资源按照被公开的方式分为公开的和私有的,总的来说只要是放在 res 目录下的资源都会被 aapt 编译,而所谓的私有资源是指仅能在 Framework 内部访问的资源,公开资源是指使用 SDK 的应用程序也能访问的系统资源。

假设要添加一个字符串资源:

<string name="cus_str">Custom string</string>

那么,可以直接把以上代码添加到 res/values/arrays.xml 中,然后重新编译 Framework,编译完毕后,在 com.android.internal.R.java 文件中,就会包含该字符串的声明,然后在 Framework 的源码中就可以直接引用该资源。

需要注意的是,cus_str 被存放到了 com.android.internal.R 文件中,而不是 android.R 文件中,这正是公开资源和私有资源的区别所在,使用 SDK 开发普通应用时,当要引用系统资源时,只能引用 android.R 文件,该文件的内容可以被认为是 com.android.internal.R 的一个子集。
如前所述,res 目录下的资源总会被 aapt 编译到一个 R.java 文件中,这个文件就是 com.android.internal.R 文件,而 android.R 文件则是来源于
res/values/public.xml 中定义的资源。

res/values/public.xml 为所有需要公开到 SDK 中的资源进行 id 的预先定义,这些 id 值在不同的 Android 版本中保持一致,从而保证了 Android 版本的资源兼容性。

想要将前面的 cus_str 公开到 SDK 中,需要在 public.xml 文件中声明该字符串,为新资源指定 id 时必须要考虑来两个个问题:

  1. 不能与已有的 id 冲突;
  2. 尽量避免与未来的 id 冲突。

本例中,就可以给 public.xml 文件添加以下代码:

<public type="string" name="cus_str" id="0x0104f000" />

id 的含义是,01 代表这是一个 Framework 资源,04 代表着是一个 string 类型的资源,f000 是该资源的编号,之所以从 f000 开始,是因为 Framework 内部的资源是从 0 开始的,防止以后递增时与我们自己定义的资源值冲突。

六、android 生成资源 id

先看一下 apk 的打包流程,Android Developer 官方流程,下面对官方流程做了系统的总结。

下图的是官网对于 Android 编译打包流程的介绍:

image

虚线方框是打包 APK 的操作,现在开发 Android 都是使用的 Android Studio 基于 gradle 来构建项目,所有打包操作都是执行 gradle 脚本来完成,gradle 编译脚本具有强大的功能,可以在里面完成多渠道,多版本,不同版本使用不同代码,不同的资源,编译后的文件重命名,混淆签名验证等等配置,虽然都是基于AndroidSdk 的 platform-tools 的文件夹下面的工具来完成的,但是有了 gradle 这个配置文件,这样就便捷了。

以下是一张 APK 打包详细步骤流程图:

具体步骤:

  1. 打包资源文件,生成 R.java 文件;
  2. 处理 aidl 文件,生成相应的 .java 文件;
  3. 编译工程源码,生成相应的 class 文件;
  4. 转换所有的 class 文件,生成 classes.dex 文件;
  5. 打包生成 apk;
  6. 对 apk 文件进行签名;
  7. 对签名后的 apk 进行对齐处理。

本文只分析 aapt 对资源文件的编译过程。

6.1、aapt

资源 id 的生成过程主要是调用 aapt 源码目录下的 Resouce.cpp 的 buildResources() 函数,该函数首先检查 AndroidManifest.xml 的合法性,然后对 res 目录下的资源目录进行处理,处理函数为 makeFileResource(),处理的内容包括资源文件名的合法性检查,向资源表 table 添加条目等。处理完后调用 compileResourceFile() 函数编译 res 与 asserts 目录下的资源并生 resource.arsc 文件,compileResourceFile() 函数位于 appt 源码目录的 ResourceTable.cpp 文件中,该函数最后会调用 parseAndAddEntry() 函数生成 R.java 文件,完成资源编译后,接下来调用 compileXmlfile() 函数对 res 目录的子目录下的 xml 文件进行编译,这样处理过的 xml 文件就简单的被"加密"了,最后将所有资源与编译生成的 resource.arsc 文件以及"加密"过的 AndroidManifest.xml 打包压缩成 resources.ap_ 文件。

上面涉及的源码代码位置在:

aapt 编译后的输出文件包括:

  • resources.ap_ 文件
  • R.java 文件

打包资源的工具 aapt,大部分文本格式的 XML 资源文件会被编译成二进制格式的 XML 资源文件,除了 assets 和 res/raw 资源被原封不动地打包进 APK 之外,其他资源都会被编译或者处理。

注意,除了 assets 和 res/raw 资源被原封不动地打包进 APK 之外,其它的资源都会被编译或者处理。除了 assets 资源之外,其他的资源都会被赋予一个资源 id。

resources.arsc 是清单文件,但是 resources.arsc 跟 R.java 区别还是非常大的,R.java 里面的只是 id 列表,并且里面的 id 值不重复。但是 drawable-xdpi 或者 drawable-xxdpi 这些不同分辨率的文件夹存放的图片和名称和 id 是一样的,在运行的时候就需要 resources.arsc 这个文件了,resources.arsc 里面会对所有的资源 id 进行组装,在 apk 运行是会根据设备的情况来采用不同的资源。resource.arsc 文件的作用就是通过一样的 id,根据不同的配置索引到最佳的资源现在 UI 中。

可以这样理解:R.java 是我们在写代码时候引用的 res 资源的 id 表,resources.arsc 是程序在运行时候用到的资源表。R.java 是给程序员读的,resources.arsc 是给机器读的

大体情况如下:

6.2、资源 id 生成规则

资源 id 是一个 32bit的数字,格式是 PPTTNNNN,其中 PP 代表资源所属的包(package),TT 代表资源的类型(type),NNNN 代表这个类型下面的资源的名称。

对于应用程序的资源来说,PP 的取值是 0×7f。TT 和 NNNN 的取值是由 aapt 工具随意指定的–基本上每一种新的资源类型的数字都是从上一个数字累加的(从1开始);而每一个新的资源条目也是从数字 1 开始向上累加的。

所以如果我们的这几个资源文件按照下面的顺序排列,aapt 会依次处理:

<code>layout/main.xml </code>

<code>drawable/icon.xml </code>

<code>layout/listitem.xml</code>

按照顺序,第一个资源的类型是”layout” 所以指定 TT==1,这个类型下面的第一个资源是”main”,所以指定 NNNN==1 ,最后这个资源就是 0x7f010001。
第二个资源类型是”drawable”,所以指定 TT==2,这个类型下的”icon” 指定 NNNN==1,所以最终的资源 ID 是 0x7f020001。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269