精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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

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

一、引言

得物App在包體積優(yōu)化方面已經(jīng)進行了諸多嘗試,收獲也頗豐,已經(jīng)集成的方案有圖片壓縮、重復(fù)資源刪除、ARSC壓縮等可移步至得物 Android 包體積資源優(yōu)化實踐。本文將主要介紹基于 XML 二進制文件的裁剪優(yōu)化。

在正式進入裁剪優(yōu)化前,需要先做準(zhǔn)備工作,我們先從上層的代碼看起,看看布局填充的方法。方便我們從始到終了解整個情況。

二、XML 解析流程

在 LayoutInflater 調(diào)用 Inflate 方法后,會將 XML 中的屬性包裝至 LayoutParams 中最后通過反射使用創(chuàng)建對應(yīng) View。

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

圖片圖片

圖片圖片

圖片圖片

圖片圖片

上層 XML 解析最終會封裝到 XmlBlock 這個類中。XmlBlock 封裝了具體 RES 文件的解析數(shù)據(jù)。其中 nativeOpenXmlAsset 返回的就是 c 中對應(yīng)的文件指針,后續(xù)取值都需要通過這個指針去操作。

圖片圖片

XmlBlock 內(nèi)部的 Parse 類實現(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")
}
最終就會調(diào)用到 XmlResourceParser 中的方法,最終調(diào)用到 Native 中。

圖片圖片

//core/jni/android_util_XmlBlock.cpp

圖片圖片

可以看到,我們最終都是通過 ResXmlParser 類傳入對應(yīng)的 ID 來完成取值。而不是通過具體的屬性名稱來進行取值。

上面介紹的是直接通過 Attrs 取值的方式,在實際開發(fā)中我們通常會使用 TypedArray 來進行相關(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 方法最終會調(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 二進制文件格式

你寫的代碼是這個樣子,App 打包過程中通過 AAPT2 工具處理完 XML文件,轉(zhuǎn)換位二進制文件后就是這個樣子。

圖片圖片

圖片圖片

要了解這個二進制文件,使用 命令行 hexdump  查看:

圖片圖片

在二進制文件中,不同數(shù)據(jù)類型分塊存儲,共同組成一個完整文件。我們可以通過依次讀取每個字節(jié),來獲取對應(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:類型 分類,對應(yīng)上面截圖中的類型

headerSize:頭信息大小

Size:總大小 (headerSize+dataSize)通過這個值,你可以跳過該 Chunk 的內(nèi)容,如果 Size 和 headerSize 一致,說明該 Chunk 沒有數(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:字符串開始的位置

stylesStart:styles 開始的位置

字符串從 stringStart 的位置相對開始,兩個字節(jié)來表示長度,最后以 0 結(jié)束。

XmlStartElementChunk

圖片圖片

startElementChunk 是布局 XML 中核心的標(biāo)簽封裝對象,里面記錄了Namespace ,Name,Attribute 及相關(guān)的 Index 信息,其中 Attribute 中有用自己的 Name Value等具體封裝。

ResourceMapChunk

ResourceMapChunk是一個 32 位的 Int 數(shù)組,在我們編寫的 XML 中沒有直觀體現(xiàn),但是在編譯為二進制文件后,它的確存在,也是我們后續(xù)能執(zhí)行裁剪屬性名的重要依據(jù):它與 String Pool 中的資源定義相匹配。

NameSpaceChunk

圖片圖片

NameSpaceChunk 就是對 Namespace 的封裝,主要包含了前綴(Android Tools  App),和具體的 URL。

ResourceType.h 文件中定義了所以需要使用的類型,也是面向?qū)ο蟮姆庋b形式。后面講解析時,也會根據(jù)每種數(shù)據(jù)類型進行具體的解析處理。

四、XML 解析過程舉例

我們以獲取 StringPool 的場景來舉例二進制文件的解析過程,通過這個過程,可以掌握字節(jié)讀取的具體實現(xiàn)。解析過程其實就是從 0 開始的字節(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 中最后一個字段獲取) 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|

工具介紹

通過上面的手動解析二進制文件字節(jié)信息,既然格式如此固定,那多半已經(jīng)有人做過相關(guān)封裝解析類吧,請看JakeWharton:https://github.com/madisp/android-chunk-utils

API 介紹

圖片圖片

StringPoolChunk 封裝

String 按之前手動解析的思路,是通過偏移量獲取 String 的開始位置及具體長度,完成不同 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 對比 String 顯得更加簡單,因為它的長度固定為的 32 位 4 字節(jié),所以用 dataSize 除以 4 就可以得到ResourceMap 的大小,然后依次調(diào)用 buffer.getInt() 方法獲取即可。

ResourceMap封裝過程:

private List<Integer> enumerateResources(ByteBuffer buffer) {
  // id 固定為 4 個字節(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();
  // 獲取屬性索引的開始位置
  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);
}

六、細節(jié)問題

Style 對應(yīng)問題

在 StringPool 中我們可以看到除了 String 的定義,這里還有 Style 的定義,那么這個 Style 到底對應(yīng)什么呢?經(jīng)過一番測試后,Style 其實對應(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 文件時,就會有有 Style 相關(guān)的屬性。

我們注意主要聚焦于 Layout 文件,所以這里不再展開分析。

字節(jié)存儲方式

Little Endian:低位字節(jié)序

Big Endian:高位字節(jié)序

在 Little Endian 字節(jié)序中,數(shù)據(jù)的最低有效字節(jié)存儲在內(nèi)存地址的最低位置,而最高有效字節(jié)則存儲在內(nèi)存地址的最高位置。這種字節(jié)序的優(yōu)點是可以更好地利用內(nèi)存,能夠更容易地處理低位字節(jié)和高位字節(jié)的組合,尤其是在處理較大的整數(shù)和浮點數(shù)時比較快速。

在 Big Endian 字節(jié)序中,數(shù)據(jù)的最高有效字節(jié)存儲在內(nèi)存地址的最低位置,而最低有效字節(jié)則存儲在內(nèi)存地址的最高位置。這種字節(jié)序雖然更符合人類讀寫的方式,但在高效率方面卻不如 Little Endian 字節(jié)序。

舉例 0x12345678

圖片圖片

低位存儲

圖片圖片

高位存儲

在 Java 中,默認(rèn)采用 Big Endian 存儲方式,所以我們修改二進制文件時,需要手動指定為低位字節(jié)序。

圖片圖片

圖片圖片

七、裁剪優(yōu)化實現(xiàn)

Namespace 移除

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

第一步使用 Android-Chunk-Utils 代碼如下,第二步和屬性名移除并列執(zhí)行。

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


}

屬性名移除

將字符串池中每一個字符串替換成""空字符串。

StringPoolChunk 中記錄了 XML 中的所有組件名稱及其屬性,而每個屬性對應(yīng)的具體 ID ,則是固定的,在ResourceMapChunk 中,由 Index 一一對應(yīng)。

圖片

圖片圖片

舉個例子,在這個布局文件中, Layout_width 的在 StringPool 中的索引是 6 ,對應(yīng)在 ResourceMapChunk 中是 16842996 的值,轉(zhuǎn)換十六進制后:10100f4,與 public.xml 中定義的屬性 ID 完全對應(yīng)。

圖片圖片

通過上面源碼的介紹,每個屬性(Attr)包含一個對應(yīng)的整型 ID 值,獲取其屬性值時都會通過該 ID 值來獲取。所以對應(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)過上面兩個步驟后,StringPool 已經(jīng)得到優(yōu)化,但是觀察新的二進制文件會發(fā)現(xiàn),目前總大小為:00 00 01 c4 (452) ,優(yōu)化前為 00 00 02 54(596)。經(jīng)過進一步分析可以發(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  |................|

可以看到空字符串第一個偏移量是 0: 72+8 80 ,從 80 開始,每一個空字符串都由一組 00 00 00 來表示,這里也會有冗余的存儲占用。那這里是否可以控制偏移量,就用一組 00 00 00 來表示呢?答案是可以的, Android-Chunk-Utils 工具類已經(jīng)給我們提供了策略支持。ResourceFile.toByteArray 回寫方法就提供了 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à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)過三步優(yōu)化,重新更新 XML 文件后再次確定二進制信息,獲取 Chunck 的總大小為:00 00 01 b0 (432),對比原始 XML 文件,一共減少 164 (28% )。當(dāng)然這個減少數(shù)據(jù)量取決于 XML 中標(biāo)簽及屬性的數(shù)量,越復(fù)雜的 XML 文件,縮減率越高。

圖片圖片

效果對比

裁剪前裁剪前

圖片圖片

裁剪后

八、API 兼容調(diào)整

雖然理論上說移除布局的屬性后對于正常的流程無影響,但是,該有的問題總還是會有的,真一個問題都沒有那才讓人心里不踏實,接下來看兼容的一些異常情況。

TabLayout 獲取 Height 的場景

圖片圖片

這個寫法同時使用了 Namespace 和 特定屬性,布局初始化時直接就會 Crash 。后面掃描了所有使用 getAttributeValue 方法的類,篩選確定后進行統(tǒng)一代碼調(diào)整。

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);
}

圖片庫獲取 SRC 的場景

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

圖片圖片

因為圖片庫調(diào)用的地方做了默認(rèn) Catch 捕獲異常,所以 App 沒有 Crash ,但是對于使用 SRC 屬性設(shè)置的圖片資源無法正常顯示。

圖片圖片

后續(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 的場景

我們的 Toolbar 有統(tǒng)一攔截設(shè)置,其中支持根據(jù)頁面設(shè)置是黑色或者白色的返回按鈕樣式,而這個設(shè)置就是通過 XML 中 Theme 主題關(guān)聯(lián)。這是之前的判斷,可以看到直接使用屬性名來判斷。由于屬性移除,該判斷條件永遠執(zhí)行不到,導(dǎo)致 Toolbar 返回按鈕在特定頁面未按預(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)依然有問題,對比異常的不同頁面發(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)化工作,講解了針對XML二進制文件的裁剪優(yōu)化。文章首先概述了XML解析流程和XML二進制文件格式,然后介紹了解析過程中的一些工具以及細節(jié)問題,探討了裁剪優(yōu)化實現(xiàn)以及API的兼容調(diào)整,最后呈現(xiàn)了包體積優(yōu)化的收益。

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

2022-05-07 15:51:47

Android資源文件文件名

2009-12-16 10:49:42

Ruby操作二進制文件

2024-01-31 09:55:53

2022-07-18 09:01:15

SwiftApple二進制目標(biāo)

2009-08-12 18:06:53

C#讀取二進制文件

2021-04-30 07:56:56

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

2024-02-01 09:04:12

2013-04-28 15:37:35

JBoss

2022-01-14 11:39:46

BOLTFacebookLLVM

2009-12-10 09:24:50

PHP函數(shù)fwrite

2020-05-22 18:00:26

Go二進制文件編程語言

2023-12-26 15:10:00

處理二進制文件

2018-10-22 14:37:16

二進制數(shù)據(jù)存儲

2009-02-27 09:37:33

Google二進制代碼

2022-10-31 08:02:42

二進制計算乘法

2009-11-02 11:27:42

VB.NET二進制文件

2023-06-25 13:00:04

2022-07-26 13:00:01

安全符號源代碼

2017-04-11 10:48:53

JS二進制

2015-07-21 11:43:14

CentosRPM
點贊
收藏

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

午夜精品久久久久久久爽 | 国产综合久久久久久鬼色| 日韩国产高清视频在线| 欧洲金发美女大战黑人| 国产999久久久| 亚洲激情女人| 亚洲视频网站在线观看| 福利片一区二区三区| 丝袜美女在线观看| 久久色在线观看| 国产精品视频一区国模私拍| 69xx绿帽三人行| 97一区二区国产好的精华液| 疯狂蹂躏欧美一区二区精品| 天堂√在线观看一区二区| aaaa一级片| 亚洲欧美日本日韩| 久久精品欧美视频| 国产精品久久久久久亚洲色| yy6080久久伦理一区二区| 一区二区三区久久| 精品国产乱码久久久久久郑州公司 | 日韩精品极品毛片系列视频| 91小视频网站| 黄色污网站在线观看| 国产精品久久久久久亚洲伦| 国产精品jizz视频| 最好看的日本字幕mv视频大全| 伊人久久大香线蕉综合四虎小说 | 久久99久国产精品黄毛片入口| 色婷婷免费视频| 91成人短视频在线观看| 婷婷六月综合网| 中日韩在线视频| 视频三区在线观看| 国产成人8x视频一区二区| 国产精品爱啪在线线免费观看| 久久久久久久久久久久久久免费看 | 亚洲综合色在线| 久久免费99精品久久久久久| 99久久夜色精品国产亚洲| 日本免费在线视频不卡一不卡二| 欧美精品久久久久久久免费观看| 精品人妻一区二区三区蜜桃视频| 大奶一区二区三区| 在线播放一区二区三区| 日本熟妇人妻xxxxx| 亚洲国产精品精华素| 中文在线免费一区三区高中清不卡| 国产一级精品aaaaa看| 国产av精国产传媒| 免费观看日韩av| 秋霞成人午夜鲁丝一区二区三区| 永久免费看片视频教学| 日韩久久综合| 尤物yw午夜国产精品视频明星| 在线观看国产网站| 日韩欧美一级| 91精品婷婷国产综合久久性色 | 五月天亚洲综合| 亚洲成人77777| 国产精品18久久久| 91欧美精品午夜性色福利在线 | 午夜精品剧场| 日韩视频免费观看| 久久久精品少妇| 日韩在线视屏| 最近免费中文字幕视频2019| 天天躁夜夜躁狠狠是什么心态| 免费一区二区| 国产亚洲成av人片在线观看桃| 久久精品成人av| 禁果av一区二区三区| 亚洲人成网7777777国产| 黄色短视频在线观看| 综合国产视频| 亚洲精品福利在线| 免费的av网站| 亚洲成a人片77777在线播放| 日韩大陆毛片av| 强伦人妻一区二区三区| 精品国产一区二区三区香蕉沈先生| 亚洲伦理中文字幕| 色婷婷国产精品免| 久久久久午夜电影| 欧美日韩第一视频| 超碰中文字幕在线| 日韩成人av影视| 国产日韩在线亚洲字幕中文| 91在线公开视频| 国产成人亚洲综合a∨猫咪| 国产精品一区二区三区在线| 四虎在线观看| 欧美高清在线一区| 少妇高潮大叫好爽喷水| h片在线观看下载| 色吊一区二区三区| 亚洲欧美日韩精品一区| 成人台湾亚洲精品一区二区| 亚洲精品一区久久久久久| 日本性高潮视频| 欧美一区二区三区免费看| 国内精品久久久久| 中文字幕视频网| 精品一区二区三区蜜桃| 国产一区再线| 999国产在线视频| 一区二区三区在线观看视频| 农村妇女精品一二区| 日韩精品第二页| 亚洲精品福利视频| 99成人在线观看| 一本久道久久综合婷婷鲸鱼| 国产美女主播一区| 天堂网av在线播放| 中文字幕一区二区三区四区不卡 | 婷婷综合久久一区二区三区| 黄色永久免费网站| xvideos.蜜桃一区二区| 国产亚洲欧洲黄色| 日韩精品人妻中文字幕| 日韩国产精品久久| 成人动漫在线观看视频| 黄色av免费在线观看| 亚洲一区二区在线免费观看视频| 日韩欧美xxxx| 久久动漫网址| 成人444kkkk在线观看| 无码人妻精品一区二区| www..com久久爱| 日本黄xxxxxxxxx100| 在线成人视屏 | 久久高清免费| 欧美有码在线观看视频| 丰满人妻一区二区三区免费视频 | av资源一区二区| 3d成人动漫在线| 91国产福利在线| 四川一级毛毛片| 红桃成人av在线播放| 欧美黑人一级爽快片淫片高清| 中文字幕在线观看第二页| av一区二区三区| 国产乱子伦精品视频| 99tv成人影院| 亚洲品质视频自拍网| 国产情侣在线视频| 成人小视频免费观看| 国产四区在线观看| 欧美爱爱视频| 亚洲欧洲午夜一线一品| 国产三级av片| 波多野结衣中文字幕一区二区三区 | 国产精品久久久久久一区二区三区| 丰满少妇被猛烈进入高清播放| av一级亚洲| 欧美国产一区二区三区| 国产情侣一区二区| 亚洲久草在线视频| 欧美日韩理论片| 偷拍欧美精品| 91观看网站| 污污影院在线观看| 日韩精品一区二区三区视频播放| 欧美成人精品激情在线视频| 国产精品一区二区在线看| 欧美爱爱视频网站| 日本久久伊人| 九九综合九九综合| 高h调教冰块play男男双性文| 亚洲一区在线电影| 大尺度在线观看| 亚洲少妇自拍| 日本午夜精品一区二区三区| 成人午夜sm精品久久久久久久| 在线视频精品一| 91影院在线播放| 一区二区欧美精品| 久久出品必属精品| 在线精品一区二区| 国模一区二区三区私拍视频| 亚洲色图官网| 国产香蕉一区二区三区在线视频 | 日韩免费高清av| 国产第一页在线播放| 国产精品一区二区久激情瑜伽| 9色视频在线观看| 国产videos久久| 91视频99| 日韩国产网站| 欧美激情一区二区三区成人| 国产视频网址在线| 日韩欧美国产精品一区| 乱子伦一区二区三区| 玉足女爽爽91| 日本性高潮视频| 成人中文字幕合集| mm131亚洲精品| 日韩视频二区| 黄色污污在线观看| 经典一区二区| 加勒比在线一区二区三区观看| 成人在线观看免费播放| 97视频在线观看成人| 欧美r级在线| 亚洲片在线观看| 免费观看黄一级视频| 欧美精品粉嫩高潮一区二区| 青青草免费观看视频| 一区二区三区四区视频精品免费 | 免费成年人高清视频| 久久av最新网址| 黄色一级片在线看| 午夜精品一区二区三区国产| 欧美污视频久久久| 国产成人aa在线观看网站站| 成人午夜一级二级三级| 日韩欧美精品一区二区综合视频| 97色在线观看| 中文在线手机av| 久久精品国产69国产精品亚洲| 极品白浆推特女神在线观看 | 91久久线看在观草草青青| 男人天堂中文字幕| 亚洲一卡二卡三卡四卡| 青花影视在线观看免费高清| 国产人伦精品一区二区| 成人免费毛片糖心| 99精品国产91久久久久久| 人妖粗暴刺激videos呻吟| 国产一区二区三区av电影 | 久久资源免费视频| 91精品专区| 一区二区三区美女xx视频| 三级在线视频| 亚洲精品久久久久久久久久久久久| 国产福利第一视频| 欧美一区二区精品在线| 97免费观看视频| 欧美一区二区三区在| 国产精品无码AV| 欧美精品三级日韩久久| 亚洲图片中文字幕| 欧美精品一二三区| 国产人妖在线播放| 日韩欧美国产综合在线一区二区三区| 国产美女免费视频| 正在播放亚洲一区| 国产黄色免费大片| 日韩一二在线观看| 高清毛片aaaaaaaaa片| 亚洲激情中文字幕| 深夜福利视频网站| 亚洲精品久久久久久久久| 日本人妖在线| 亚洲乱码国产乱码精品精| 国产小视频免费在线观看| 尤物tv国产一区| av电影免费在线观看| 久久久久久久久国产| 欧美办公室脚交xxxx| 国产精品久久二区| 欧美videos粗暴| 粉嫩av一区二区三区免费观看| 狠狠一区二区三区| 欧美精品欧美精品| 色综合五月天| 成人免费观看在线| 天堂精品中文字幕在线| 网站在线你懂的| 成人不卡免费av| 最近中文字幕免费| 亚洲免费资源在线播放| 国产在线观看免费视频今夜| 欧美视频二区36p| 国产一区二区三区黄片| 欧美va亚洲va香蕉在线| 秋霞av在线| 久久久99免费视频| √8天堂资源地址中文在线| 国产精品h在线观看| 精品国产三区在线| 久久久久久国产精品mv| 日韩在线精品| 亚洲熟妇无码另类久久久| 免费在线欧美视频| 日韩综合第一页| 国产精品国模大尺度视频| 久久久久久久久久久久久久久久久 | 日本一级黄视频| 玖玖精品视频| 成人一区二区三区仙踪林| 久久精品一二三| 欧美成人三级视频| 在线观看日产精品| 欧美 日韩 国产 成人 在线| 综合国产在线视频| 蜜桃麻豆av在线| 成人精品久久久| 亚洲自拍电影| 成人在线免费观看视频网站| 丝袜美腿亚洲一区| 中文字幕在线视频播放| 国产精品久久久久桃色tv| 黄色片网站在线免费观看| 日韩欧美专区在线| 午夜毛片在线| 日本精品一区二区三区在线| 亚洲天堂av资源在线观看| 五月天亚洲综合情| 国产日韩欧美一区| 亚洲精品第二页| 亚洲精品视频在线观看免费| 凹凸精品一区二区三区| 日韩电影大片中文字幕| 欧美videosex性欧美黑吊| 成人精品视频久久久久| 国产区精品区| 无码aⅴ精品一区二区三区浪潮 | 久久国产麻豆精品| 久久美女免费视频| 高跟丝袜一区二区三区| 精品久久久久中文慕人妻| www.美女亚洲精品| 亚洲日本在线观看视频| 欧美一区二区福利| 另类图片国产| 尤物视频最新网址| 精品成人乱色一区二区| 手机av在线免费观看| 欧美丰满老妇厨房牲生活| 秋霞影院一区| 日本黄网站色大片免费观看| 国产一区二区三区四| 久久中文免费视频| 日韩一级完整毛片| 中文字幕中文字幕在线十八区| 91亚洲精品一区二区| 五月天综合网站| 黄色一级片免费播放| 亚洲另类春色国产| 精品人妻一区二区三区含羞草 | 午夜在线视频免费观看| 久久精品av麻豆的观看方式| 国产白丝一区二区三区| 欧美日韩国产综合久久| 天天影视久久综合| 国产欧美精品在线| 91超碰国产精品| 国产亚洲精品成人a| 亚洲电影一区二区| 午夜影院在线视频| 国产91精品最新在线播放| 国产精品免费大片| 中文字幕成人在线视频| 亚洲日本在线a| 人妻精品一区二区三区| 97香蕉久久夜色精品国产| 欧美精品第一区| 亚洲欧美国产中文| 一区二区在线观看视频在线观看| 国精品人妻无码一区二区三区喝尿 | 国产福利91精品一区二区三区| 久一视频在线观看| 日韩大片免费观看视频播放| 欧美精品总汇| 国产成人精品免费看在线播放| 国产99久久久久| 五月婷婷中文字幕| 色七七影院综合| 99亚洲乱人伦aⅴ精品| www黄色av| 最新不卡av在线| 欧美一区二区三区黄片| 国产成人啪精品视频免费网| 国产精品99视频| 成人午夜精品无码区| 欧美伊人久久大香线蕉综合69| 黄色大片在线播放| 久久66热这里只有精品| 蜜臀av一区二区| 国产精品18p| 日韩综合中文字幕| 欧美男男freegayvideosroom| 国产又黄又猛又粗又爽的视频| 亚洲激情校园春色| 毛片免费在线观看| y111111国产精品久久婷婷| 先锋亚洲精品| 国产suv一区二区三区| 亚洲美女在线视频| 欧美国产中文高清| 热久久精品国产| 亚洲伊人伊色伊影伊综合网| 国产网站在线播放| 国产欧美在线一区二区| 精油按摩中文字幕久久| 国产伦精品一区二区三区视频网站| 久久五月天综合| 国模吧精品视频|