From 46eead20284b13318bfb4ae6b347414715b20338 Mon Sep 17 00:00:00 2001 From: rotund_tapir Date: Thu, 27 Aug 2026 09:57:45 +0100 Subject: [PATCH 1/4] Update block indexes once after sorting instead of on every swap BlockList.sort() registered a swap listener that called setIndex() on both items of every quicksort swap. For items that maintain back references, such as StringItem, each setIndex() triggers onIndexChanged() which rewrites every reference to the item, so a single sort of a large string pool performed O(swaps x references) work while all intermediate index values were immediately overwritten. The updateIndex() pass that already runs after the sort assigns each item its final index exactly once, firing onIndexChanged() a single time per moved item, so the per-swap updates are redundant. Sorting a resource table string pool of a large apk gets several times faster; the resulting order and all reference values are unchanged. --- .../java/com/reandroid/arsc/container/BlockList.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/com/reandroid/arsc/container/BlockList.java b/src/main/java/com/reandroid/arsc/container/BlockList.java index cff7a2be7..2b85ffca3 100755 --- a/src/main/java/com/reandroid/arsc/container/BlockList.java +++ b/src/main/java/com/reandroid/arsc/container/BlockList.java @@ -201,16 +201,7 @@ public boolean sort(Comparator comparator) { if (size() < 2) { return false; } - boolean sorted = mItems.sort(comparator, (i, j) -> { - T item1 = get(i); - T item2 = get(j); - if (item1 != null) { - item1.setIndex(i); - } - if (item2 != null) { - item2.setIndex(j); - } - }); + boolean sorted = mItems.sortItems(comparator); if (sorted) { updateIndex(); } From c70337acdb38a9f46372acbcd5f4b6268ce056dd Mon Sep 17 00:00:00 2001 From: rotund_tapir Date: Thu, 27 Aug 2026 09:57:57 +0100 Subject: [PATCH 2/4] Skip sorting a block list that is already in order BlockList.sort() ran the full quicksort even when the list was already sorted, costing O(n log n) comparisons per call. The string pools re-sort on every refresh whenever the sort-required flag is set, and operations that preserve order, like removing unused strings, set that flag too, so a resource table refreshed several times during encoding paid for the same sort repeatedly. Use the existing needsSort() linear scan to detect the already-sorted case and return false, matching the previous return value since the quicksort performs no swaps on a sorted list. needsSort() also covers the size() < 2 check. A genuinely unsorted list pays one extra O(n) scan, which is small against the O(n log n) sort that follows. --- src/main/java/com/reandroid/arsc/container/BlockList.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/reandroid/arsc/container/BlockList.java b/src/main/java/com/reandroid/arsc/container/BlockList.java index 2b85ffca3..9a89910e6 100755 --- a/src/main/java/com/reandroid/arsc/container/BlockList.java +++ b/src/main/java/com/reandroid/arsc/container/BlockList.java @@ -198,7 +198,7 @@ public void destroy() { onChanged(); } public boolean sort(Comparator comparator) { - if (size() < 2) { + if (!needsSort(comparator)) { return false; } boolean sorted = mItems.sortItems(comparator); From 8b20a7fc342f5563905d9671adf4651e36d082e6 Mon Sep 17 00:00:00 2001 From: rotund_tapir Date: Thu, 27 Aug 2026 10:01:07 +0100 Subject: [PATCH 3/4] Drop the per-package refresh while encoding values encodeValues() refreshed every package block right after encoding it, which recomputes sizes, offsets and header fields of the whole package, and re-sorts its string pools. All of that is thrown away: the table is refreshed at the end of scanResourceFiles() and again by ApkModuleEncoder.refreshTable(), and nothing between the per-package refresh and those passes reads the computed sizes or offsets. Keep sortTypes(), which later steps do rely on, and let the table-level refresh do the byte-level bookkeeping once. --- .../java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java b/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java index afcb4fa6d..67d8b4eb4 100644 --- a/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java +++ b/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java @@ -187,7 +187,6 @@ private void encodeValues(List pubXmlFileList) throws IOException, XmlPull filePathEncoder.encodePackageResDir(packageBlock, resDir); packageBlock.sortTypes(); - packageBlock.refresh(); } } private void encodeAttrs(List pubXmlFileList) throws IOException, XmlPullParserException { From 8572381e426d4ad1614c363c14ae0ce5eab0a7af Mon Sep 17 00:00:00 2001 From: rotund_tapir Date: Thu, 27 Aug 2026 10:07:15 +0100 Subject: [PATCH 4/4] Look up defined entries by resolved id instead of scanning references TypeBlock.getOrCreateDefinedEntry() started with getEntry(name), which walks every entry of the whole package referencing the spec string with that name and tests each for membership in this type block. While encoding values XML the lookup almost always misses, because the current type block is the one being filled, so each encoded item paid for a walk over all previously encoded configurations of the same resource: quadratic in the number of configurations, and the dominant cost of building a resource table with many locales. The subsequent resolveResourceId() walked the same references again. Resolve the id first (the reference walk stops at the first entry of this type), then address the entry array directly by entry id and compare the name. The scan by name is kept only for the ambiguous case where the defined id already holds a different name. Behavior is unchanged: a defined entry is returned when present, created at its defined id when not, and an undefined name still returns null. --- .../com/reandroid/arsc/chunk/TypeBlock.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/reandroid/arsc/chunk/TypeBlock.java b/src/main/java/com/reandroid/arsc/chunk/TypeBlock.java index b5431d0dd..a64f01c25 100755 --- a/src/main/java/com/reandroid/arsc/chunk/TypeBlock.java +++ b/src/main/java/com/reandroid/arsc/chunk/TypeBlock.java @@ -186,10 +186,6 @@ public SpecTypePair getParentSpecTypePair() { return getParent(SpecTypePair.class); } public Entry getOrCreateDefinedEntry(String name) { - Entry entry = getEntry(name); - if (entry != null) { - return entry; - } PackageBlock packageBlock = getPackageBlock(); if (packageBlock == null) { return null; @@ -198,6 +194,21 @@ public Entry getOrCreateDefinedEntry(String name) { if (id == 0) { return null; } + Entry entry = getEntry(id & 0xffff); + if (entry != null) { + String entryName = entry.getName(); + if (name.equals(entryName)) { + return entry; + } + if (entryName != null && !entry.isNull()) { + // The defined id holds a different name; fall back to + // scanning this type block by name. + Entry existing = getEntry(name); + if (existing != null) { + return existing; + } + } + } SpecStringPool stringPool = packageBlock.getSpecStringPool(); SpecString specString = stringPool.getOrCreate(name); entry = getOrCreateEntry(id & 0xffff);