Skip to content

fix(android): skip redundant document reloads in drawPdf() to fix a native memory leak - #1047

Open
GaelCO wants to merge 1 commit into
wonday:masterfrom
GaelCO:fix-native-memory-leak
Open

GaelCO wants to merge 1 commit into
wonday:masterfrom
GaelCO:fix-native-memory-leak

Conversation

@GaelCO

@GaelCO GaelCO commented Oct 1, 2026

Copy link
Copy Markdown

Fixes #1046

Summary

On Android, every mount of <Pdf> leaked about 3 MB of native memory. That was one pdfium document opened by a cancelled load and never closed. This PR stops drawPdf() from reloading the document when nothing changed, which removes those cancelled loads.

Root cause

  • PdfView.drawPdf() runs three times per mount (posted from PdfManager.setPath(), posted from PdfManager.onAfterUpdateTransaction(), and called from onAttachedToWindow()), and again on every prop transaction. Each call builds a new Configurator and reloads the document, even if no prop that affects loading changed.
  • Each reload goes through PDFView.recycle(), which cancels the previous DecodingAsyncTask. Those tasks run on AsyncTask.THREAD_POOL_EXECUTOR, in parallel, so a cancelled task has usually already opened its document natively.
  • In AndroidPdfViewer 4.0.1, DecodingAsyncTask.onCancelled() only sets a flag. The PdfFile it opened is never disposed, so its native document leaks.

Details and logs are in #1046.

Fix

drawPdf() now builds a string from every value it passes to the Configurator (path, password, spacing, fit policy, paging flags, swipe/double-tap/annotation/RTL flags, min/max scale). It skips the reload when that string matches the last load and the view is not recycled.

  • Real changes still reload. Any change to one of those props, a detach/re-attach (the view is recycled), or a load error (loadError() recycles the view) goes through as before.
  • page is not part of the key. It tracks the displayed page (onPageChanged), and setPage() already applies a new page prop to the live view. Including it would trigger a reload after every scroll.
  • Side benefit: a parent re-render that doesn't change these props no longer reloads the document, so zoom and scroll are no longer reset. This is likely what the memo workaround in Crash on Android When Closing PDF Page After Scrolling || java.lang.IllegalStateException: Already closed #976 was compensating for.

Testing

Physical device: Samsung Galaxy Tab Active5 (SM-X306B), Android 16 / API 36, arm64, debug build of FabricExample (RN 0.81.1, New Architecture), local file:// PDF (1.3 MB, 21 pages).

Scenario Before After
Mount → onLoadComplete + 1.5 s → unmount, repeated native heap 81 → 513 MB over 142 mounts (≈ 3.0 MB/mount), never released flat, 100–111 MB over 145 mounts
sLibraryReferenceCount after each unmount (open native documents) grows by ~1 per mount back to 0 after every unmount
Source switched on a mounted view, 500 times — 497 loads, 0 errors, 0 timeouts: source changes still reload
Unmount 0–150 ms after load, 20 % of unmounts before load completes, 2 × 500 iterations grows to ~1.5 GB 81 → 273 MB, see limitation below

No crash in any of these runs.

Remaining limitation

Unmounting the viewer while the document is still decoding still cancels a DecodingAsyncTask. In the stress test above, that leaves roughly one orphaned document per 7 early unmounts. That cancellation is legitimate, so the proper fix belongs in AndroidPdfViewer: DecodingAsyncTask.onCancelled() should dispose the PdfFile it opened. Working around it from here would mean reflection on a private field of that class, so I left it out of this PR.

Related

…ative memory leak

drawPdf() runs several times per mount (setPath, onAfterUpdateTransaction,
onAttachedToWindow) and again on every prop transaction, and each call
reloads the document. A reload cancels the in-flight DecodingAsyncTask, but
AndroidPdfViewer 4.0.1's DecodingAsyncTask.onCancelled() never disposes the
document it may already have opened, which leaked about 3 MB of native
memory per mount.

drawPdf() now skips the reload when the view is not recycled and the load
configuration is unchanged. page is left out of that configuration since it
tracks the displayed page and setPage() already applies it to the live view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

[Android] Native memory leak : redundant drawPdf() reloads leak the pdfium document of cancelled loads

1 participant