From d7e83abb29df6a52d642febf53dad335faac9139 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 964ee0cda..193c88948 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 026913521f06e4af8c769d8ddc12f6d9002dc13e 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 193c88948..8fe84a96d 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 ac90b4c414f4d0004df120d13ef5fc32d9228c93 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 beddb9d5a..5da54b179 100644 --- a/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java +++ b/src/main/java/com/reandroid/apk/xmlencoder/XMLTableBlockEncoder.java @@ -237,7 +237,6 @@ private void encodeValues(List pubXmlFileList) throws IOException, XmlPull filePathEncoder.encodePackageResDir(packageBlock, resDir); packageBlock.sortTypes(); - packageBlock.refresh(); } } private void encodeResDir(PackageBlock packageBlock, File resDir) throws IOException, XmlPullParserException { From cb90b11d0363a87b9ab95e274d58136c483508c2 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 645f42973..566871042 100755 --- a/src/main/java/com/reandroid/arsc/chunk/TypeBlock.java +++ b/src/main/java/com/reandroid/arsc/chunk/TypeBlock.java @@ -188,10 +188,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; @@ -200,6 +196,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);