Dex DebugInfo Line Number Optimization in Android Applications
The article explains how Baidu’s Dex line-number optimization replaces original line numbers with compact PC-based mappings, unifies debug_info_items, and uses a server-side mapping file to shrink DebugInfo by roughly 8% of the Dex size while keeping stack-trace line numbers fully recoverable.
In the previous article we briefly introduced the basic ideas of Android package size optimization and its various optimization items. This article focuses on line‑number optimization of Dex DebugInfo, aiming to reduce the size of DebugInfo while keeping the original debugging information traceable.
We refer to existing line‑number optimization schemes from the industry (e.g., Alipay, R8) that replace the line‑number set with a PC set, achieving maximum reuse of DebugInfo and solving the overlapping line‑number range problem for overloaded methods, while providing a complete original line‑number retrace solution.
As shown in Figure 1‑1, the DebugInfo of two methods is visualized. The mapping between instruction set and original line numbers is exported to a mapping file and uploaded to the server for later retrace processing. After mapping, the DebugInfo of both methods becomes identical, achieving a reusable state.
Next we will detail DebugInfo analysis, compare existing solutions, present Baidu APP's optimization scheme, and discuss the benefits.
2.1 Dex DebugInfo
In the Dex file format, DebugInfo resides in the data section and consists of a series of debug_info_item structures.
Typically, each debug_info_item corresponds one‑to‑one with a class method. The reference relationship in Dex is shown in Figure 2‑2, where offsets (x_off) determine the location of reference areas.
The debug_info_item structure consists of a header and a series of debug_event s. The header contains the method start line, the number of method parameters, and parameter names. The debug_events act as a state machine, recording PC pointers and line‑number deltas.
Common debug_event types are listed in the table below:
Name
Value
Parameter
Description
DBG_END_SEQUENCE
0x00
None
End of debug_info_item, cannot be modified
DBG_ADVANCE_PC
0x01
pcDelta
Contains only PC offset
DBG_ADVANCE_LINE
0x02
lineDelta
Contains only line offset
Special Opcodes
[0x0a,0xff]
None
PC and line offsets can be derived from the value
The conversion formula for Special Opcodes is:
DBG_FIRST_SPECIAL = 0x0a // smallest special opcode
DBG_LINE_BASE = -4 // smallest line number increment
DBG_LINE_RANGE = 15 // number of line increments represented
adjusted_opcode = opcode - DBG_FIRST_SPECIAL
line += DBG_LINE_BASE + (adjusted_opcode % DBG_LINE_RANGE)
address += (adjusted_opcode / DBG_LINE_RANGE)2.2 DebugInfo Usage Scenarios
DebugInfo is commonly used for breakpoint debugging and stack trace location (including crash, ANR, memory analysis, etc.). When an exception occurs, the JVM obtains a StackTrace that stores ArtMethod objects and corresponding PC values without line numbers. The native method nativeGetStackTrace converts this to a StackTraceElement[] , which includes source file and line number (see Figure 2‑4).
The relevant code path is shown below (excerpt from ART source):
// art/runtime/native/java_lang_Throwable.cc
static jobjectArray Throwable_nativeGetStackTrace(JNIEnv* env, jclass, jobject javaStackState) {
...
ScopedFastNativeObjectAccess soa(env);
return Thread::InternalStackTraceToStackTraceElementArray(soa, javaStackState);
}
// art/runtime/thread.cc
jobjectArray Thread::InternalStackTraceToStackTraceElementArray(const ScopedObjectAccessAlreadyRunnable& soa,
jobject internal, jobjectArray output_array, int* stack_depth) {
...
for (uint32_t i = 0; i < static_cast
(depth); ++i) {
ObjPtr
> decoded_traces = ...;
const ObjPtr
method_trace = ObjPtr
::DownCast(decoded_traces->Get(0));
ArtMethod* method = method_trace->GetElementPtrSize
(i, kRuntimePointerSize);
uint32_t dex_pc = method_trace->GetElementPtrSize
(i + static_cast
(method_trace->GetLength()) / 2, kRuntimePointerSize);
const ObjPtr
obj = CreateStackTraceElement(soa, method, dex_pc);
...
}
return result;
}
static ObjPtr
CreateStackTraceElement(const ScopedObjectAccessAlreadyRunnable& soa,
ArtMethod* method, uint32_t dex_pc) {
int32_t line_number = method->GetLineNumFromDexPC(dex_pc);
...
}From the GetLineNumForPc method we can see that the VM traverses the corresponding DebugInfo to retrieve the original line number. In our scheme we set all pcDelta to 1, which slightly increases traversal length but the processing is trivial, so performance impact is negligible.
3 Existing Optimization Schemes
3.1 Extreme Optimization
DebugInfo can be completely removed, which saves size but makes stack traces lose line numbers (they show –1). This is acceptable only for highly stable apps where debugging is rarely needed.
Java compilers and obfuscation tools provide options to omit DebugInfo attributes (SourceFile, SourceDebugExtension, LineNumberTable, LocalVariableTable) from class files (see Figure 3‑1).
3.2 Mapping Optimization
Instead of removing DebugInfo, we keep the DebugInfo region but change the 1‑to‑1 relationship between methods and debug_info_item to an N‑to‑1 reuse relationship. This reduces the number of debug_info_item structures and thus the size, while a mapping file records the before‑and‑after relationship for later retrace. Alipay, R8, and Baidu APP all use this approach.
Two debug_info_item s are considered equal if their start line, parameters, and events are identical. Since we do not need method parameters for stack traces, only start line and debug_events need to be unified.
// Pseudo‑code for equality check
public boolean equals(DebugInfoItem other) {
return this.startLine == other.startLine &&
this.parameters.equals(other.parameters) &&
this.events.equals(other.events);
}Similarly, debug_event equality is defined as:
public boolean equals(DebugEvent other) {
return this.type == other.type && this.value == other.value;
}To achieve reuse we control the following variables: startLine , number of debug_events , event type, lineDelta , and pcDelta (opcode is derived from the two deltas).
3.3 Alipay Line‑Number Optimization
Alipay proposes two schemes:
Extract all DebugInfo into a separate debugInfo.dex file, removing DebugInfo from the main APK.
When a crash occurs, hook Throwable to obtain the instruction‑set line number and upload it.
The performance platform uses the uploaded debugInfo.dex to map the instruction line number back to the original source line.
This approach works only for Throwable scenarios and requires handling different JVM stack‑trace structures.
3.4 R8 Line‑Number Optimization
R8 keeps LineNumberTable and modifies debug_info_item as follows:
startLine : default 1; for overloaded methods the later method’s startLine becomes previous endLine +1.
lineDelta : default 1.
Although this enables some reuse, the number of debug_events and pcDelta remain uncontrolled, limiting reuse.
4 Baidu APP Dex Line‑Number Optimization Scheme
4.1 Client‑Side Optimization
Variables are controlled as follows:
startLine : default 100000 (much larger than R8’s default of 1) to avoid overlap in hot‑fix or plugin scenarios.
debug_event : besides the start/end events, all other events are special opcodes with pcDelta=lineDelta=1 . The number of events equals the method’s instruction count.
pcDelta : first special opcode is 0, others are 1.
lineDelta : same as pcDelta , i.e., 1.
Figure 4‑2 illustrates the ideal line‑number interval distribution, and Figures 4‑3/4‑4 show the mapping between instruction count and debug_event quantity.
4.2 Performance Platform Line‑Number Retrace
After optimization, the app reports virtual line numbers. The performance platform receives crash/ANR data, looks up the corresponding mapping file (uploaded during release), and converts virtual line numbers back to real source lines. The architecture consists of a streaming computation service, a multi‑level cache (in‑memory → Redis → Table), and a mapping‑file parsing service.
Mapping files have the format:
ClassName:
methodDescriptor:
[mappedStart-mappedEnd] -> originalLine
...Example:
com.baidu.searchbox.Application:
void onCreate(android.os.Bundle):
[1000-1050] -> 20
[1051-2000] -> 22
void onCreate():
[3000-3020] -> 30
[3021-3033] -> 31The streaming service caches hot mapping entries in operator memory, falls back to Redis, and finally to persistent storage, ensuring millisecond‑level lookup latency.
5 Summary
This article introduced the structure of Dex DebugInfo, analyzed its usage, compared several optimization schemes, and detailed Baidu APP’s line‑number optimization and retrace workflow. By controlling startLine , pcDelta , lineDelta , and the number of debug_event s, Baidu achieved a significant reduction in Dex size (approximately 8% of Dex, 3.04 MB of the APK) while preserving accurate stack‑trace information through a robust server‑side mapping service.
Baidu App Technology
Official Baidu App Tech Account
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.