包體積:Layout 二進(jìn)制文件裁剪優(yōu)化
一、引言
得物App在包體積優(yōu)化方面已經(jīng)進(jìn)行了諸多嘗試,收獲也頗豐,已經(jīng)集成的方案有圖片壓縮、重復(fù)資源刪除、ARSC壓縮等可移步至得物 Android 包體積資源優(yōu)化實(shí)踐。本文將主要介紹基于 XML 二進(jìn)制文件的裁剪優(yōu)化。
在正式進(jìn)入裁剪優(yōu)化前,需要先做準(zhǔn)備工作,我們先從上層的代碼看起,看看布局填充的方法。方便我們從始到終了解整個(gè)情況。
二、XML 解析流程
在 LayoutInflater 調(diào)用 Inflate 方法后,會(huì)將 XML 中的屬性包裝至 LayoutParams 中最后通過(guò)反射使用創(chuàng)建對(duì)應(yīng) View。
而在反射前,傳入的 R.layout.xxx 文件是如何完成 XML 解析類的創(chuàng)建,后續(xù)又是如何通過(guò)該類完成 XML 中的數(shù)據(jù)解析呢?
圖片
圖片
圖片
圖片
上層 XML 解析最終會(huì)封裝到 XmlBlock 這個(gè)類中。XmlBlock 封裝了具體 RES 文件的解析數(shù)據(jù)。其中 nativeOpenXmlAsset 返回的就是 c 中對(duì)應(yīng)的文件指針,后續(xù)取值都需要通過(guò)這個(gè)指針去操作。
圖片
XmlBlock 內(nèi)部的 Parse 類實(shí)現(xiàn)了 XmlResourceParser ,最終被包裝為 AttributeSet 接口返回。
圖片
例如調(diào)用 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")
}
最終就會(huì)調(diào)用到 XmlResourceParser 中的方法,最終調(diào)用到 Native 中。
圖片
//core/jni/android_util_XmlBlock.cpp
圖片
可以看到,我們最終都是通過(guò) ResXmlParser 類傳入對(duì)應(yīng)的 ID 來(lái)完成取值。而不是通過(guò)具體的屬性名稱來(lái)進(jìn)行取值。
上面介紹的是直接通過(guò) Attrs 取值的方式,在實(shí)際開(kāi)發(fā)中我們通常會(huì)使用 TypedArray 來(lái)進(jìn)行相關(guān)屬性值的獲取。例如 FrameLayout 的創(chuàng)建工程。
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 方法最終會(huì)調(diào)用到 AssetManager 中的 applyStyle 方法,最終調(diào)用到 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 < 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 二進(jìn)制文件格式
你寫(xiě)的代碼是這個(gè)樣子,App 打包過(guò)程中通過(guò) AAPT2 工具處理完 XML文件,轉(zhuǎn)換位二進(jìn)制文件后就是這個(gè)樣子。
圖片
圖片
要了解這個(gè)二進(jìn)制文件,使用 命令行 hexdump 查看:
圖片
在二進(jìn)制文件中,不同數(shù)據(jù)類型分塊存儲(chǔ),共同組成一個(gè)完整文件。我們可以通過(guò)依次讀取每個(gè)字節(jié),來(lái)獲取對(duì)應(yīng)的信息。要準(zhǔn)確讀取信息,就必須清楚它的定義規(guī)則和順序,確保可以正確讀取出內(nèi)容。
https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/libs/androidfw/include/androidfw/ResourceTypes.h
圖片
圖片
每一塊(Chunk)都按固定格式生成,最基礎(chǔ)的定義有:
Type:類型 分類,對(duì)應(yīng)上面截圖中的類型
headerSize:頭信息大小
Size:總大小 (headerSize+dataSize)通過(guò)這個(gè)值,你可以跳過(guò)該 Chunk 的內(nèi)容,如果 Size 和 headerSize 一致,說(shuō)明該 Chunk 沒(méi)有數(shù)據(jù)內(nèi)容。
StringPoolChunk
在 StringPool 中,除了基礎(chǔ)的 ResChunk ,還額外包含以下信息:
stringCount: 字符串常量池的總數(shù)量
styleCount: style 相關(guān)的的總數(shù)量
Flag: UTF_8 或者 UTF_16 的標(biāo)志位 我們這里默認(rèn)就是 UTF_8
stringsStart:字符串開(kāi)始的位置
stylesStart:styles 開(kāi)始的位置
字符串從 stringStart 的位置相對(duì)開(kāi)始,兩個(gè)字節(jié)來(lái)表示長(zhǎng)度,最后以 0 結(jié)束。
XmlStartElementChunk
圖片
startElementChunk 是布局 XML 中核心的標(biāo)簽封裝對(duì)象,里面記錄了Namespace ,Name,Attribute 及相關(guān)的 Index 信息,其中 Attribute 中有用自己的 Name Value等具體封裝。
ResourceMapChunk
ResourceMapChunk是一個(gè) 32 位的 Int 數(shù)組,在我們編寫(xiě)的 XML 中沒(méi)有直觀體現(xiàn),但是在編譯為二進(jìn)制文件后,它的確存在,也是我們后續(xù)能執(zhí)行裁剪屬性名的重要依據(jù):它與 String Pool 中的資源定義相匹配。
NameSpaceChunk
圖片
NameSpaceChunk 就是對(duì) Namespace 的封裝,主要包含了前綴(Android Tools App),和具體的 URL。
ResourceType.h 文件中定義了所以需要使用的類型,也是面向?qū)ο蟮姆庋b形式。后面講解析時(shí),也會(huì)根據(jù)每種數(shù)據(jù)類型進(jìn)行具體的解析處理。
四、XML 解析過(guò)程舉例
我們以獲取 StringPool 的場(chǎng)景來(lái)舉例二進(jìn)制文件的解析過(guò)程,通過(guò)這個(gè)過(guò)程,可以掌握字節(jié)讀取的具體實(shí)現(xiàn)。解析過(guò)程其實(shí)就是從 0 開(kāi)始的字節(jié)偏移量獲取。每次讀取多少字節(jié),依賴前面 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 中最后一個(gè)字段獲?。?00(index 36) 00 00 00 : readStrings 第一次偏移 0 (72 + 8 從 index 80 開(kāi)始)
0b 00 00 00: readStrings 第二次偏移 11 (80+11 從 91 開(kāi)始)
00 00 00 17:readString 第三次偏移 23 (80 +23 從 103 開(kāi)始)
第四行
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 開(kāi)始)
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|
五
工具介紹
通過(guò)上面的手動(dòng)解析二進(jìn)制文件字節(jié)信息,既然格式如此固定,那多半已經(jīng)有人做過(guò)相關(guān)封裝解析類吧,請(qǐng)看JakeWharton:https://github.com/madisp/android-chunk-utils
API 介紹
圖片
StringPoolChunk 封裝
protected Chunk(ByteBuffer buffer, @Nullable Chunk parent) {
this.parent = parent;
offset = buffer.position() - 2;
headerSize = (buffer.getShort() & 0xFFFF);
chunkSize = buffer.getInt();
}
//StringPoolChunk
protected 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
@Override
protected 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());
}
ResourceMapChunk 封裝
資源 ID 對(duì)比 String 顯得更加簡(jiǎn)單,因?yàn)樗拈L(zhǎng)度固定為的 32 位 4 字節(jié),所以用 dataSize 除以 4 就可以得到ResourceMap 的大小,然后依次調(diào)用 buffer.getInt() 方法獲取即可。
ResourceMap封裝過(guò)程:
private List<Integer> enumerateResources(ByteBuffer buffer) {
// id 固定為 4 個(gè)字節(jié)
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;
}
XmlStartElementChunk 封裝
protected XmlStartElementChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
// 獲取namespace的id
namespace = buffer.getInt();
// 獲取名稱
name = buffer.getInt();
// 獲取屬性索引的開(kāi)始位置
attributeStart = (buffer.getShort() & 0xFFFF);
// 獲取索引的總大小
int attributeSize = (buffer.getShort() & 0xFFFF);
// 強(qiáng)制檢查 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);
}
六、細(xì)節(jié)問(wèn)題
Style 對(duì)應(yīng)問(wèn)題
<string name="spannable_string">
This is a <b>bold</b> text, this is an <i>italic</i>
</string>
這種內(nèi)容,最后在 解析 Arsc 文件時(shí),就會(huì)有有 Style 相關(guān)的屬性。
我們注意主要聚焦于 Layout 文件,所以這里不再展開(kāi)分析。
字節(jié)存儲(chǔ)方式
Little Endian:低位字節(jié)序
Big Endian:高位字節(jié)序
在 Little Endian 字節(jié)序中,數(shù)據(jù)的最低有效字節(jié)存儲(chǔ)在內(nèi)存地址的最低位置,而最高有效字節(jié)則存儲(chǔ)在內(nèi)存地址的最高位置。這種字節(jié)序的優(yōu)點(diǎn)是可以更好地利用內(nèi)存,能夠更容易地處理低位字節(jié)和高位字節(jié)的組合,尤其是在處理較大的整數(shù)和浮點(diǎn)數(shù)時(shí)比較快速。
在 Big Endian 字節(jié)序中,數(shù)據(jù)的最高有效字節(jié)存儲(chǔ)在內(nèi)存地址的最低位置,而最低有效字節(jié)則存儲(chǔ)在內(nèi)存地址的最高位置。這種字節(jié)序雖然更符合人類讀寫(xiě)的方式,但在高效率方面卻不如 Little Endian 字節(jié)序。
舉例 0x12345678
圖片
低位存儲(chǔ)
圖片
高位存儲(chǔ)
在 Java 中,默認(rèn)采用 Big Endian 存儲(chǔ)方式,所以我們修改二進(jìn)制文件時(shí),需要手動(dòng)指定為低位字節(jié)序。
圖片
圖片
七、裁剪優(yōu)化實(shí)現(xiàn)
Namespace 移除
第一步使用 Android-Chunk-Utils 代碼如下,第二步和屬性名移除并列執(zhí)行。
FileInputStream(resourcesFile).use { inputStream ->
val resouce = ResourceFile.fromInputStream(inputStream)
val chunks = sChunk.chunks
// 過(guò)濾出所有的 NameSpaceChunk 對(duì)象
val result = chunks.values.filter { it is XmlNamespaceChunk }
// 移除
chunks.values.removeAll(result.toSet())
}
屬性名移除
StringPoolChunk 中記錄了 XML 中的所有組件名稱及其屬性,而每個(gè)屬性對(duì)應(yīng)的具體 ID ,則是固定的,在ResourceMapChunk 中,由 Index 一一對(duì)應(yīng)。
圖片
舉個(gè)例子,在這個(gè)布局文件中, Layout_width 的在 StringPool 中的索引是 6 ,對(duì)應(yīng)在 ResourceMapChunk 中是 16842996 的值,轉(zhuǎn)換十六進(jìn)制后:10100f4,與 public.xml 中定義的屬性 ID 完全對(duì)應(yīng)。
圖片
通過(guò)上面源碼的介紹,每個(gè)屬性(Attr)包含一個(gè)對(duì)應(yīng)的整型 ID 值,獲取其屬性值時(shí)都會(huì)通過(guò)該 ID 值來(lái)獲取。所以對(duì)應(yīng)的屬性名理論上可以移除。具體代碼如下:
private fun handleStringPoolValue(strings: MutableList<String>, resources: MutableList<Int>?, stringPoolChunk: StringPoolChunk, emptyIndexs: MutableList<Int>) {
strings.forEachIndexed { i, k ->
val res = resources
// 默認(rèn)屬性置空
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)
}
}
}
Stringpool 偏移量修改
圖片
圖片
第二行
00000010 0b 00 00 00 00 00 00 00 00 01 00 00 48 00 00 00 |............H...|
第三行
00000020 00 00 00 00 00(index 36) 00 00 00 03 00 00 00 06 00 00 00 |................|
第四行
00000030 09 00 00 00 0c 00 00 00 0f 00 00 00 12 00 00 00 |................|
第六行
00000050 00(index 80) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
可以看到空字符串第一個(gè)偏移量是 0: 72+8 80 ,從 80 開(kāi)始,每一個(gè)空字符串都由一組 00 00 00 來(lái)表示,這里也會(huì)有冗余的存儲(chǔ)占用。那這里是否可以控制偏移量,就用一組 00 00 00 來(lái)表示呢?答案是可以的, Android-Chunk-Utils 工具類已經(jīng)給我們提供了策略支持。ResourceFile.toByteArray 回寫(xiě)方法就提供了 Shrink 參數(shù)。
FileOutputStream(resourcesFile).use {
it.write(newResouce.toByteArray(true))
}
@Override
public byte[] toByteArray(boolean shrink) throws IOException {
ByteArrayDataOutput output = ByteStreams.newDataOutput();
for (Chunk chunk : chunks) {
output.write(chunk.toByteArray(shrink));
}
return output.toByteArray();
}
//StringPoolChunk 中的具體寫(xiě)入實(shí)現(xiàn)
@Override
protected 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)) {
// 如果支持優(yōu)化,將復(fù)用之前的數(shù)據(jù)和 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;
}
}
經(jīng)過(guò)三步優(yōu)化,重新更新 XML 文件后再次確定二進(jìn)制信息,獲取 Chunck 的總大小為:00 00 01 b0 (432),對(duì)比原始 XML 文件,一共減少 164 (28% )。當(dāng)然這個(gè)減少數(shù)據(jù)量取決于 XML 中標(biāo)簽及屬性的數(shù)量,越復(fù)雜的 XML 文件,縮減率越高。
圖片
效果對(duì)比
裁剪前
圖片
裁剪后
八、API 兼容調(diào)整
TabLayout 獲取 Height 的場(chǎng)景
圖片
這個(gè)寫(xiě)法同時(shí)使用了 Namespace 和 特定屬性,布局初始化時(shí)直接就會(huì) Crash 。后面掃描了所有使用 getAttributeValue 方法的類,篩選確定后進(jìn)行統(tǒng)一代碼調(diào)整。
int[] systemAttrs = {android.R.attr.layout_height};
TypedArray a = context.obtainStyledAttributes(attrs, systemAttrs);
try {
// 如果定義的是 WRAP_CONTENT 或者 MATCH_PARENT 這里會(huì)異常,然后通過(guò) getInt 獲取值(-1 -2)
mHeight = a.getDimensionPixelSize(0, ViewGroup.LayoutParams.WRAP_CONTENT);
} catch (Exception e) {
// e.printStackTrace();
mHeight = a.getInt(0, ViewGroup.LayoutParams.WRAP_CONTENT);
}
圖片庫(kù)獲取 SRC 的場(chǎng)景
圖片
因?yàn)閳D片庫(kù)調(diào)用的地方做了默認(rèn) Catch 捕獲異常,所以 App 沒(méi)有 Crash ,但是對(duì)于使用 SRC 屬性設(shè)置的圖片資源無(wú)法正常顯示。
圖片
后續(xù)調(diào)整為:
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()
}
DuToolbar 獲取 Theme 的場(chǎng)景
圖片
圖片
調(diào)整為:
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();
}
修改后發(fā)現(xiàn)依然有問(wèn)題,對(duì)比異常的不同頁(yè)面發(fā)現(xiàn)以下區(qū)別:
<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="領(lǐng)取數(shù)字藏品"
app:titleTextColor="@android:color/white" />
使用 Android 命名空間生成的屬性是系統(tǒng)自帶屬性,定義在 public.xml 中,使用 App 生成的屬性是自定義屬性,打包到 Arsc 的 Attr 中。
圖片
圖片
所以,上面僅判斷 Android.R.attr.theme 不夠,還需要增加 R.attr.theme 。
TypedArray a = context.obtainStyledAttributes(attrs, new int[]{R.attr.theme, android.R.attr.theme});
九、收益
最后,確定下總體包體積優(yōu)化收益:
移除 Namespace 及屬性值后:
圖片
優(yōu)化空字符串后:
圖片
由于這是 Apk 解壓后的所有文件匯總收益,重新壓縮打包 Apk 后,包體積整體收益在 2.2 M左右。
十、總結(jié)
本文介紹了得物App的包體積優(yōu)化工作,講解了針對(duì)XML二進(jìn)制文件的裁剪優(yōu)化。文章首先概述了XML解析流程和XML二進(jìn)制文件格式,然后介紹了解析過(guò)程中的一些工具以及細(xì)節(jié)問(wèn)題,探討了裁剪優(yōu)化實(shí)現(xiàn)以及API的兼容調(diào)整,最后呈現(xiàn)了包體積優(yōu)化的收益。