自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

包體積:Layout 二進(jìn)制文件裁剪優(yōu)化

開(kāi)發(fā) 前端
而在反射前,傳入的 R.layout.xxx 文件是如何完成 XML 解析類的創(chuàng)建,后續(xù)又是如何通過(guò)該類完成 XML 中的數(shù)據(jù)解析呢?

一、引言

得物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

所有的 Chunk 中,都包含 ResChunk ,作為基礎(chǔ)信息。這里以 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 封裝

String 按之前手動(dòng)解析的思路,是通過(guò)偏移量獲取 String 的開(kāi)始位置及具體長(zhǎng)度,完成不同 String 的讀取。
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)題

在 StringPool 中我們可以看到除了 String 的定義,這里還有 Style 的定義,那么這個(gè) Style 到底對(duì)應(yīng)什么呢?經(jīng)過(guò)一番測(cè)試后,Style 其實(shí)對(duì)應(yīng) Strings.xml 中定義的富文本內(nèi)容,例如:
<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 移除

將字符串池中命名空間字符串替換成""空串,以達(dá)到減少文件內(nèi)容的效果。有上面 NameSpaceChunk 中的介紹可知,nameSpace 的前綴及具體 URL 都存放在 StringPool 中,具體內(nèi)容(字節(jié),偏移量)封裝在  NameSpaceChunk 中。所以需要執(zhí)行兩個(gè)步驟:1. 移除 NameSpaceChunk 對(duì)象 2. 置空 StringPool 中的字符串內(nèi)容。

第一步使用 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())


}

屬性名移除

將字符串池中每一個(gè)字符串替換成""空字符串。

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 偏移量修改

經(jīng)過(guò)上面兩個(gè)步驟后,StringPool 已經(jīng)得到優(yōu)化,但是觀察新的二進(jìn)制文件會(huì)發(fā)現(xiàn),目前總大小為:00 00 01 c4 (452) ,優(yōu)化前為 00 00 02 54(596)。經(jīng)過(guò)進(jìn)一步分析可以發(fā)現(xiàn),StringPool 中字符串的偏移量還可以優(yōu)化。

圖片圖片

圖片圖片

第二行

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)整

雖然理論上說(shuō)移除布局的屬性后對(duì)于正常的流程無(wú)影響,但是,該有的問(wèn)題總還是會(huì)有的,真一個(gè)問(wèn)題都沒(méi)有那才讓人心里不踏實(shí),接下來(lái)看兼容的一些異常情況。

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)景

圖片庫(kù)內(nèi)部默認(rèn)支持了 ImageView 的 SRC 屬性,具體獲取方式使用了 getAttributeResourceValue 的方法。

圖片圖片

因?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)景

我們的 Toolbar 有統(tǒng)一攔截設(shè)置,其中支持根據(jù)頁(yè)面設(shè)置是黑色或者白色的返回按鈕樣式,而這個(gè)設(shè)置就是通過(guò) XML 中 Theme 主題關(guān)聯(lián)。這是之前的判斷,可以看到直接使用屬性名來(lái)判斷。由于屬性移除,該判斷條件永遠(yuǎn)執(zhí)行不到,導(dǎo)致 Toolbar 返回按鈕在特定頁(yè)面未按預(yù)期顏色展示。

圖片圖片

圖片

圖片圖片

調(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)化的收益。

責(zé)任編輯:武曉燕 來(lái)源: 得物技術(shù)
相關(guān)推薦

2022-05-07 15:51:47

Android資源文件文件名

2022-07-18 09:01:15

SwiftApple二進(jìn)制目標(biāo)

2024-01-31 09:55:53

2009-12-16 10:49:42

Ruby操作二進(jìn)制文件

2009-08-12 18:06:53

C#讀取二進(jìn)制文件

2021-04-30 07:56:56

MySQL數(shù)據(jù)庫(kù)二進(jìn)制包安裝

2022-01-14 11:39:46

BOLTFacebookLLVM

2013-04-28 15:37:35

JBoss

2024-02-01 09:04:12

2009-12-10 09:24:50

PHP函數(shù)fwrite

2020-05-22 18:00:26

Go二進(jìn)制文件編程語(yǔ)言

2023-12-26 15:10:00

處理二進(jìn)制文件

2009-11-02 11:27:42

VB.NET二進(jìn)制文件

2018-10-22 14:37:16

二進(jìn)制數(shù)據(jù)存儲(chǔ)

2009-02-27 09:37:33

Google二進(jìn)制代碼

2022-10-31 08:02:42

二進(jìn)制計(jì)算乘法

2023-06-25 13:00:04

2015-07-21 11:43:14

CentosRPM

2022-07-26 13:00:01

安全符號(hào)源代碼

2017-04-11 10:48:53

JS二進(jìn)制
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)