当前位置:首页 > 科技  > 软件

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

来源: 责编: 时间:2023-09-20 21:56:03 262观看
导读一、引言得物App在包体积优化方面已经进行了诸多尝试,收获也颇丰,已经集成的方案有图片压缩、重复资源删除、ARSC压缩等可移步至得物 Android 包体积资源优化实践。本文将主要介绍基于 XML 二进制文件的裁剪优化。在正

一、引言

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

在正式进入裁剪优化前,需要先做准备工作,我们先从上层的代码看起,看看布局填充的方法。方便我们从始到终了解整个情况。edI28资讯网——每日最新资讯28at.com

二、XML 解析流程

在 LayoutInflater 调用 Inflate 方法后,会将 XML 中的属性包装至 LayoutParams 中最后通过反射使用创建对应 View。edI28资讯网——每日最新资讯28at.com

而在反射前,传入的 R.layout.xxx 文件是如何完成 XML 解析类的创建,后续又是如何通过该类完成 XML 中的数据解析呢?edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

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

图片图片edI28资讯网——每日最新资讯28at.com

XmlBlock 内部的 Parse 类实现了 XmlResourceParser ,最终被包装为 AttributeSet 接口返回。edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

例如调用 AttributeSet 的方法:edI28资讯网——每日最新资讯28at.com

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

图片图片edI28资讯网——每日最新资讯28at.com

//core/jni/android_util_XmlBlock.cppedI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

可以看到,我们最终都是通过 ResXmlParser 类传入对应的 ID 来完成取值。而不是通过具体的属性名称来进行取值。edI28资讯网——每日最新资讯28at.com

上面介绍的是直接通过 Attrs 取值的方式,在实际开发中我们通常会使用 TypedArray 来进行相关属性值的获取。例如 FrameLayout 的创建工程。edI28资讯网——每日最新资讯28at.com

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 方法。edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

//https://android.googlesource.com/platform/frameworks/base/+/6d0e2c9cb948a10137e6b5a4eb00e601622fe8ee/core/jni/android_util_AssetManager.cppstatic 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.cppssize_t ResXMLParser::getAttributeValue(size_t idx, Res_value* outValue) const{    if (mEventCode == START_TAG) {        const ResXMLTree_attrExt* tag = (const ResXMLTree_attrExt*)mCurExt;        if (idx < dtohs(tag->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文件,转换位二进制文件后就是这个样子。edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

要了解这个二进制文件,使用 命令行 hexdump  查看:edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

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

https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/libs/androidfw/include/androidfw/ResourceTypes.hedI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

每一块(Chunk)都按固定格式生成,最基础的定义有:edI28资讯网——每日最新资讯28at.com

Type:类型 分类,对应上面截图中的类型edI28资讯网——每日最新资讯28at.com

headerSize:头信息大小edI28资讯网——每日最新资讯28at.com

Size:总大小 (headerSize+dataSize)通过这个值,你可以跳过该 Chunk 的内容,如果 Size 和 headerSize 一致,说明该 Chunk 没有数据内容。edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

StringPoolChunk

edI28资讯网——每日最新资讯28at.com

所有的 Chunk 中,都包含 ResChunk ,作为基础信息。这里以 StringPoolChunk 举例:

edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

在 StringPool 中,除了基础的 ResChunk ,还额外包含以下信息:edI28资讯网——每日最新资讯28at.com

stringCount: 字符串常量池的总数量edI28资讯网——每日最新资讯28at.com

styleCount: style 相关的的总数量edI28资讯网——每日最新资讯28at.com

Flag: UTF_8 或者 UTF_16 的标志位  我们这里默认就是 UTF_8edI28资讯网——每日最新资讯28at.com

stringsStart:字符串开始的位置edI28资讯网——每日最新资讯28at.com

stylesStart:styles 开始的位置edI28资讯网——每日最新资讯28at.com

字符串从 stringStart 的位置相对开始,两个字节来表示长度,最后以 0 结束。edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

XmlStartElementChunk

edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

startElementChunk 是布局 XML 中核心的标签封装对象,里面记录了Namespace ,Name,Attribute 及相关的 Index 信息,其中 Attribute 中有用自己的 Name Value等具体封装。edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

ResourceMapChunk

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

edI28资讯网——每日最新资讯28at.com

NameSpaceChunk

edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

NameSpaceChunk 就是对 Namespace 的封装,主要包含了前缀(Android Tools  App),和具体的 URL。edI28资讯网——每日最新资讯28at.com

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

四、XML 解析过程举例

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

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

第一行edI28资讯网——每日最新资讯28at.com

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) edI28资讯网——每日最新资讯28at.com

第二行 edI28资讯网——每日最新资讯28at.com

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) 72edI28资讯网——每日最新资讯28at.com

第三行 edI28资讯网——每日最新资讯28at.com

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 开始)edI28资讯网——每日最新资讯28at.com

0b 00 00 00: readStrings 第二次偏移 11 (80+11 从 91 开始)edI28资讯网——每日最新资讯28at.com

00 00 00 17:readString 第三次偏移 23 (80 +23 从 103 开始)edI28资讯网——每日最新资讯28at.com

第四行edI28资讯网——每日最新资讯28at.com

00000030  1c 00 00 00 2b 00 00 00    3b 00 00 00 42 00 00 00  |....+...;...B...|edI28资讯网——每日最新资讯28at.com

00 00 00 1c:readString 第四次偏移 28 (80+28 从 108 开始)edI28资讯网——每日最新资讯28at.com

00 00 00 2b:readString 第五次偏移 43edI28资讯网——每日最新资讯28at.com

第六行 edI28资讯网——每日最新资讯28at.com

00000050  08(index 80) 08 74 65 78 74 53 69  7a 65 00 09(index 91) 09 74 65 78  |..textSize...tex|edI28资讯网——每日最新资讯28at.com

第七行 edI28资讯网——每日最新资讯28at.com

00000060  74 43 6f 6c 6f 72 00 02(index 103)  02 69 64 00 0c(index 108) 0c 6c 61  |tColor...id...la|edI28资讯网——每日最新资讯28at.com

第八行edI28资讯网——每日最新资讯28at.com

00000070  79 6f 75 74 5f 77 69 64  74 68 00 0d 0d 6c 61 79  |yout_width...lay|edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

工具介绍edI28资讯网——每日最新资讯28at.com

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

edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

API 介绍

图片图片edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

StringPoolChunk 封装

edI28资讯网——每日最新资讯28at.com

String 按之前手动解析的思路,是通过偏移量获取 String 的开始位置及具体长度,完成不同 String 的读取。
protected Chunk(ByteBuffer buffer, @Nullable Chunk parent) {  this.parent = parent;  offset = buffer.position() - 2;  headerSize = (buffer.getShort() & 0xFFFF);  chunkSize = buffer.getInt();}//StringPoolChunkprotected StringPoolChunk(ByteBuffer buffer, @Nullable Chunk parent) {  super(buffer, parent);  stringCount = buffer.getInt();  styleCount = buffer.getInt();  flags        = buffer.getInt();  stringsStart = buffer.getInt();  stylesStart  = buffer.getInt();}// StringPoolChunk@Overrideprotected void init(ByteBuffer buffer) {  super.init(buffer);  strings.addAll(readStrings(buffer, offset + stringsStart, stringCount));  styles.addAll(readStyles(buffer, offset + stylesStart, styleCount));}private List<String> readStrings(ByteBuffer buffer, int offset, int count) {  List<String> result = new ArrayList<>();  int previousOffset = -1;  // After the header, we now have an array of offsets for the strings in this pool.  for (int i = 0; i < count; ++i) {    int stringOffset = offset + buffer.getInt();    result.add(ResourceString.decodeString(buffer, stringOffset, getStringType()));    if (stringOffset <= previousOffset) {      isOriginalDeduped = true;    }    previousOffset = stringOffset;  }  return result;}public static String decodeString(ByteBuffer buffer, int offset, Type type) {  int length;  int characterCount = decodeLength(buffer, offset, type);  offset += computeLengthOffset(characterCount, type);  // UTF-8 strings have 2 lengths: the number of characters, and then the encoding length.  // UTF-16 strings, however, only have 1 length: the number of characters.  if (type == Type.UTF8) {    length = decodeLength(buffer, offset, type);    offset += computeLengthOffset(length, type);  } else {    length = characterCount * 2;  }  return new String(buffer.array(), offset, length, type.charset());}

edI28资讯网——每日最新资讯28at.com

ResourceMapChunk 封装

edI28资讯网——每日最新资讯28at.com

资源 ID 对比 String 显得更加简单,因为它的长度固定为的 32 位 4 字节,所以用 dataSize 除以 4 就可以得到ResourceMap 的大小,然后依次调用 buffer.getInt() 方法获取即可。edI28资讯网——每日最新资讯28at.com

ResourceMap封装过程:private List<Integer> enumerateResources(ByteBuffer buffer) {  // id 固定为 4 个字节  int resourceCount = (getOriginalChunkSize() - getHeaderSize()) / RESOURCE_SIZE;   List<Integer> result = new ArrayList<>(resourceCount);  int offset = this.offset + getHeaderSize();  buffer.mark();  buffer.position(offset);  for (int i = 0; i < resourceCount; ++i) {    result.add(buffer.getInt());  }  buffer.reset();  return result;}

edI28资讯网——每日最新资讯28at.com

XmlStartElementChunk 封装

edI28资讯网——每日最新资讯28at.com

protected XmlStartElementChunk(ByteBuffer buffer, @Nullable Chunk parent) {  super(buffer, parent);  // 获取namespace的id  namespace = buffer.getInt();  // 获取名称  name = buffer.getInt();  // 获取属性索引的开始位置  attributeStart = (buffer.getShort() & 0xFFFF);  // 获取索引的总大小  int attributeSize = (buffer.getShort() & 0xFFFF);  // 强制检查 attributeSize 的值是否为固定值,  Preconditions.checkState(attributeSize == XmlAttribute.SIZE, // 20       "attributeSize is wrong size. Got %s, want %s", attributeSize, XmlAttribute.SIZE);  attributeCount = (buffer.getShort() & 0xFFFF);  // The following indices are 1-based and need to be adjusted.  idIndex = (buffer.getShort() & 0xFFFF) - 1;  classIndex = (buffer.getShort() & 0xFFFF) - 1;  styleIndex = (buffer.getShort() & 0xFFFF) - 1;}private List<XmlAttribute> enumerateAttributes(ByteBuffer buffer) {  List<XmlAttribute> result = new ArrayList<>(attributeCount);  int offset = this.offset + getHeaderSize() + attributeStart;  int endOffset = offset + XmlAttribute.SIZE * attributeCount;  buffer.mark();  buffer.position(offset);  while (offset < endOffset) {    result.add(XmlAttribute.create(buffer, this));    offset += XmlAttribute.SIZE;  }  buffer.reset();  return result;}
/** * Creates a new {@link XmlAttribute} based on the bytes at the current {@code buffer} position. * * @param buffer A buffer whose position is at the start of a {@link XmlAttribute}. * @param parent The parent chunk that contains this attribute; used for string lookups. */public static XmlAttribute create(ByteBuffer buffer, XmlNodeChunk parent) {  int namespace = buffer.getInt(); // 4  int name = buffer.getInt(); // 4  int rawValue = buffer.getInt(); // 4  ResourceValue typedValue = ResourceValue.create(buffer);  return new AutoValue_XmlAttribute(namespace, name, rawValue, typedValue, parent);}public static ResourceValue create(ByteBuffer buffer) {  int size = (buffer.getShort() & 0xFFFF); //2  buffer.get();  // Unused // Always set to 0. 1    Type type = Type.fromCode(buffer.get());//1  int data = buffer.getInt(); // 4  return new AutoValue_ResourceValue(size, type, data);}

六、细节问题

edI28资讯网——每日最新资讯28at.com

Style 对应问题

edI28资讯网——每日最新资讯28at.com

在 StringPool 中我们可以看到除了 String 的定义,这里还有 Style 的定义,那么这个 Style 到底对应什么呢?经过一番测试后,Style 其实对应 Strings.xml 中定义的富文本内容,例如:
<string name="spannable_string">  This is a <b>bold</b> text, this is an <i>italic</i></string>

这种内容,最后在 解析 Arsc 文件时,就会有有 Style 相关的属性。edI28资讯网——每日最新资讯28at.com

我们注意主要聚焦于 Layout 文件,所以这里不再展开分析。edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

字节存储方式

edI28资讯网——每日最新资讯28at.com

Little Endian:低位字节序edI28资讯网——每日最新资讯28at.com

Big Endian:高位字节序edI28资讯网——每日最新资讯28at.com

在 Little Endian 字节序中,数据的最低有效字节存储在内存地址的最低位置,而最高有效字节则存储在内存地址的最高位置。这种字节序的优点是可以更好地利用内存,能够更容易地处理低位字节和高位字节的组合,尤其是在处理较大的整数和浮点数时比较快速。edI28资讯网——每日最新资讯28at.com

在 Big Endian 字节序中,数据的最高有效字节存储在内存地址的最低位置,而最低有效字节则存储在内存地址的最高位置。这种字节序虽然更符合人类读写的方式,但在高效率方面却不如 Little Endian 字节序。edI28资讯网——每日最新资讯28at.com

举例 0x12345678edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

低位存储edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

高位存储edI28资讯网——每日最新资讯28at.com

在 Java 中,默认采用 Big Endian 存储方式,所以我们修改二进制文件时,需要手动指定为低位字节序。edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

七、裁剪优化实现

edI28资讯网——每日最新资讯28at.com

Namespace 移除

edI28资讯网——每日最新资讯28at.com

将字符串池中命名空间字符串替换成""空串,以达到减少文件内容的效果。有上面 NameSpaceChunk 中的介绍可知,nameSpace 的前缀及具体 URL 都存放在 StringPool 中,具体内容(字节,偏移量)封装在  NameSpaceChunk 中。所以需要执行两个步骤:1. 移除 NameSpaceChunk 对象 2. 置空 StringPool 中的字符串内容。

第一步使用 Android-Chunk-Utils 代码如下,第二步和属性名移除并列执行。edI28资讯网——每日最新资讯28at.com

FileInputStream(resourcesFile).use { inputStream ->    val resouce = ResourceFile.fromInputStream(inputStream)    val chunks = sChunk.chunks    // 过滤出所有的 NameSpaceChunk 对象    val result = chunks.values.filter { it is XmlNamespaceChunk }    // 移除    chunks.values.removeAll(result.toSet())}

edI28资讯网——每日最新资讯28at.com

属性名移除

edI28资讯网——每日最新资讯28at.com

将字符串池中每一个字符串替换成""空字符串。

StringPoolChunk 中记录了 XML 中的所有组件名称及其属性,而每个属性对应的具体 ID ,则是固定的,在ResourceMapChunk 中,由 Index 一一对应。edI28资讯网——每日最新资讯28at.com

图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

举个例子,在这个布局文件中, Layout_width 的在 StringPool 中的索引是 6 ,对应在 ResourceMapChunk 中是 16842996 的值,转换十六进制后:10100f4,与 public.xml 中定义的属性 ID 完全对应。edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

通过上面源码的介绍,每个属性(Attr)包含一个对应的整型 ID 值,获取其属性值时都会通过该 ID 值来获取。所以对应的属性名理论上可以移除。具体代码如下:edI28资讯网——每日最新资讯28at.com

private fun handleStringPoolValue(strings: MutableList<String>, resources: MutableList<Int>?, stringPoolChunk: StringPoolChunk, emptyIndexs: MutableList<Int>) {    strings.forEachIndexed { i, k ->        val res = resources        // 默认属性置空        if (res != null && i < res.size) {            stringPoolChunk.setString(i, "")            emptyIndexs.add(i)        }        // 命名空间置空        else if (k == "http://schemas.android.com/apk/res/android") {            stringPoolChunk.setString(i, "")            emptyIndexs.add(i)        } else if (k == "http://schemas.android.com/apk/res-auto") {            stringPoolChunk.setString(i, "")            emptyIndexs.add(i)        } else if (k == "http://schemas.android.com/tools") {            stringPoolChunk.setString(i, "")            emptyIndexs.add(i)        } else if (k == "android") {            stringPoolChunk.setString(i, "")            emptyIndexs.add(i)        } else if (k == "app") {            stringPoolChunk.setString(i, "")            emptyIndexs.add(i)        } else if (k == "tools") {            stringPoolChunk.setString(i, "")            emptyIndexs.add(i)        }    }}

edI28资讯网——每日最新资讯28at.com

Stringpool 偏移量修改

edI28资讯网——每日最新资讯28at.com

经过上面两个步骤后,StringPool 已经得到优化,但是观察新的二进制文件会发现,目前总大小为:00 00 01 c4 (452) ,优化前为 00 00 02 54(596)。经过进一步分析可以发现,StringPool 中字符串的偏移量还可以优化。

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

第二行edI28资讯网——每日最新资讯28at.com

00000010  0b 00 00 00 00 00 00 00  00 01 00 00 48 00 00 00  |............H...|edI28资讯网——每日最新资讯28at.com

第三行edI28资讯网——每日最新资讯28at.com

00000020  00 00 00 00 00(index 36) 00 00 00  03 00 00 00 06 00 00 00  |................|edI28资讯网——每日最新资讯28at.com

第四行edI28资讯网——每日最新资讯28at.com

00000030  09 00 00 00 0c 00 00 00  0f 00 00 00 12 00 00 00  |................|edI28资讯网——每日最新资讯28at.com

第六行edI28资讯网——每日最新资讯28at.com

00000050  00(index 80) 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|edI28资讯网——每日最新资讯28at.com

可以看到空字符串第一个偏移量是 0: 72+8 80 ,从 80 开始,每一个空字符串都由一组 00 00 00 来表示,这里也会有冗余的存储占用。那这里是否可以控制偏移量,就用一组 00 00 00 来表示呢?答案是可以的, Android-Chunk-Utils 工具类已经给我们提供了策略支持。ResourceFile.toByteArray 回写方法就提供了 Shrink 参数。edI28资讯网——每日最新资讯28at.com

FileOutputStream(resourcesFile).use {    it.write(newResouce.toByteArray(true))}@Overridepublic byte[] toByteArray(boolean shrink) throws IOException {  ByteArrayDataOutput output = ByteStreams.newDataOutput();  for (Chunk chunk : chunks) {    output.write(chunk.toByteArray(shrink));  }  return output.toByteArray();}
//StringPoolChunk 中的具体写入实现@Overrideprotected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)    throws IOException {  ByteArrayOutputStream baos = new ByteArrayOutputStream();  int stringOffset = 0;  ByteBuffer offsets = ByteBuffer.allocate(getOffsetSize());  offsets.order(ByteOrder.LITTLE_ENDIAN);  // Write to a temporary payload so we can rearrange this and put the offsets first  try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) {    stringOffset = writeStrings(payload, offsets, shrink);    writeStyles(payload, offsets, shrink);  }  output.write(offsets.array());  output.write(baos.toByteArray());  if (!styles.isEmpty()) {    header.putInt(STYLE_START_OFFSET, getHeaderSize() + getOffsetSize() + stringOffset);  }}private int writeStrings(DataOutput payload, ByteBuffer offsets, boolean shrink)    throws IOException {  int stringOffset = 0;  Map<String, Integer> used = new HashMap<>();  // Keeps track of strings already written  for (String string : strings) {    // Dedupe everything except stylized strings, unless shrink is true (then dedupe everything)    if (used.containsKey(string) && (shrink || isOriginalDeduped)) {      // 如果支持优化,将复用之前的数据和 offest      Integer offset = used.get(string);      offsets.putInt(offset == null ? 0 : offset);    } else {      byte[] encodedString = ResourceString.encodeString(string, getStringType());      payload.write(encodedString);      used.put(string, stringOffset);      offsets.putInt(stringOffset);      stringOffset += encodedString.length;    }  }

经过三步优化,重新更新 XML 文件后再次确定二进制信息,获取 Chunck 的总大小为:00 00 01 b0 (432),对比原始 XML 文件,一共减少 164 (28% )。当然这个减少数据量取决于 XML 中标签及属性的数量,越复杂的 XML 文件,缩减率越高。edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

效果对比

edI28资讯网——每日最新资讯28at.com

裁剪前裁剪前edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

裁剪后edI28资讯网——每日最新资讯28at.com

八、API 兼容调整

虽然理论上说移除布局的属性后对于正常的流程无影响,但是,该有的问题总还是会有的,真一个问题都没有那才让人心里不踏实,接下来看兼容的一些异常情况。

edI28资讯网——每日最新资讯28at.com

TabLayout 获取 Height 的场景

edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

这个写法同时使用了 Namespace 和 特定属性,布局初始化时直接就会 Crash 。后面扫描了所有使用 getAttributeValue 方法的类,筛选确定后进行统一代码调整。edI28资讯网——每日最新资讯28at.com

int[] systemAttrs = {android.R.attr.layout_height};TypedArray a = context.obtainStyledAttributes(attrs, systemAttrs);try {    // 如果定义的是 WRAP_CONTENT 或者 MATCH_PARENT 这里会异常,然后通过 getInt 获取值(-1 -2)    mHeight = a.getDimensionPixelSize(0, ViewGroup.LayoutParams.WRAP_CONTENT);} catch (Exception e) {    // e.printStackTrace();    mHeight = a.getInt(0, ViewGroup.LayoutParams.WRAP_CONTENT);}

edI28资讯网——每日最新资讯28at.com

图片库获取 SRC 的场景

edI28资讯网——每日最新资讯28at.com

图片库内部默认支持了 ImageView 的 SRC 属性,具体获取方式使用了 getAttributeResourceValue 的方法。

图片图片edI28资讯网——每日最新资讯28at.com

因为图片库调用的地方做了默认 Catch 捕获异常,所以 App 没有 Crash ,但是对于使用 SRC 属性设置的图片资源无法正常显示。edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

后续调整为:edI28资讯网——每日最新资讯28at.com

try {    val a = context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.src))    if (a.hasValue(0)) {        val drawable = a.getDrawable(0)        load(drawable)    }    a.recycle()} catch (e: Exception) {    e.printStackTrace()}

edI28资讯网——每日最新资讯28at.com

DuToolbar 获取 Theme 的场景edI28资讯网——每日最新资讯28at.com

edI28资讯网——每日最新资讯28at.com

我们的 Toolbar 有统一拦截设置,其中支持根据页面设置是黑色或者白色的返回按钮样式,而这个设置就是通过 XML 中 Theme 主题关联。这是之前的判断,可以看到直接使用属性名来判断。由于属性移除,该判断条件永远执行不到,导致 Toolbar 返回按钮在特定页面未按预期颜色展示。

图片图片edI28资讯网——每日最新资讯28at.com

图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

调整为:edI28资讯网——每日最新资讯28at.com

if (attrs != null) {    TypedArray a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.theme});    if (a.hasValue(0)) {        int[] attr = new int[]{R.attr.colorControlNormal, android.R.attr.textColorPrimary};        TypedArray array = context.obtainStyledAttributes(attrs.getAttributeResourceValue(0, 0), attr);        try {            mNavigationIconTintColor = array.getColor(0, Color.BLACK);            mTitleTextColor = array.getColor(1, Color.BLACK);        } finally {            array.recycle();        }    }    a.recycle();}

修改后发现依然有问题,对比异常的不同页面发现以下区别:edI28资讯网——每日最新资讯28at.com

<androidx.appcompat.widget.Toolbar    android:id="@+id/toolbar"    android:layout_width="match_parent"    android:layout_height="?attr/actionBarSize"    app:layout_collapseMode="pin"    app:popupTheme="@style/ThemeToolbarWhite"    app:theme="@style/ThemeToolbarWhite"><androidx.appcompat.widget.Toolbar    android:id="@+id/toolbar"    android:layout_width="match_parent"    android:layout_height="?attr/actionBarSize"    android:theme="@style/ThemeToolbarWhite"    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"    app:title="领取数字藏品"    app:titleTextColor="@android:color/white" />

使用 Android 命名空间生成的属性是系统自带属性,定义在 public.xml 中,使用 App 生成的属性是自定义属性,打包到 Arsc 的 Attr 中。edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

所以,上面仅判断 Android.R.attr.theme 不够,还需要增加 R.attr.theme 。edI28资讯网——每日最新资讯28at.com

TypedArray a = context.obtainStyledAttributes(attrs, new int[]{R.attr.theme, android.R.attr.theme});

九、收益

最后,确定下总体包体积优化收益:edI28资讯网——每日最新资讯28at.com

移除 Namespace 及属性值后:edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

优化空字符串后:edI28资讯网——每日最新资讯28at.com

图片图片edI28资讯网——每日最新资讯28at.com

由于这是 Apk 解压后的所有文件汇总收益,重新压缩打包 Apk 后,包体积整体收益在 2.2 M左右。edI28资讯网——每日最新资讯28at.com

图片edI28资讯网——每日最新资讯28at.com

十、总结

本文介绍了得物App的包体积优化工作,讲解了针对XML二进制文件的裁剪优化。文章首先概述了XML解析流程和XML二进制文件格式,然后介绍了解析过程中的一些工具以及细节问题,探讨了裁剪优化实现以及API的兼容调整,最后呈现了包体积优化的收益。edI28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-10542-0.html包体积:Layout 二进制文件裁剪优化

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 为什么在 C++14 中删除了 get 函数?

下一篇: 冠闵信息CEO颜伟志谈MSP发展趋势:利用各种工具持续交付高度自动化的服务

标签:
  • 热门焦点
Top