Skip to content

Merge bundle tables without copying the base table - #107

Open
rotundtapir wants to merge 1 commit into
REAndroid:mainfrom
rotundtapir:pr/bundle-merge
Open

rotundtapir wants to merge 1 commit into
REAndroid:mainfrom
rotundtapir:pr/bundle-merge

Conversation

@rotundtapir

Copy link
Copy Markdown

ApkBundle.mergeModules() merges the base into an empty module, which copies the whole base table entry by entry into a new TableBlock while the base keeps its own parsed copy. TableBlock.merge() then refreshes the whole table after every split. On a large bundle that means two full tables on the heap and one full refresh per split. A user of Morphe (which uses ARSCLib to merge splits on the phone) ran out of memory at this step with a 1280 MB heap on the Google app (com.google.android.googlequicksearchbox).

Changes:

  • A table parsed only for the merge becomes the merged table when the target has none, and a split's parsed table is dropped once merged.
  • TableBlock.merge(table, refresh) lets the bundle refresh once at the end. The existing merge() methods still refresh.
  • The largest table is picked by entry size, so a bundle without a base no longer parses every table up front.
  • A stored dex stays stored under the name it is renumbered to, instead of being deflated while writing.

Split bundle of the Google app (com.google.android.googlequicksearchbox 17.61.20: base 268 MB + 8 splits), JDK 21:

main this PR
mergeModules + writeApk 65.6 s 25.8 s
peak heap 1.9 GB 1.0 GB
smallest -Xmx that works 1280m 768m

Taking the base table over also fixes string corruption. The copy merges the string pool by XML text, so a plain string containing markup could turn into the styled string with the same text. In the Google app, assistant_onboarding_account_ineligible_shell_app_error_message became styled text with its link lost in 36 locales. With this PR all 36 match base.apk again.

Otherwise the decoded tables match main, and dex and lib entries are byte-identical. One visible difference: when validateResourcesDir() runs after the merge, a few files shared between the default and -night configurations can land in the other directory (11 in Instagram). Their content is identical and every reference points at them. Tested on the Google app, Reddit (34 splits), Instagram, Google Photos, Twitch, SoundCloud and Duolingo bundles. ApkBundleMergeTest covers the merge and the stored dex.

Independent of #104. Second of the split-merge changes after #106; the per-entry memory change will follow separately.

ApkBundle.mergeModules() merged the base into an empty module, which copied
the whole base table entry by entry into a new TableBlock while the base kept
its own parsed copy, and TableBlock.merge() refreshed the whole table after
every module. On a large app (Google app, 87 MB resources.arsc) that is two
full tables on the heap and one full refresh per split.

A table that is only parsed for the merge is now taken over as the merged
table when the target has none, a split's parsed table is dropped once
merged, and TableBlock.merge(table, refresh) lets the bundle refresh once at
the end. The public merge() methods still refresh. The largest table is
picked by entry size, so a bundle without a base does not parse every table
up front.

Taking the base table over also keeps its strings as they are: the copy
merged the string pool by XML text, which could turn a plain string
containing markup into the styled string of the same text.

Also keep a stored dex stored under the name it is renumbered to, instead of
deflating it while writing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant