Native Memory and Object Lifetime Pixelnetica™ Document Scanning SDK for Android
The SDK’s engine keeps images and recognition state in native memory, outside the Java heap — which is why a ScanPicture holding a 12-megapixel page does not strain your heap, and also why the garbage collector alone is a poor manager for it: the collector sees a small Java object and feels no pressure, while megabytes sit allocated behind it. Since version 3.2.0 you can release that memory deterministically. This page is the contract.
Which Objects Hold Native Memory
ScanPicture, ScanReader, ScanText, ScanCutout, ScanDetector, FrameObserver, ScanLayout, and the ImageWriter family (ImageWriterPdf, ImageWriterTiff, ImageWriterPng) are all backed by native memory, and every one of them implements AutoCloseable. As a rule of thumb: if the API reference shows a type inheriting from ScanningSdkLibrary.Instance, it holds a native handle.
The Contract
Closing is optional for the scan types. An unclosed instance is still released by the garbage collector eventually, so existing code keeps working unchanged. The one exception is the writers: an ImageWriter must be closed to finish rendering its output file.
close() is idempotent. A second call is a harmless no-op.
A closed object refuses native work. After close(), an otherwise-valid operation that needs the instance’s native state — whether the object is the receiver or an argument — throws a catchable ScanningSdkException. Plain Java-side access stays safe.
You own what the SDK returns. Any instance the SDK hands you may be closed by you.
The Patterns
Scope an object to a block with Kotlin’s use { } — it closes at the end of the block, on the happy path and on exceptions alike:
ScanPicture(context, uri).use { page ->
page.detectCutout().use { cutout ->
page.refine(
listOf(
RefineFeature.Rectify.WithCutout(cutout),
RefineFeature.Profile(RefineFeature.Profile.Type.Bitonal),
),
)
}
}
Note that the detected cutout is a native-handle object in its own right — anything the SDK returns is yours to close, so it gets its own use { }. From Java, the same contract is try-with-resources:
try (ScanPicture page = new ScanPicture(context, uri);
ScanCutout cutout = page.detectCutout()) {
// process the page
}
Where a block does not fit — an object living in a view model, say — call close() at the natural end of its life, such as onCleared().
Why Loops Are the Point
One page’s native memory rarely matters. Loops are where the contract earns its place: a batch that scans, recognizes, or exports many pages accumulates every page’s native memory until the collector runs — unless each page is released as it finishes:
pageUris.forEach { uri ->
ScanPicture(context, uri).use { page ->
// detect, refine, export this page
}
}
With use { } per page, the batch’s native footprint stays flat at roughly one page regardless of length. The demo application processes its batches exactly this way.
Threads
Two rules cover the SDK’s native handles:
- Confine an instance to one thread unless its own reference page says otherwise. Most types hold native state with no locking of their own, so concurrent use is undefined.
FrameObserveris the documented exception — see its reference page for what it supports. - Never close an instance while another thread is inside a call using it. This is the same discipline as
android.graphics.Bitmap.recycle(): finish the work, then close.
One Trap Worth Knowing
An SDK object records the license state in force when it is constructed, permanently. Construct one before ScanningSdkLibrary.load() and it stays bound to the unlicensed state — its output watermarked — no matter what loads afterwards, and nothing throws to warn you. Initialize the SDK in Application.onCreate() (see Get started) and the trap never fires.