崩溃报告与符号化堆栈跟踪
结合 Crashlytics 和 Sentry 使用反混淆映射,生成可采取行动的崩溃报告。
崩溃报告与符号化堆栈跟踪 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Raw Crash Traces Are Useless
When you ship a Flutter app in release mode, the Dart compiler produces optimized AOT machine code and strips human-readable symbols. A crash that happens in production no longer reports UserRepository.fetchProfile — it reports a hex offset like #00 abs 0000007f9c2a1b40.
- Debug builds keep symbols, so traces are readable locally.
- Release builds obfuscate and strip, so the on-device trace is just addresses.
- To turn those addresses back into method names you need symbolication (native iOS/Android symbols) and deobfuscation (Dart symbol mapping).
This lesson wires up Crashlytics and Sentry so every production crash arrives fully symbolicated and actionable.
Obfuscation and the Symbol Map File
Flutter obfuscation is opt-in. You enable it at build time and Flutter writes a Dart symbol map per target architecture into a directory you choose.
--obfuscaterenames Dart identifiers to short tokens.--split-debug-infowrites the mapping files (e.g.app.android-arm64.symbols) so you can reverse the obfuscation later.
You must archive these symbol files per release. Without the exact file that matches the shipped binary, the trace can never be deobfuscated.
# Build an obfuscated Android release and keep the Dart symbols
flutter build appbundle --release \
--obfuscate \
--split-debug-info=build/symbols/v1.4.0
# iOS equivalent
flutter build ipa --release \
--obfuscate \
--split-debug-info=build/symbols/v1.4.0Two Layers: Native vs Dart Symbols
A Flutter crash report has two distinct symbolication problems, and confusing them is the #1 reason traces stay unreadable.
- Native layer (iOS dSYM, Android NDK
.sosymbols): resolves crashes inside the engine, plugins, and platform code. Handled by Crashlytics' native symbol upload. - Dart layer (the
--split-debug-infofiles): resolves crashes inside your Dart business logic after obfuscation.
Crashlytics symbolicates the native frames automatically once you upload dSYMs/NDK symbols, but the Dart frames still need the Flutter symbol file run through flutter symbolize.
Initializing Firebase Crashlytics
Crashlytics must be initialized before runApp and wired into Flutter's error hooks so nothing slips through.
FlutterError.onErrorcaptures synchronous framework errors.PlatformDispatcher.instance.onErrorcaptures async errors that escape the zone.
Routing both into recordFlutterFatalError / recordError guarantees every uncaught Dart error reaches Crashlytics.
import 'dart:ui';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
final crashlytics = FirebaseCrashlytics.instance;
FlutterError.onError = crashlytics.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
crashlytics.recordError(error, stack, fatal: true);
return true;
};
runApp(const MyApp());
}Uploading Native Symbols to Crashlytics
For native frames, Crashlytics needs the build's symbol files uploaded to Firebase.
- iOS: the
FirebaseCrashlyticsrun script uploads dSYMs; for bitcode or missing dSYMs use theupload-symbolstool. - Android: the
firebase-crashlyticsGradle plugin uploads NDK symbols whennativeSymbolUploadEnabledis true.
These uploads happen at build/release time, not at runtime, so they belong in your CI pipeline.
// android/app/build.gradle
plugins {
id 'com.google.firebase.crashlytics'
}
android {
buildTypes {
release {
firebaseCrashlytics {
// Upload Android NDK native symbols
nativeSymbolUploadEnabled true
unstrippedNativeLibsDir 'build/app/intermediates/merged_native_libs'
}
}
}
}Deobfuscating Dart Frames with flutter symbolize
Crashlytics cannot read your Dart --split-debug-info map. When a crash's Dart frames show obfuscated tokens, you export the raw trace and run it through the Flutter SDK tool.
flutter symbolizetakes the obfuscated stack trace plus the exact symbol file for that ABI and version.- The output replaces hex offsets and short tokens with real Dart method names and line numbers.
This is why archiving the per-release symbol directory is non-negotiable — you need the file matching the crashing build.
# stack.txt holds the obfuscated trace copied from Crashlytics
flutter symbolize \
-i stack.txt \
-d build/symbols/v1.4.0/app.android-arm64.symbols
# Output now shows real frames, e.g.:
# #00 UserRepository.fetchProfile (package:app/data/user_repository.dart:42)Adding Sentry Alongside Crashlytics
Sentry is a popular alternative or companion to Crashlytics. The Flutter SDK wraps your app and auto-captures unhandled errors, but the real value is its debug symbol pipeline.
SentryFlutter.initinstalls the integrations and lets you set sampling, release, and environment.- Wrapping
runAppinappRunnerensures the zone-level error handler is active.
Set the release string to name+buildNumber so Sentry can match the crash to the correct uploaded symbols.
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async {
await SentryFlutter.init(
(options) {
options.dsn = 'https://examplePublicKey@o0.ingest.sentry.io/0';
options.tracesSampleRate = 0.2;
options.environment = 'production';
options.release = 'com.coddykit.app@1.4.0+140';
},
appRunner: () => runApp(const MyApp()),
);
}Uploading Debug Symbols to Sentry
Sentry deobfuscates automatically server-side once you upload the right artifacts with sentry-cli (or the Sentry Dart plugin during the build).
- Upload the native dSYM / ELF debug files via
debug-files upload. - Upload the Flutter Dart symbols (the
--split-debug-infooutput) so obfuscated Dart frames resolve.
Because Sentry matches by debug ID and release, uploads from CI must use the same --split-debug-info directory that produced the shipped binary.
# Upload native + Dart debug information files for this release
sentry-cli debug-files upload \
--org coddykit --project flutter-app \
build/symbols/v1.4.0
# Associate the release so Sentry can match incoming events
sentry-cli releases new com.coddykit.app@1.4.0+140
sentry-cli releases finalize com.coddykit.app@1.4.0+140Enriching Reports with Context and Breadcrumbs
A symbolicated trace tells you where; context tells you why. Both SDKs let you attach metadata so a crash is reproducible.
- Custom keys: feature flags, user tier, current route.
- Breadcrumbs / logs: a trail of recent actions leading to the crash.
- User identifier: to gauge how many users a crash affects.
Never log PII. Use opaque IDs and feature-relevant flags only.
Future<void> tagCrashContext(FirebaseCrashlytics c) async {
await c.setUserIdentifier('user_8f3a');
await c.setCustomKey('subscription', 'pro');
await c.setCustomKey('active_route', '/checkout');
await c.log('Tapped Pay button with cart total 49.99');
}Recording Handled (Non-Fatal) Errors
Not every error should crash the app. A failed network call or a caught parsing error is a non-fatal event you still want visibility into.
- Crashlytics:
recordError(e, st, fatal: false). - Sentry:
Sentry.captureException(e, stackTrace: st).
This standalone Dart example shows the catch-and-report pattern you would feed into either SDK from a service layer.
void reportHandled(Object error, StackTrace stack) {
// In production this would call recordError / captureException.
print('NON-FATAL: $error');
print('Top frame: ${stack.toString().split('\n').first}');
}
Map<String, dynamic> parseProfile(String raw) {
if (!raw.startsWith('{')) {
throw const FormatException('Profile payload is not JSON');
}
return {'ok': true};
}
void main() {
try {
parseProfile('not-json');
} catch (e, st) {
reportHandled(e, st);
}
print('App keeps running after handled error');
}Making Symbol Upload Part of CI
The single biggest cause of unreadable production crashes is a broken release process: someone ships a build but forgets to upload or archive its symbols. Automate it.
- Build with
--obfuscate --split-debug-info=build/symbols/$VERSION. - Upload native symbols (Crashlytics Gradle plugin / iOS run script) and Dart symbols (
sentry-cli) in the same CI job. - Persist
build/symbols/$VERSIONas a CI artifact retained for as long as the version can crash in the wild.
Tie the version string used everywhere — Sentry release, Crashlytics setCustomKey, and the symbol directory name — to one source of truth so matching never drifts.
# CI snippet (bash) — one job that builds and ships symbols
VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}')
flutter build appbundle --release \
--obfuscate --split-debug-info=build/symbols/$VERSION
# Dart + native symbols to Sentry
sentry-cli debug-files upload --org coddykit --project flutter-app \
build/symbols/$VERSION
# Keep the map for later flutter symbolize runs
tar -czf symbols-$VERSION.tgz build/symbols/$VERSIONQuick Check: Why Dart Frames Stay Obfuscated
You shipped a Flutter release built with --obfuscate --split-debug-info. Native frames in Crashlytics are readable, but the Dart frames in your business logic still show short tokens and hex offsets. What is the correct fix?
Recap: An Actionable Crash Pipeline
You now have an end-to-end pipeline that turns cryptic production crashes into fixable bug reports.
- Build release with
--obfuscate --split-debug-info=build/symbols/$VERSIONand archive that directory per release. - Capture via
FlutterError.onError+PlatformDispatcher.onError(Crashlytics) and/orSentryFlutter.initwithappRunner. - Symbolicate two layers: native (dSYM/NDK uploads) and Dart (
flutter symbolizefor Crashlytics, automatic server-side for Sentry after symbol upload). - Enrich with user IDs, custom keys, and breadcrumbs — no PII.
- Automate symbol upload in CI and keep one version string as the single source of truth.
The golden rule: a crash report is only as good as the symbol file you kept for that exact build.
常见问题解答
「崩溃报告与符号化堆栈跟踪」课时是免费的吗?
是的 — 「崩溃报告与符号化堆栈跟踪」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。
「崩溃报告与符号化堆栈跟踪」这节课中我会学到什么?
结合 Crashlytics 和 Sentry 使用反混淆映射,生成可采取行动的崩溃报告。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Flutter Mobile Development 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「崩溃报告与符号化堆栈跟踪」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Flutter Mobile Development 课中编写并运行代码吗?
能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。