关于Android的资源加载

667 阅读6分钟
  • 资源 Resource

旧版本的Android Studio,可以直接找到这个R.java类,它是由AAPT工具在编译打包过程中,自动生成的。 该类主要是一堆的常量。但是新版的Android Studio它已经隐藏起来了。通过打开apk,可以看到带有arsc后缀的文件。打开它,可以看到所有的资源ID。 右边表格第一列就是资源ID,它是用一个16进制表示,0x表示16进制,7f对比多个应用都一样,08是资源类型, drawable(08),color(06),后面4位代表每组类型的资源顺序,后面4位是会变化的,每次打包都可能会变化。 这个ID就是整个资源表的一个主键,你可以通过它就找到关于这个资源的其他信息,包括资源类型,名称,文件对应目录等。还可以通过另外一种方式,找到所有的资源ID。

/app/build/intermediates/runtime_symbol_list/debug/R.txt

int anim abc_tooltip_enter 0x7f01000a
int anim abc_tooltip_exit 0x7f01000b
  • 资源内容类型 TypeValue

文件资源查找是在Native层实现的,解析好后会存放在TypeValue中

XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
        throws NotFoundException {
    final TypedValue value = obtainTempTypedValue();
    ...
    final ResourcesImpl impl = mResourcesImpl;
    impl.getValue(id, value, true);
    if (value.type == TypedValue.TYPE_STRING) {
        return loadXmlResourceParser(value.string.toString(), id,
                value.assetCookie, type);
    }
    ...
}

boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
        boolean resolveRefs) {
    ...
    final int cookie = nativeGetResourceValue(
            mObject, resId, (short) densityDpi, outValue, resolveRefs);
    ...
    return true;
}

如果把Resource理解成是一个文件,文件类型也有很多种,而TypeValue 是存储解析后的文件内容。而内容的类型也有很多种,下面分析几种常见的。先来看一下常见的资源具体内容有哪些,怎么配置的。

颜色
<color name="purple_500">#FF6200EE</color>
大小
<dimen name="pull_up_drawer_height">64.0000dp</dimen>
文字
<string name="app_name">My Application</string>
整数
<integer name="item2">11</integer>
数值
<bool name="item3">true</bool>
图形
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/Blue_10"/>
</shape>
布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:text="button"
        android:id="@+id/button"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</RelativeLayout>

Native解析后,把逻辑存储到TypeValue中,针对每一种内容,有不同的逻辑表示。其中它的type属性用来表示数据类型,data用来存储数据。

  • 如果是dimen,那它的属性typeTYPE_DIMENSION表示;
public float getDimension(@DimenRes int id) throws NotFoundException {
    ...
    if (value.type == TypedValue.TYPE_DIMENSION) {
        return TypedValue.complexToDimension(value.data, impl.getDisplayMetrics());
    }
    ...
}
public static float complexToDimension(int data, DisplayMetrics metrics)
{
    return applyDimension(
        (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
        complexToFloat(data),
        metrics);
}

Dimen 也是用一个整数表示,但是它包括了单位,小数,整数部分,其中单位是低4位表示,通过位运算(data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,可以取出单位。

  • 如果是color,那它的属性type用一个整数表示,整数在TYPE_FIRST_INTTYPE_LAST_INT之前。
@ColorInt
public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
    ...
    if (value.type >= TypedValue.TYPE_FIRST_INT
&& value.type <= TypedValue.TYPE_LAST_INT) {
        return value.data;
    }
    ...
}

配置中的颜色值是通过16进制表示,但是Native层解析后的颜色是一个10进制,可以通过转换得到16进制

String hexColor = "0x" Integer.toHexString(color)

  • 如果是string,那它的属性typeTYPE_STRING表示;

字符串的加载跟color,dimen差不多,但是它多了一层缓存逻辑,每个应用都会有一个ApkAssets去管理,使用StringBlock字符串块去缓存了应用中的字符串资源。前面讲到Native层解析完后,会把数据存放到TypeValue,字符类型的TypeValue,它的data属性,代表着字符资源索引idJava层会继续通过这个资源索引id去请求字符串。

boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
        boolean resolveRefs) {
    ...
    synchronized (this) {
        final int cookie = nativeGetResourceValue(
                mObject, resId, (short) densityDpi, outValue, resolveRefs);
        ...
        if (outValue.type == TypedValue.TYPE_STRING) {
            if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
                return false;
            }
        }
        return true;
    }
}

//字符串块
CharSequence getStringFromPool(int idx) {
    if (mStringBlock == null) {
        return null;
    }

    synchronized (this) {
        return mStringBlock.getSequence(idx);
    }
}
//读取缓存
public CharSequence getSequence(int idx) {
    synchronized (this) {
        if (mStrings != null) {
            CharSequence res = mStrings[idx];
            if (res != null) {
                return res;
            }
        } else if (mSparseStrings != null) {
            CharSequence res = mSparseStrings.get(idx);//如果缓存中有就拿缓存的
            if (res != null) {
                return res;
            }
        } else {
            final int num = nativeGetSize(mNative);
            if (mUseSparse && num > 250) {
                mSparseStrings = new SparseArray<CharSequence>();
            } else {
                mStrings = new CharSequence[num];
            }
        }
        String str = nativeGetString(mNative, idx);//通过id请求字符串
        if (str == null) {
            return null;
        }
        CharSequence res = str;
        ...
        //style
        ...
        if (res != null) {
            if (mStrings != null) mStrings[idx] = res;
            else mSparseStrings.put(idx, res);//缓存数据
        }
        return res;
    }
}

字符串是支持Html style,它的逻辑实现也是在StringBlock

for (int styleIndex = 0; styleIndex < style.length; styleIndex += 3) {
    int styleId = style[styleIndex];
    ...
    if (styleTag.equals("b")) {
        mStyleIDs.boldId = styleId;
    }
    ...
    } else if (styleTag.equals("marquee")) {
        mStyleIDs.marqueeId = styleId;
    }
}
res = applyStyles(str, style, mStyleIDs);

虽然做了缓存,但是框架并不会一直持有这些资源,在应用退出之后,会释放掉的。

  • 如果是drawable

它会先找到文件的对应类型,文件类型有很多,前面有讲到,然后再根据不同文件类型,去解析文件内容。

ResourcesImpl.java
private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
        int id, int density) {
    ...
    final String typeName = getResourceTypeName(id);
    if (typeName != null && typeName.equals("color")) {
        dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
    } else {
        dr = loadXmlDrawable(wrapper, value, id, density, file);
    }
    ...
}

之所以有颜色判断区分,是因为getDrawable 方法可以传入colorId 最后得到Drawable 对象,只不过它是一个颜色相关的ColorDrawable对象

Drawable drawable = getResources().getDrawable(R.drawable.bg,null);

Drawable drawable1 = getDrawable(R.color.purple_500);//color

但是drawable还有很多种类可以配置在xml中,也是通过inflateFromTag文件内容的标签生成。比如标签shape,代表的是我们常用的图形。

DrawableInflater.java

@NonNull Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
        @NonNull AttributeSet attrs, int density, @Nullable Theme theme)
        throws XmlPullParserException, IOException {
    ...
    Drawable drawable = inflateFromTag(name);
    if (drawable == null) {
        drawable = inflateFromClass(name);
    }
    drawable.setSrcDensityOverride(density);
    drawable.inflate(mRes, parser, attrs, theme);
    ...
    return drawable;
}

private @Nullable Drawable inflateFromTag(@NonNull String name) {
    switch (name) {
        case "selector":
            return new StateListDrawable();
        case "layer-list":
            return new LayerDrawable();
        ...
        case "color":
            return new ColorDrawable();
        case "shape":
            return new GradientDrawable();
        ...
        default:
            return null;
    }
}

类型很多,为了方便看代码,我删除掉一些类型。inflateFromTag方法就是根据不同配置标签,实例化不同的对象。 但是实例化好对象之后,如何根据具体的对象,去初始化不同的属性?因为每一种Drawable他们对象属性都是不一样的。通过drawable.inflate(mRes, parser, attrs, theme);会继续初始化配置中的属性参数。

比如常用的图形shape

GradientDrawable.java
private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
        Theme theme) throws XmlPullParserException, IOException {
    TypedArray a;
    int type;

    final int innerDepth = parser.getDepth() + 1;
    int depth;
    while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
           && ((depth=parser.getDepth()) >= innerDepth
                   || type != XmlPullParser.END_TAG)) {
        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        if (depth > innerDepth) {
            continue;
        }

        String name = parser.getName();

        if (name.equals("size")) {
            a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableSize);
            updateGradientDrawableSize(a);
            a.recycle();
        } else if (name.equals("gradient")) {
            a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableGradient);
            updateGradientDrawableGradient(r, a);
            a.recycle();
        } else if (name.equals("solid")) {
            a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableSolid);
            updateGradientDrawableSolid(a);
            a.recycle();
        } else if (name.equals("stroke")) {
            a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableStroke);
            updateGradientDrawableStroke(a);
            a.recycle();
        } else if (name.equals("corners")) {
            a = obtainAttributes(r, theme, attrs, R.styleable.DrawableCorners);
            updateDrawableCorners(a);
            a.recycle();
        } else if (name.equals("padding")) {
            a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawablePadding);
            updateGradientDrawablePadding(a);
            a.recycle();
        } else {
            Log.w("drawable", "Bad element under <shape>: " + name);
        }
    }
}

但是如果它是一张图片资源,比如getDrawable(R.drawable.img),它是不需要配置的。所以它并不需要走解析文件内容的流程,直接通过读取文件流解析成BitmapDrawable,通过文件前后缀名称判断是否需要走解析流程。

ResourcesImpl.java
if (file.endsWith(".xml")) {
    final String typeName = getResourceTypeName(id);
    if (typeName != null && typeName.equals("color")) {
        dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
    } else {
        dr = loadXmlDrawable(wrapper, value, id, density, file);
    }
} else if (file.startsWith("frro://")) {
    ...
} else {
    final InputStream is = mAssets.openNonAsset(
            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
    final AssetInputStream ais = (AssetInputStream) is;
    dr = decodeImageDrawable(ais, wrapper, value);
}
  • 如果是layout
LayoutInflater layoutInflater = LayoutInflater.from(getApplicationContext());
View view = layoutInflater.inflate(R.layout.activity1,null);

一般通过LayoutInflater加载layout 先来看一下他主要的解析代码

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    ...
    final View view = createViewFromTag(parent, name, context, attrs);
    final ViewGroup viewGroup = (ViewGroup) parent;
    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
    rInflateChildren(parser, view, attrs, true);
    viewGroup.addView(view, params);
    ...

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

createViewFromTag方法主要是通过标签生成对象,比如,RelativeLayout,然后继续循环子标签rInflateChildren,把所有的子标签也加载完成。循环结束之后,调用parent.onFinishInflate(); createViewFromTag 方法最终会调用createView方法,通过反射机制创建实例对象。

try {
    final View view = constructor.newInstance(args);
    if (view instanceof ViewStub) {
        // Use the same context when inflating ViewStub later.
        final ViewStub viewStub = (ViewStub) view;
        viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
    }
    return view;
} finally {
    mConstructorArgs[0] = lastContext;
}