包体积:Layout 二进制文件裁剪优化

一、引言

得物App在包体积优化方面已经进行了诸多尝试,收获也颇丰,已经集成的方案有图片压缩、重复资源删除、ARSC压缩等可移步至得物 Android 包体积资源优化实践。本文将主要介绍基于 XML 二进制文件的裁剪优化。

在正式进入裁剪优化前,需要先做准备工作,我们先从上层的代码看起,看看布局填充的方法。方便我们从始到终了解整个情况。

二、XML 解析流程

在 LayoutInflater 调用 Inflate 方法后,会将 XML 中的属性包装至 LayoutParams 中最后通过反射使用创建对应 View。

而在反射前,传入的 R.layout.xxx 文件是如何完成 XML 解析类的创建,后续又是如何通过该类完成 XML 中的数据解析呢?

图片图片

图片图片

图片图片

图片图片

上层 XML 解析最终会封装到 XmlBlock 这个类中。XmlBlock 封装了具体 RES 文件的解析数据。其中 nativeOpenXmlAsset 返回的就是 c 中对应的文件指针,后续取值都需要通过这个指针去操作。

图片图片

XmlBlock 内部的 Parse 类实现了 XmlResourceParser ,最终被包装为 AttributeSet 接口返回。

图片图片

例如调用 AttributeSet 的方法:

val attributeCount = attrs.attributeCount
for (i in 0 until attributeCount) {
    val result = attrs.getAttributeValue(i)
    val name = attrs.getAttributeName(i)
    println("name:$name ,value::::$result")
}
最终就会调用到 XmlResourceParser 中的方法,最终调用到 Native 中。

图片图片

//core/jni/android_util_XmlBlock.cpp

图片图片

可以看到,我们最终都是通过 ResXmlParser 类传入对应的 ID 来完成取值。而不是通过具体的属性名称来进行取值。

上面介绍的是直接通过 Attrs 取值的方式,在实际开发中我们通常会使用 TypedArray 来进行相关属性值的获取。例如 FrameLayout 的创建工程。

public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
        @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);

    final TypedArray a = context.obtainStyledAttributes(
            attrs, R.styleable.FrameLayout, defStyleAttr, defStyleRes);
    saveAttributeDataForStyleable(context, R.styleable.FrameLayout,
            attrs, a, defStyleAttr, defStyleRes);

    if (a.getBoolean(R.styleable.FrameLayout_measureAllChildren, false)) {
        setMeasureAllChildren(true);
    }

    a.recycle();
}

而 obtainStyledAttributes 方法最终会调用到 AssetManager 中的 applyStyle 方法,最终调用到 Native 的 nitiveApplyStyle 方法。

图片图片

图片图片

//https://android.googlesource.com/platform/frameworks/base/+/6d0e2c9cb948a10137e6b5a4eb00e601622fe8ee/core/jni/android_util_AssetManager.cpp
static jboolean android_content_AssetManager_applyStyle(JNIEnv* env, jobject clazz,
                                                        jlong themeToken,
                                                        jint defStyleAttr,
                                                        jint defStyleRes,
                                                        jlong xmlParserToken,
                                                        jintArray attrs,
                                                        jintArray outValues,
                                                        jintArray outIndices)
{
...
        const jsize xmlAttrIdx = xmlAttrFinder.find(curIdent);
        if (xmlAttrIdx != xmlAttrEnd) {
            // We found the attribute we were looking for.
            block = kXmlBlock;
            xmlParser->getAttributeValue(xmlAttrIdx, &value);
            DEBUG_STYLES(ALOGI("-> From XML: type=0x%x, data=0x%08x",
                    value.dataType, value.data));
        }
...

}

//https://android.googlesource.com/platform/frameworks/base/+/6d0e2c9cb948a10137e6b5a4eb00e601622fe8ee/libs/androidfw/ResourceTypes.cpp
ssize_t ResXMLParser::getAttributeValue(size_t idx, Res_value* outValue) const
{
    if (mEventCode == START_TAG) {
        const ResXMLTree_attrExt* tag = (const ResXMLTree_attrExt*)mCurExt;
        if (idx attributeCount)) {
            const ResXMLTree_attribute* attr = (const ResXMLTree_attribute*)
                (((const uint8_t*)tag)
                 + dtohs(tag->attributeStart)
                 + (dtohs(tag->attributeSize)*idx));
            outValue->copyFrom_dtoh(attr->typedValue);
            if (mTree.mDynamicRefTable != NULL &&
                    mTree.mDynamicRefTable->lookupResourceValue(outValue) != NO_ERROR) {
                return BAD_TYPE;
            }
            return sizeof(Res_value);
        }
    }
    return BAD_TYPE;
}

三、XML 二进制文件格式

你写的代码是这个样子,App 打包过程中通过 AAPT2 工具处理完 XML文件,转换位二进制文件后就是这个样子。

图片图片

图片图片

要了解这个二进制文件,使用 命令行 hexdump  查看:

图片图片

在二进制文件中,不同数据类型分块存储,共同组成一个完整文件。我们可以通过依次读取每个字节,来获取对应的信息。要准确读取信息,就必须清楚它的定义规则和顺序,确保可以正确读取出内容。

https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/libs/androidfw/include/androidfw/ResourceTypes.h

图片图片

图片图片

每一块(Chunk)都按固定格式生成,最基础的定义有:

Type:类型 分类,对应上面截图中的类型

headerSize:头信息大小

Size:总大小 (headerSize+dataSize)通过这个值,你可以跳过该 Chunk 的内容,如果 Size 和 headerSize 一致,说明该 Chunk 没有数据内容。

StringPoolChunk

在 StringPool 中,除了基础的 ResChunk ,还额外包含以下信息:

stringCount: 字符串常量池的总数量

styleCount: style 相关的的总数量

Flag: UTF_8 或者 UTF_16 的标志位  我们这里默认就是 UTF_8

stringsStart:字符串开始的位置

stylesStart:styles 开始的位置

字符串从 stringStart 的位置相对开始,两个字节来表示长度,最后以 0 结束。

XmlStartElementChunk

图片图片

startElementChunk 是布局 XML 中核心的标签封装对象,里面记录了Namespace ,Name,Attribute 及相关的 Index 信息,其中 Attribute 中有用自己的 Name Value等具体封装。

ResourceMapChunk

ResourceMapChunk是一个 32 位的 Int 数组,在我们编写的 XML 中没有直观体现,但是在编译为二进制文件后,它的确存在,也是我们后续能执行裁剪属性名的重要依据:它与 String Pool 中的资源定义相匹配。

NameSpaceChunk

图片图片

NameSpaceChunk 就是对 Namespace 的封装,主要包含了前缀(Android Tools  App),和具体的 URL。

ResourceType.h 文件中定义了所以需要使用的类型,也是面向对象的封装形式。后面讲解析时,也会根据每种数据类型进行具体的解析处理。

四、XML 解析过程举例

我们以获取 StringPool 的场景来举例二进制文件的解析过程,通过这个过程,可以掌握字节读取的具体实现。解析过程其实就是从 0 开始的字节偏移量获取。每次读取多少字节,依赖前面 ResourceTypes.h 中的格式定义。

图片图片

图片图片

第一行

00000000  03 00 08 00 54 02 00 00   01 00 1c 00 e4 00 00 00  |....T...........| 00 03       XML 类型 00 08       header size 54 02 00 00 Chunksize (0254 596) 00 01 : StringPool 00 1c headersize (28) 00 00 00 e4 :Chunksize (228) 

第二行 

00000010  0b 00 00 00 00 00 00 00    00 01 00 00 48 00 00 00  |............H...| 00 00 00 0b : stringCount (getInt) 11 00 00 00 00 : styleCount (getInt) 0 00 00 01 00 : flags (getInt)  1 使用 UTF-8 00 00 00 48 : StringStart (getInt) 72

第三行 

00000020  00 00 00 00 00(indx 36) 00 00 00  0b 00 00 00 17 00 00 00  |................| 00 00 00 00 : styleStart(getInt) 0  (StringPoolChunk 中最后一个字段获取) 00(index 36) 00 00 00 : readStrings 第一次偏移 0 (72 + 8 从 index 80 开始)

0b 00 00 00: readStrings 第二次偏移 11 (80+11 从 91 开始)

00 00 00 17:readString 第三次偏移 23 (80 +23 从 103 开始)

第四行

00000030  1c 00 00 00 2b 00 00 00    3b 00 00 00 42 00 00 00  |....+...;...B...|

00 00 00 1c:readString 第四次偏移 28 (80+28 从 108 开始)

00 00 00 2b:readString 第五次偏移 43

第六行 

00000050  08(index 80) 08 74 65 78 74 53 69  7a 65 00 09(index 91) 09 74 65 78  |..textSize...tex|

第七行 

00000060  74 43 6f 6c 6f 72 00 02(index 103)  02 69 64 00 0c(index 108) 0c 6c 61  |tColor...id...la|

第八行

00000070  79 6f 75 74 5f 77 69 64  74 68 00 0d 0d 6c 61 79  |yout_width...lay|

工具介绍

通过上面的手动解析二进制文件字节信息,既然格式如此固定,那多半已经有人做过相关封装解析类吧,请看JakeWharton:https://github.com/madisp/android-chunk-utils

API 介绍

图片图片

StringPoolChunk 封装