Runtime and integration

Native Library Load Order for Android Local AI

Make dependency order, class-loader ownership, initialization, duplicate loads, failure propagation, and cleanup observable.

Review status: Fact and editorial QA complete; owner review pending · 2026-10-05 · Fact IDs: COVE-LIFECYCLE-01, MEMORY-03

Direct answer

Native libraries have dependencies and initialization behavior that should be visible in the application's runtime design. Android's JNI guidance describes loading with System.loadLibrary and recommends placing the call in a static initializer or another deliberately early owner. The key diagnostic is not “load every library until something works”; it is to identify which library owns a method, which dependency it needs, and which initialization step failed first. A model runtime can depend on more than one shared object, while application code may also have JNI callbacks or vendor delegates. A missing file, unresolved dynamic dependency, duplicate initialization, or native method registration failure can surface through different exceptions and should not be merged into one generic “model failed” event. Write the dependency graph before changing code. Record library names without platform prefixes, the module or dependency that packages each one, and the code path that first calls into it. Distinguish a library that is part of the app package from a system-provided or optional library declared for a platform backend. Do not guess that a library exists on every phone, or declare an optional native component required merely to silence a warning. The official API documents the name passed to System.loadLibrary; the NDK JNI documentation discusses load and registration behavior. Neither source proves which exact native objects a particular Cove artifact contains. A safe owner should perform one intentional initialization path and retain the resulting state. Avoid a System.loadLibrary call as a side effect of a composable, repeated screen constructor, or many concurrent feature entry points. Let an application or runtime adapter own initialization, record success or failure, and prevent later tasks from silently re-running a half-completed initialization. If a retry is allowed, define which state and resources are reset first. The owner must not swallow UnsatisfiedLinkError; preserve the earliest failure type and sanitized stage so the diagnosis can distinguish “library not found,” “library found but cannot be opened,” “JNI registration failed,” and “method exists but throws.” Test on the exact packaged artifact, not only from a developer build directory. Compare debug and release packaging, inspect which libraries are actually present in the APK or install splits, and run a cold load plus a repeated request in the same process. Include a missing-dependency fixture only in a controlled test build, then verify the app produces a bounded error and leaves no partial engine marked ready. If the library loads but a callback cannot be found only after minification, investigate R8 separately. If the library is absent for the device ABI, inspect packaging. If the method is found and throws, inspect the JNI exception boundary rather than changing load order. Record useful diagnostics without leaking content: build variant, app version, runtime artifact revision, library name, ABI, first load stage, exception class, and whether the runtime owner closed. Do not log prompts, images, audio, or complete private file paths. A load-order test can prove a sequence for the exact build, but cannot prove universal device compatibility or inference performance. In particular, a System.loadLibrary success is only evidence that one library was mapped; it does not prove the model artifact, delegate, or task is ready. Design recovery as a state transition. If loading fails, release partially created objects according to their ownership contract, expose an unavailable state, and prevent requests from entering a native object that never reached ready. A retry should be user-initiated or policy-controlled, bounded, and observable. It should not loop on every composition or app resume. When a later load succeeds, clear only the error state belonging to that attempt and bind new sessions to the newly initialized runtime. This prevents stale callbacks from an old failed attempt from changing current readiness. When there are multiple libraries, minimize order assumptions that are undocumented. Use the owning runtime's public initialization path rather than manually preloading internal dependencies unless official documentation says to. Keep application-level ordering explicit and covered by tests: process startup, owner initialization, engine creation, task, close, then optional reinitialization. This is a testable sequence, not an assertion that every library uses the same global initialization model. The seed facts identify lifecycle and memory boundaries; they do not supply a verified Cove linker trace. Keep conclusions limited to observed library packaging and reproducible runtime diagnostics.

A cold launch fails before the model engine becomes ready

A release build starts a local-AI feature. The adapter loads one native component, then receives a loader error before task submission; a second screen may also have attempted initialization. The team needs to retain the first failure and prevent duplicate load attempts.

Decision

Trace the dependency and owner sequence from the packaged artifact, classify the first failure, and prohibit a second initializer from masking it.

Expected outcome

The user sees a bounded unavailable state, while the team can distinguish packaging, linker, registration, and task failures.

Evidence to collect

The claim becomes reviewable only when the following evidence is attached to the same artifact and test run:

  • Native dependency graph
  • Cold and repeated load traces
  • Failure and post-failure recovery

A runtime comparison is publishable only when artifact identity, build configuration, selected backend, task fixture, and failures are recorded.

Implementation workflow

  1. Inventory the actual native dependency graph. For each library name, record its packaging owner, required dependencies, optional platform status, and first caller. Separate app-bundled objects from libraries provided or conditionally required by the platform.
  2. Choose one initialization owner. Place loading behind a runtime adapter or application-level owner. UI construction and recomposition must observe initialization state rather than independently triggering library loads.
  3. Preserve the earliest loader failure. Capture the exception category and stage before recovery attempts. Distinguish absent file, open/link failure, JNI registration or symbol lookup, and a method that is found but throws.
  4. Reproduce against the installed artifact. Inspect the actual APK and device-targeted splits for library presence and ABI, then test cold and repeated loads from the installed debug and release artifacts.
  5. Assert failure cleanup and controlled retry. Keep the engine unavailable after a partial failure, close owned partial resources, and allow only a bounded retry under a defined policy. Verify stale callbacks cannot mark a newer attempt ready.

Keep each transition observable. A failure should identify the stage, artifact, runtime, and recovery action without logging private user content.

Acceptance criteria

  • The dependency graph and initialization owner are documented for the exact tested build and runtime revision.
  • The earliest failure category is retained; packaging, linker, JNI registration, and task exceptions are not collapsed into one message.
  • A failed partial initialization never exposes the engine as ready or accepts a new inference request.
  • Retry and close behavior are bounded, owned, and tested on an installed artifact; no content is written to diagnostic logs.

Decision table

DimensionChoice or evidenceHow to interpret it
Library absent from installed packageInspect APK/split contents and ABI packagingA delivery boundary; changing runtime call order cannot add a missing object.
Library exists but loader cannot open itInspect dependency/linker error and load stageA native mapping or dependency boundary; keep the earliest exception details.
Library loaded but method or callback failsInspect JNI registration, R8, and exception boundaryLoading succeeded; classify symbol visibility and method behavior separately.

Failure patterns to prevent

  • Loading libraries from UI composition
  • Swallowing loader exceptions

Also prevent silent fallback, unpinned artifacts, missing cancellation, and conclusions that combine unlike configurations. Store unsuccessful runs alongside successful ones.

Minimum reproducibility record

LayerRecord
DeviceManufacturer, model, chipset, RAM class, operating-system build
SoftwareApplication version and git commit
ModelFamily, variant, revision, format, file length, hash, quantization
RuntimeName, revision, requested backend, observed backend evidence
WorkloadFixture revision, input hash, prompt hash, output policy
OutcomeCompleted, failed, cancelled, fallback, and privacy-safe diagnostics

Release checklist

  • ☐ The primary query is answered without an unsupported number.
  • ☐ Every artifact and runtime is pinned.
  • ☐ The representative task and failure policy are explicit.
  • ☐ Lifecycle, cancellation, cleanup, and fallback are tested.
  • ☐ User-content and network boundaries are documented.
  • ☐ Result wording applies only to the recorded configuration.
  • ☐ The page links to raw method or evidence when results are added.

Sources and related evidence

Chinese deployment and troubleshooting content is organized in the .奇连 AI 端侧专题.

Frequently asked questions

Should every native dependency be loaded manually at application startup?

No. Follow the owning runtime’s documented initialization path. Manually preloading internal libraries can add undocumented order assumptions and obscure the first failure.

Does one successful loadLibrary call prove inference is ready?

No. It proves only that a named object was mapped in that process. Model validation, engine initialization, backend setup, and task acceptance remain separate gates.

Can the app retry automatically whenever a screen opens?

Avoid that pattern. A retry needs a bounded policy, cleanup of partial resources, one owner, and a new observable attempt; recomposition should never trigger it.