AVt天堂网 手机版,亚洲va久久久噜噜噜久久4399,天天综合亚洲色在线精品,亚洲一级Av无码毛片久久精品

當前位置:首頁 > 科技  > 軟件

包體積:Layout 二進制文件裁剪優化

來源: 責編: 時間:2023-09-20 21:56:03 325觀看
導讀一、引言得物App在包體積優化方面已經進行了諸多嘗試,收獲也頗豐,已經集成的方案有圖片壓縮、重復資源刪除、ARSC壓縮等可移步至得物 Android 包體積資源優化實踐。本文將主要介紹基于 XML 二進制文件的裁剪優化。在正

一、引言

得物App在包體積優化方面已經進行了諸多嘗試,收獲也頗豐,已經集成的方案有圖片壓縮、重復資源刪除、ARSC壓縮等可移步至得物 Android 包體積資源優化實踐。本文將主要介紹基于 XML 二進制文件的裁剪優化。evs28資訊網——每日最新資訊28at.com

在正式進入裁剪優化前,需要先做準備工作,我們先從上層的代碼看起,看看布局填充的方法。方便我們從始到終了解整個情況。evs28資訊網——每日最新資訊28at.com

二、XML 解析流程

在 LayoutInflater 調用 Inflate 方法后,會將 XML 中的屬性包裝至 LayoutParams 中最后通過反射使用創建對應 View。evs28資訊網——每日最新資訊28at.com

而在反射前,傳入的 R.layout.xxx 文件是如何完成 XML 解析類的創建,后續又是如何通過該類完成 XML 中的數據解析呢?evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

上層 XML 解析最終會封裝到 XmlBlock 這個類中。XmlBlock 封裝了具體 RES 文件的解析數據。其中 nativeOpenXmlAsset 返回的就是 c 中對應的文件指針,后續取值都需要通過這個指針去操作。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

XmlBlock 內部的 Parse 類實現了 XmlResourceParser ,最終被包裝為 AttributeSet 接口返回。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

例如調用 AttributeSet 的方法:evs28資訊網——每日最新資訊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 中。

圖片圖片evs28資訊網——每日最新資訊28at.com

//core/jni/android_util_XmlBlock.cppevs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

可以看到,我們最終都是通過 ResXmlParser 類傳入對應的 ID 來完成取值。而不是通過具體的屬性名稱來進行取值。evs28資訊網——每日最新資訊28at.com

上面介紹的是直接通過 Attrs 取值的方式,在實際開發中我們通常會使用 TypedArray 來進行相關屬性值的獲取。例如 FrameLayout 的創建工程。evs28資訊網——每日最新資訊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 方法。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊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文件,轉換位二進制文件后就是這個樣子。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

要了解這個二進制文件,使用 命令行 hexdump  查看:evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

在二進制文件中,不同數據類型分塊存儲,共同組成一個完整文件。我們可以通過依次讀取每個字節,來獲取對應的信息。要準確讀取信息,就必須清楚它的定義規則和順序,確保可以正確讀取出內容。evs28資訊網——每日最新資訊28at.com

https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/libs/androidfw/include/androidfw/ResourceTypes.hevs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

每一塊(Chunk)都按固定格式生成,最基礎的定義有:evs28資訊網——每日最新資訊28at.com

Type:類型 分類,對應上面截圖中的類型evs28資訊網——每日最新資訊28at.com

headerSize:頭信息大小evs28資訊網——每日最新資訊28at.com

Size:總大小 (headerSize+dataSize)通過這個值,你可以跳過該 Chunk 的內容,如果 Size 和 headerSize 一致,說明該 Chunk 沒有數據內容。evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

StringPoolChunk

evs28資訊網——每日最新資訊28at.com

所有的 Chunk 中,都包含 ResChunk ,作為基礎信息。這里以 StringPoolChunk 舉例:

evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

在 StringPool 中,除了基礎的 ResChunk ,還額外包含以下信息:evs28資訊網——每日最新資訊28at.com

stringCount: 字符串常量池的總數量evs28資訊網——每日最新資訊28at.com

styleCount: style 相關的的總數量evs28資訊網——每日最新資訊28at.com

Flag: UTF_8 或者 UTF_16 的標志位  我們這里默認就是 UTF_8evs28資訊網——每日最新資訊28at.com

stringsStart:字符串開始的位置evs28資訊網——每日最新資訊28at.com

stylesStart:styles 開始的位置evs28資訊網——每日最新資訊28at.com

字符串從 stringStart 的位置相對開始,兩個字節來表示長度,最后以 0 結束。evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

XmlStartElementChunk

evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

startElementChunk 是布局 XML 中核心的標簽封裝對象,里面記錄了Namespace ,Name,Attribute 及相關的 Index 信息,其中 Attribute 中有用自己的 Name Value等具體封裝。evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

ResourceMapChunk

ResourceMapChunk是一個 32 位的 Int 數組,在我們編寫的 XML 中沒有直觀體現,但是在編譯為二進制文件后,它的確存在,也是我們后續能執行裁剪屬性名的重要依據:它與 String Pool 中的資源定義相匹配。evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

NameSpaceChunk

evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

NameSpaceChunk 就是對 Namespace 的封裝,主要包含了前綴(Android Tools  App),和具體的 URL。evs28資訊網——每日最新資訊28at.com

ResourceType.h 文件中定義了所以需要使用的類型,也是面向對象的封裝形式。后面講解析時,也會根據每種數據類型進行具體的解析處理。evs28資訊網——每日最新資訊28at.com

四、XML 解析過程舉例

我們以獲取 StringPool 的場景來舉例二進制文件的解析過程,通過這個過程,可以掌握字節讀取的具體實現。解析過程其實就是從 0 開始的字節偏移量獲取。每次讀取多少字節,依賴前面 ResourceTypes.h 中的格式定義。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

第一行evs28資訊網——每日最新資訊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) evs28資訊網——每日最新資訊28at.com

第二行 evs28資訊網——每日最新資訊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) 72evs28資訊網——每日最新資訊28at.com

第三行 evs28資訊網——每日最新資訊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 開始)evs28資訊網——每日最新資訊28at.com

0b 00 00 00: readStrings 第二次偏移 11 (80+11 從 91 開始)evs28資訊網——每日最新資訊28at.com

00 00 00 17:readString 第三次偏移 23 (80 +23 從 103 開始)evs28資訊網——每日最新資訊28at.com

第四行evs28資訊網——每日最新資訊28at.com

00000030  1c 00 00 00 2b 00 00 00    3b 00 00 00 42 00 00 00  |....+...;...B...|evs28資訊網——每日最新資訊28at.com

00 00 00 1c:readString 第四次偏移 28 (80+28 從 108 開始)evs28資訊網——每日最新資訊28at.com

00 00 00 2b:readString 第五次偏移 43evs28資訊網——每日最新資訊28at.com

第六行 evs28資訊網——每日最新資訊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|evs28資訊網——每日最新資訊28at.com

第七行 evs28資訊網——每日最新資訊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|evs28資訊網——每日最新資訊28at.com

第八行evs28資訊網——每日最新資訊28at.com

00000070  79 6f 75 74 5f 77 69 64  74 68 00 0d 0d 6c 61 79  |yout_width...lay|evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

工具介紹evs28資訊網——每日最新資訊28at.com

通過上面的手動解析二進制文件字節信息,既然格式如此固定,那多半已經有人做過相關封裝解析類吧,請看JakeWharton:https://github.com/madisp/android-chunk-utilsevs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

API 介紹

圖片圖片evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

StringPoolChunk 封裝

evs28資訊網——每日最新資訊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());}

evs28資訊網——每日最新資訊28at.com

ResourceMapChunk 封裝

evs28資訊網——每日最新資訊28at.com

資源 ID 對比 String 顯得更加簡單,因為它的長度固定為的 32 位 4 字節,所以用 dataSize 除以 4 就可以得到ResourceMap 的大小,然后依次調用 buffer.getInt() 方法獲取即可。evs28資訊網——每日最新資訊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;}

evs28資訊網——每日最新資訊28at.com

XmlStartElementChunk 封裝

evs28資訊網——每日最新資訊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);}

六、細節問題

evs28資訊網——每日最新資訊28at.com

Style 對應問題

evs28資訊網——每日最新資訊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 相關的屬性。evs28資訊網——每日最新資訊28at.com

我們注意主要聚焦于 Layout 文件,所以這里不再展開分析。evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

字節存儲方式

evs28資訊網——每日最新資訊28at.com

Little Endian:低位字節序evs28資訊網——每日最新資訊28at.com

Big Endian:高位字節序evs28資訊網——每日最新資訊28at.com

在 Little Endian 字節序中,數據的最低有效字節存儲在內存地址的最低位置,而最高有效字節則存儲在內存地址的最高位置。這種字節序的優點是可以更好地利用內存,能夠更容易地處理低位字節和高位字節的組合,尤其是在處理較大的整數和浮點數時比較快速。evs28資訊網——每日最新資訊28at.com

在 Big Endian 字節序中,數據的最高有效字節存儲在內存地址的最低位置,而最低有效字節則存儲在內存地址的最高位置。這種字節序雖然更符合人類讀寫的方式,但在高效率方面卻不如 Little Endian 字節序。evs28資訊網——每日最新資訊28at.com

舉例 0x12345678evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

低位存儲evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

高位存儲evs28資訊網——每日最新資訊28at.com

在 Java 中,默認采用 Big Endian 存儲方式,所以我們修改二進制文件時,需要手動指定為低位字節序。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

七、裁剪優化實現

evs28資訊網——每日最新資訊28at.com

Namespace 移除

evs28資訊網——每日最新資訊28at.com

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

第一步使用 Android-Chunk-Utils 代碼如下,第二步和屬性名移除并列執行。evs28資訊網——每日最新資訊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())}

evs28資訊網——每日最新資訊28at.com

屬性名移除

evs28資訊網——每日最新資訊28at.com

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

StringPoolChunk 中記錄了 XML 中的所有組件名稱及其屬性,而每個屬性對應的具體 ID ,則是固定的,在ResourceMapChunk 中,由 Index 一一對應。evs28資訊網——每日最新資訊28at.com

圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

舉個例子,在這個布局文件中, Layout_width 的在 StringPool 中的索引是 6 ,對應在 ResourceMapChunk 中是 16842996 的值,轉換十六進制后:10100f4,與 public.xml 中定義的屬性 ID 完全對應。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

通過上面源碼的介紹,每個屬性(Attr)包含一個對應的整型 ID 值,獲取其屬性值時都會通過該 ID 值來獲取。所以對應的屬性名理論上可以移除。具體代碼如下:evs28資訊網——每日最新資訊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)        }    }}

evs28資訊網——每日最新資訊28at.com

Stringpool 偏移量修改

evs28資訊網——每日最新資訊28at.com

經過上面兩個步驟后,StringPool 已經得到優化,但是觀察新的二進制文件會發現,目前總大小為:00 00 01 c4 (452) ,優化前為 00 00 02 54(596)。經過進一步分析可以發現,StringPool 中字符串的偏移量還可以優化。

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

第二行evs28資訊網——每日最新資訊28at.com

00000010  0b 00 00 00 00 00 00 00  00 01 00 00 48 00 00 00  |............H...|evs28資訊網——每日最新資訊28at.com

第三行evs28資訊網——每日最新資訊28at.com

00000020  00 00 00 00 00(index 36) 00 00 00  03 00 00 00 06 00 00 00  |................|evs28資訊網——每日最新資訊28at.com

第四行evs28資訊網——每日最新資訊28at.com

00000030  09 00 00 00 0c 00 00 00  0f 00 00 00 12 00 00 00  |................|evs28資訊網——每日最新資訊28at.com

第六行evs28資訊網——每日最新資訊28at.com

00000050  00(index 80) 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|evs28資訊網——每日最新資訊28at.com

可以看到空字符串第一個偏移量是 0: 72+8 80 ,從 80 開始,每一個空字符串都由一組 00 00 00 來表示,這里也會有冗余的存儲占用。那這里是否可以控制偏移量,就用一組 00 00 00 來表示呢?答案是可以的, Android-Chunk-Utils 工具類已經給我們提供了策略支持。ResourceFile.toByteArray 回寫方法就提供了 Shrink 參數。evs28資訊網——每日最新資訊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 文件,縮減率越高。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

效果對比

evs28資訊網——每日最新資訊28at.com

裁剪前裁剪前evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

裁剪后evs28資訊網——每日最新資訊28at.com

八、API 兼容調整

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

evs28資訊網——每日最新資訊28at.com

TabLayout 獲取 Height 的場景

evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

這個寫法同時使用了 Namespace 和 特定屬性,布局初始化時直接就會 Crash 。后面掃描了所有使用 getAttributeValue 方法的類,篩選確定后進行統一代碼調整。evs28資訊網——每日最新資訊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);}

evs28資訊網——每日最新資訊28at.com

圖片庫獲取 SRC 的場景

evs28資訊網——每日最新資訊28at.com

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

圖片圖片evs28資訊網——每日最新資訊28at.com

因為圖片庫調用的地方做了默認 Catch 捕獲異常,所以 App 沒有 Crash ,但是對于使用 SRC 屬性設置的圖片資源無法正常顯示。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

后續調整為:evs28資訊網——每日最新資訊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()}

evs28資訊網——每日最新資訊28at.com

DuToolbar 獲取 Theme 的場景evs28資訊網——每日最新資訊28at.com

evs28資訊網——每日最新資訊28at.com

我們的 Toolbar 有統一攔截設置,其中支持根據頁面設置是黑色或者白色的返回按鈕樣式,而這個設置就是通過 XML 中 Theme 主題關聯。這是之前的判斷,可以看到直接使用屬性名來判斷。由于屬性移除,該判斷條件永遠執行不到,導致 Toolbar 返回按鈕在特定頁面未按預期顏色展示。

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

調整為:evs28資訊網——每日最新資訊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();}

修改后發現依然有問題,對比異常的不同頁面發現以下區別:evs28資訊網——每日最新資訊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 中。evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

所以,上面僅判斷 Android.R.attr.theme 不夠,還需要增加 R.attr.theme 。evs28資訊網——每日最新資訊28at.com

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

九、收益

最后,確定下總體包體積優化收益:evs28資訊網——每日最新資訊28at.com

移除 Namespace 及屬性值后:evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

優化空字符串后:evs28資訊網——每日最新資訊28at.com

圖片圖片evs28資訊網——每日最新資訊28at.com

由于這是 Apk 解壓后的所有文件匯總收益,重新壓縮打包 Apk 后,包體積整體收益在 2.2 M左右。evs28資訊網——每日最新資訊28at.com

圖片evs28資訊網——每日最新資訊28at.com

十、總結

本文介紹了得物App的包體積優化工作,講解了針對XML二進制文件的裁剪優化。文章首先概述了XML解析流程和XML二進制文件格式,然后介紹了解析過程中的一些工具以及細節問題,探討了裁剪優化實現以及API的兼容調整,最后呈現了包體積優化的收益。evs28資訊網——每日最新資訊28at.com

本文鏈接:http://www.tebozhan.com/showinfo-26-10542-0.html包體積:Layout 二進制文件裁剪優化

聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 為什么在 C++14 中刪除了 get 函數?

下一篇: 冠閔信息CEO顏偉志談MSP發展趨勢:利用各種工具持續交付高度自動化的服務

標簽:
  • 熱門焦點
Top