In Flutter, what do --obfuscate and --split-debug-info each do, and why does the tool refuse --obfuscate on its own?
answer
- rename symbols, move debug info out
- the map lives in the symbols file
- app.<arch>.symbols per architecture
- runtimeType.toString breaks
- not encryption, not on web
basics
~10 s--split-debug-info moves Dart debug symbols out of the app into app.<arch>.symbols files; --obfuscate renames Dart identifiers. Obfuscate requires split-debug-info because those files hold the mapping needed to read obfuscated stack traces.
solid answer
~40 s`--split-debug-info=<dir>` stores the Dart program's debugging information in `<dir>/app.<arch>.symbols` files on the build machine instead of in the app, and makes stack traces print addresses (DWARF stack traces) that `flutter symbolize` can turn back into frames. `--obfuscate` renames classes, functions and other identifiers in the compiled Dart code. The tool exits with `"--obfuscate" can only be used in combination with "--split-debug-info"` because the mapping from obfuscated names back to real ones is stored in those symbol files; without them a crash could never be read. It works only in release builds on mobile and desktop targets, not on the web, where the compiler minifies instead. It does not encrypt assets or strings, and code that relies on `runtimeType.toString()` or type names breaks.
code
bash · 5 linesflutter build appbundle --obfuscate --split-debug-info=build/symbols/1.4.0+87
ls build/symbols/1.4.0+87
# app.android-arm.symbols app.android-arm64.symbols app.android-x64.symbols
flutter build ipa --obfuscate --split-debug-info=build/symbols/1.4.0+87-iosgo deeper
Recall that --obfuscate renames Dart identifiers and always needs --split-debug-info, which writes the symbol files.
Explain what the symbol files contain, why traces become addresses, which targets are supported, and what name-dependent code breaks.
Make obfuscated releases operable: per-release symbol storage, name-independent logging and analytics, and a tested symbolization path before a crash arrives.
Weigh obfuscation's modest protection against its operational cost, and keep it from being mistaken for a security control.
## Two flags, two jobs A release Flutter app contains **AOT-compiled Dart code**: machine code produced from Dart ahead of time. Two `flutter build` options change what that code carries. - **`--split-debug-info=<directory>`** — tells the Dart snapshot generator to save the program's debugging information into `<directory>/app.<arch>.symbols`, for example `app.android-arm64.symbols` or `app.ios-arm64.symbols`, instead of keeping it in the app. The tool also turns on DWARF stack traces, so crashes print as addresses rather than names. - **`--obfuscate`** — renames identifiers in the compiled Dart code to meaningless short names, making the binary harder to read. ## Why --obfuscate needs --split-debug-info Once names are replaced, a stack trace from a user's device is unreadable unless you keep the mapping. The tool stores that mapping in the same symbols files, so it enforces the pairing: ``` "--obfuscate" can only be used in combination with "--split-debug-info" ``` The flag's own help states the mapping between the random values and the original identifiers lives in the symbol map in that directory, and that `flutter symbolize` with the right file is needed for a readable trace. ## What each flag changes | | `--split-debug-info` only | plus `--obfuscate` | |---|---|---| | Debug info in app | moved out to `.symbols` files | moved out | | Crash stack traces | addresses, symbolize to read | addresses, symbolize to read | | Identifier names in binary | original | renamed | | `runtimeType.toString()` | real name | obfuscated name | Note the first row: even without obfuscation, `--split-debug-info` alone already makes traces need symbolization, because the names are no longer in the app. ## Where it works 1. **Release builds only**; the docs state obfuscation works only on a release build. 2. **Supported targets** listed in the docs: `aar`, `apk`, `appbundle`, `ios`, `ios-framework`, `ipa`, `linux`, `macos`, `macos-framework`, `windows`. 3. **Not the web**: web apps do not support obfuscation; the release web compiler minifies instead. 4. **Windows x64** writes a PDB file instead of a `.symbols` file, and `flutter symbolize` cannot read PDBs; a Windows debugger does. `--split-debug-info` also cannot be combined with `--analyze-size`. ## What obfuscation does not do - It does **not encrypt** resources or strings; the docs say it only renames symbols and does not protect against reverse engineering. - It is **not a place for secrets**: an API key in the binary is still extractable. - It **breaks name-dependent code**. The tool's help lists `Object.runtimeType`, `Type.toString`, `Enum.toString`, `StackTrace.toString` and `Symbol.toString` as returning obfuscated results. A `switch` on `runtimeType.toString()`, a log that keys analytics events on class names, or a test asserting `foo.runtimeType.toString() == 'Foo'` all change behaviour. The docs page separately says enum names are not obfuscated; since the tool's help says otherwise, do not rely on either. ## Reading the tool's messages A few messages and behaviours are worth recognising: - **Missing pairing.** `--obfuscate` alone stops the build before compiling, with the message quoted above. - **Wrong mode.** Obfuscation only means something in release builds; in debug the app runs on the JIT and is not obfuscated. - **Size side effect.** Moving debug info out of the app also makes it smaller; how much is a size question rather than a versioning one. - **Crash reports change shape.** Once `--split-debug-info` is on, every Dart crash report from the field needs symbolization, including from builds that were never obfuscated. Teams that enable it only for one platform often forget that their other platform's reports now look different. - **Symbols per architecture.** An Android build writes one file per target ABI, so an app bundle with three ABIs produces three files that must all be kept. ## Operating it - Pass a directory per release, such as `build/symbols/1.4.0+87`, so symbols from different builds never overwrite each other. - **Back up the symbols** with the release; the docs warn you need them to de-obfuscate a stack trace later. - Optionally save a JSON obfuscation map with `--extra-gen-snapshot-options=--save-obfuscation-map=<file>` to look up an individual renamed name.
- Your analytics events are named with runtimeType.toString(); what happens after enabling --obfuscate?Event names become the obfuscated identifiers, and they can change between builds, so dashboards split and break. Map each type to an explicit string, for example with a `switch` over a sealed class, instead of reading type names at runtime.
- Can you obfuscate a Flutter web build?No. The docs say web apps do not support obfuscation; a release web build is minified by the web compiler, which gives a similar effect. `--obfuscate` applies to the mobile and desktop targets the docs list.
saying these in an interview costs you the question
- --obfuscate encrypts the app's assets and string constants.
- Obfuscation makes it safe to ship API keys in the binary.
- --split-debug-info alone leaves stack traces human-readable.
- You can use --obfuscate without --split-debug-info and symbolize later.
- Obfuscation works the same way for Flutter web builds.
- runtimeType.toString() keeps returning real class names after obfuscation.