百度APP Android包体积优化实践(二)Dex行号优化
<img src="https://mmbiz.qpic.cn/mmbiz_gif/5p8giadRibbOichNxgNgW2xyHYqnHnww2mnGT5PC84tpKOThGv2k88zXbdh8DTfA38RniadrhC3y4JagKaIEPTNPlQ/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">GEEK TALK</span><p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><strong style="color: blue;">01</strong></span></p><strong style="color: blue;">前言</strong><span style="color: black;">在上一篇<span style="color: black;">文案</span>中,</span><span style="color: black;"><span style="color: black;">咱们</span>简要介绍了 Android 包体积优化的基本思路以及各优化项。本文<span style="color: black;">咱们</span>会重点讲述 Dex 体积优化中的行号优化,优化<span style="color: black;">目的</span>是在可追溯原始调试信息的前提下,尽可能减少 DebugInfo 体积。</span><span style="color: black;"><span style="color: black;">咱们</span>参考了业界已有的行号优化<span style="color: black;">方法</span></span><span style="color: black;">(如支付宝、R8)</span><span style="color: black;">,采用将行号集改为pc集的方式,做到最大程度复用 DebugInfo,<span style="color: black;">同期</span><span style="color: black;">处理</span>了重载<span style="color: black;">办法</span>行号区间重叠问题,并<span style="color: black;">供给</span>完整的原始行号 retrace <span style="color: black;">方法</span>。</span><span style="color: black;">如图1-1所示,为两个<span style="color: black;">办法</span>的 DebugInfo 可视化映射过程,<span style="color: black;">咱们</span>会将指令集与原始行号的映射关系导出为 mapping 文件,并上传给服务端做后续的 retrace处理。<span style="color: black;">能够</span><span style="color: black;">发掘</span>,映射完成后两个<span style="color: black;">办法</span>的 DebugInfo 信息一致,即达到了可复用状态。</span><img src="https://mmbiz.qpic.cn/mmbiz_png/Ce6bSqXkduy40e47kThpibiaYUibvImzgDGG17ibqQgrpplMibL5sYuluuibB1ZnmQkOFqWLfTs8GqGN318RvxHTiaJUA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图1-1 两个<span style="color: black;">办法</span> DebugInfo 映射过程</span><span style="color: black;">接下来将<span style="color: black;">仔细</span>讲述 DebugInfo 分析、现有<span style="color: black;">方法</span>对比、百度APP优化<span style="color: black;">方法</span>及收益 </span><span style="color: black;">等内容。</span><span style="color: black;">GEEK TALK</span>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><strong style="color: blue;">02</strong></span></p><strong style="color: blue;">解构DebugInfo</strong><span style="color: black;">调试信息</span><span style="color: black;">(DebugInfo)</span><span style="color: black;">指的是应用于调试场景的字节码信息,<span style="color: black;">重点</span><span style="color: black;">包含</span>源文件名、行号、局部变量、扩展调试信息等。行号优化<span style="color: black;">便是</span>去优化 DebugInfo 中<span style="color: black;">包括</span>的行号信息,以减少 DebugInfo 区域<span style="color: black;">体积</span>,从而达到减少字节码文件体积的目的。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><strong style="color: blue;"><strong style="color: blue;"><strong style="color: blue;">丨</strong></strong></strong></span><span style="color: black;">2.1 Dex DebugInfo</span></h3><span style="color: black;">如图2-1所示,在Dex文件格式</span><span style="color: black;"></span><span style="color: black;">中,DebugInfo <span style="color: black;">处在</span> data 区域,由一系列debug_info_item <span style="color: black;">构成</span>。</span><img src="https://mmbiz.qpic.cn/mmbiz_png/Ce6bSqXkduy40e47kThpibiaYUibvImzgDGiaMpXQrhRA1o3Wo38eD2sKVKTuMZy5WwWibeWpCic8YibmcmPyd0j0lKog/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图2-1 Dex文件结构</span><span style="color: black;"><span style="color: black;">一般</span><span style="color: black;">状况</span>下,debug_info_item 与类<span style="color: black;">办法</span>一一对应,其在 Dex 中的引用关系如下图2-2所示。Dex 为块状结构,引用区域的位置均<span style="color: black;">经过</span> x_off 偏移量确定。</span><img src="https://mmbiz.qpic.cn/mmbiz_png/Ce6bSqXkduy40e47kThpibiaYUibvImzgDGDM8kjymQDUkBYmcxFbDu9KXdSWgtNL7DYukxEeE8nzwN5dcwicSnDcw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图2-2 class -> method -> debug_info引用关系</span><span style="color: black;">debug_info_item结构如图2-3所示,<span style="color: black;">重点</span>由两部分<span style="color: black;">形成</span>:header 和一系列debug_event。</span><span style="color: black;">header 中<span style="color: black;">包括</span><span style="color: black;">办法</span><span style="color: black;">初始</span>行号、<span style="color: black;">办法</span>参数数量、<span style="color: black;">办法</span>参数名三部分信息;除header 外的 debug_events <span style="color: black;">能够</span>理解为一系列状态寄存器,记录pc指针与行号的偏移量。debug_info_item 本质上是一个状态机。</span><img src="https://mmbiz.qpic.cn/mmbiz_png/Ce6bSqXkduy40e47kThpibiaYUibvImzgDGNdzIibvtRrD9takKxdTXcMWpM4QGhtOB01hXTQGtr6bHMCdo0PkX8bQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图2-3 debug_info_item 结构</span><span style="color: black;">常用的 de</span><span style="color: black;">bug_event 有以下几类:</span><strong style="color: blue;"><span style="color: black;">名字</span></strong><strong style="color: blue;"><span style="color: black;">value</span></strong><strong style="color: blue;"><span style="color: black;">参数</span></strong><strong style="color: blue;"><span style="color: black;">描述</span></strong><span style="color: black;">DBG_END_SEQUENCE</span><span style="color: black;">0x00</span><span style="color: black;"><span style="color: black;">没</span></span><span style="color: black;">debug_info_item状态结束标识,不可修改</span><span style="color: black;">DBG_ADVANCE_PC</span><span style="color: black;">0x01</span><span style="color: black;">pcDelta</span><span style="color: black;">仅<span style="color: black;">包括</span>pc偏移值</span><span style="color: black;">DBG_ADVANCE_LINE</span><span style="color: black;">0x02</span><span style="color: black;">lineDelta</span><span style="color: black;">仅<span style="color: black;">包括</span>line偏移值</span><span style="color: black;">Special Opcodes</span><span style="color: black;"></span><span style="color: black;"><span style="color: black;">没</span></span><span style="color: black;">可由value得到pc偏移值和line偏移值</span><span style="color: black;">Special Opcodes value 与 pcDelta & lineDelta 的换算公式如下:</span><span style="color: black;">DBG_FIRST_SPECIAL = 0x0a // the smallest special opcode</span><span style="color: black;">DBG_LINE_BASE = -4 // the smallest line number increment</span><span style="color: black;">DBG_LINE_RANGE = 15 // the number of line increments represented</span><span style="color: black;">adjusted_opcode = opcode - DBG_FIRST_SPECIAL</span><span style="color: black;">line += DBG_LINE_BASE + (adjusted_opcode % DBG_LINE_RANGE)</span><span style="color: black;">address += (adjusted_opcode / DBG_LINE_RANGE)</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><strong style="color: blue;"><strong style="color: blue;"><strong style="color: blue;">丨</strong></strong></strong></span><span style="color: black;">2.2 DebugInfo <span style="color: black;">运用</span>场景</span></h3><span style="color: black;">DebugInfo <span style="color: black;">平常</span>的<span style="color: black;">运用</span>场景是断点调试及堆栈定位</span><span style="color: black;">(<span style="color: black;">包含</span>崩溃、ANR、内存分析等所有可输出<span style="color: black;">办法</span>堆栈的场景)</span><span style="color: black;">。接下来以打印崩溃堆栈为例,系统<span style="color: black;">怎样</span><span style="color: black;">经过</span>解析DebugInfo 输出<span style="color: black;">反常</span>定位。</span><span style="color: black;">Throwable 对象初始化时会<span style="color: black;">首要</span>调用 nativeFillInStackTrace() <span style="color: black;">办法</span>获取当前线程中 StackTrace,而 StackTrace 中存储的是 ArtMethod</span><span style="color: black;">(ART虚拟机中<span style="color: black;">办法</span>对象)</span><span style="color: black;">和对应pc值,<span style="color: black;">无</span>行号信息;真正打印堆栈时,<span style="color: black;">经过</span>调用 nativeGetStackTrace <span style="color: black;">办法</span>将StackTrace 转化为 StackTraceElement[] ,StackTraceElement 会<span style="color: black;">包括</span><span style="color: black;">办法</span>所属源文件与<span style="color: black;">办法</span>行号。如图2-4所示,<span style="color: black;">反常</span>堆栈末尾会<span style="color: black;">表示</span><span style="color: black;">办法</span>源文件与行号。</span><img src="https://mmbiz.qpic.cn/mmbiz_png/Ce6bSqXkduy40e47kThpibiaYUibvImzgDGuZRMb1Ecg8LWj2fspSGwQdo35brbMy9y062DqJYuJkUoLKNVWMu8xg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图2-4 <span style="color: black;">反常</span>堆栈</span><span style="color: black;">StackTrace 转化为 StackTraceElement[] 的代码调用路径如下所示,即虚拟机将当前线程<span style="color: black;">办法</span>栈内容转化为图2-4中可读的堆栈信息的过程。</span><span style="color: black;"><span style="color: black;">// art/runtime/native/java_lang_Throwable.cc</span></span><span style="color: black;"><span style="color: black;"><span style="color: black;">static</span> jobjectArray <span style="color: black;">Throwable_nativeGetStackTrace</span><span style="color: black;">(JNIEnv* env, jclass, jobject javaStackState)</span> </span>{</span><span style="color: black;"> ...</span><span style="color: black;"> <span style="color: black;">ScopedFastNativeObjectAccess <span style="color: black;">soa</span><span style="color: black;">(env)</span></span>;</span><span style="color: black;"> <span style="color: black;">return</span>Thread::InternalStackTraceToStackTraceElementArray(soa, javaStackState);<span style="color: black;">// 将StackTrace转化为StackTraceElement[]</span></span><span style="color: black;">}</span><span style="color: black;"><span style="color: black;">// art/runtime/thread.cc</span></span><span style="color: black;">jobjectArray Thread::InternalStackTraceToStackTraceElementArray(</span><span style="color: black;"> <span style="color: black;">const</span>ScopedObjectAccessAlreadyRunnable& soa,</span><span style="color: black;"> jobject internal,</span><span style="color: black;"> jobjectArray output_array,</span><span style="color: black;"> <span style="color: black;">int</span>* stack_depth) {</span><span style="color: black;"> ...</span><span style="color: black;"> <span style="color: black;">// 遍历StackTrace</span></span><span style="color: black;"> <span style="color: black;">for</span> (<span style="color: black;">uint32_t</span> i = <span style="color: black;">0</span>; i < <span style="color: black;">static_cast</span><<span style="color: black;">uint32_t</span>>(depth); ++i) {</span><span style="color: black;">ObjPtr<mirror::ObjectArray<mirror::Object>> decoded_traces = soa.Decode<mirror::Object>(internal)->AsObjectArray<mirror::Object>();</span><span style="color: black;"> <span style="color: black;">const</span> ObjPtr<mirror::PointerArray> method_trace = ObjPtr<mirror::PointerArray>::DownCast(decoded_traces->Get(<span style="color: black;">0</span>));</span><span style="color: black;"> <span style="color: black;">// 从StackTrace中获取 ArtMethod与对应pc</span></span><span style="color: black;">ArtMethod* method = method_trace->GetElementPtrSize<ArtMethod*>(i, kRuntimePointerSize);</span><span style="color: black;"> <span style="color: black;">uint32_t</span> dex_pc = method_trace->GetElementPtrSize<<span style="color: black;">uint32_t</span>>(i + <span style="color: black;">static_cast</span><<span style="color: black;">uint32_t</span>>(method_trace->GetLength()) / <span style="color: black;">2</span>, kRuntimePointerSize);</span><span style="color: black;"> <span style="color: black;">// <span style="color: black;">按照</span> ArtMethod与对应pc 创建 StackTraceElement对象</span></span><span style="color: black;"> <span style="color: black;">const</span>ObjPtr<mirror::StackTraceElement> obj = CreateStackTraceElement(soa, method, dex_pc);</span><span style="color: black;"> soa.Decode<mirror::ObjectArray<mirror::StackTraceElement>>(result)->Set<<span style="color: black;">false</span>>(<span style="color: black;">static_cast</span><<span style="color: black;">int32_t</span>>(i), obj);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">return</span> result;</span><span style="color: black;">}</span><span style="color: black;"><span style="color: black;">static</span> ObjPtr<mirror::StackTraceElement> CreateStackTraceElement(</span><span style="color: black;"> <span style="color: black;">const</span> ScopedObjectAccessAlreadyRunnable& soa,</span><span style="color: black;">ArtMethod* method,</span><span style="color: black;"> <span style="color: black;">uint32_t</span> dex_pc) REQUIRES_SHARED(Locks::mutator_lock_) {</span><span style="color: black;"> ...</span><span style="color: black;"> <span style="color: black;">// 获取pc对应的代码行号</span></span><span style="color: black;"> <span style="color: black;">int32_t</span> line_number;</span><span style="color: black;"> line_number = method->GetLineNumFromDexPC(dex_pc);</span><span style="color: black;"> ...</span><span style="color: black;">}</span><span style="color: black;"><span style="color: black;">// ... art_method.h -> code_item_accessors.h</span></span><span style="color: black;"><span style="color: black;">// 当遍历debugInfo过程中,pc满足<span style="color: black;">要求</span>(大于等于StackTrace记录的pc)时,返回对应的行号</span></span><span style="color: black;"><span style="color: black;">inline</span> <span style="color: black;">bool</span>CodeItemDebugInfoAccessor::GetLineNumForPc(<span style="color: black;">const</span> <span style="color: black;">uint32_t</span> address,</span><span style="color: black;"> <span style="color: black;">uint32_t</span>* line_num) <span style="color: black;">const</span> {</span><span style="color: black;"> <span style="color: black;">return</span> DecodeDebugPositionInfo([&](<span style="color: black;">const</span> DexFile::PositionInfo& entry) {</span><span style="color: black;"> <span style="color: black;">if</span> (entry.address_ > address) {</span><span style="color: black;"> <span style="color: black;">return</span> <span style="color: black;">true</span>;</span><span style="color: black;"> }</span><span style="color: black;"> *line_num = entry.line_;</span><span style="color: black;"> <span style="color: black;">return</span> entry.address_ == address;</span><span style="color: black;"> });</span><span style="color: black;">}</span><span style="color: black;"><span style="color: black;">// code_item_accessors.h -> dex_file.h</span></span><span style="color: black;"><span style="color: black;">// 遍历dex中对应的debugInfo</span></span><span style="color: black;"><span style="color: black;">bool</span>DexFile::DecodeDebugPositionInfo(<span style="color: black;">const</span> <span style="color: black;">uint8_t</span>* stream,</span><span style="color: black;"> <span style="color: black;">const</span> IndexToStringData& index_to_string_data,</span><span style="color: black;"> <span style="color: black;">const</span> DexDebugNewPosition& position_functor) {</span><span style="color: black;"> PositionInfo entry;</span><span style="color: black;">entry.line_ = DecodeDebugInfoParameterNames(&stream, VoidFunctor());</span><span style="color: black;"> <span style="color: black;">for</span> (;;) {</span><span style="color: black;"> <span style="color: black;">uint8_t</span> opcode = *stream++;</span><span style="color: black;"> <span style="color: black;">switch</span> (opcode) {</span><span style="color: black;"> <span style="color: black;">case</span> DBG_END_SEQUENCE:</span><span style="color: black;"> <span style="color: black;">return</span> <span style="color: black;">true</span>; <span style="color: black;">// end of stream.</span></span><span style="color: black;"> <span style="color: black;">case</span> DBG_ADVANCE_PC:</span><span style="color: black;"> entry.address_ += DecodeUnsignedLeb128(&stream);</span><span style="color: black;"> <span style="color: black;">break</span>;</span><span style="color: black;"> <span style="color: black;">case</span> DBG_ADVANCE_LINE:</span><span style="color: black;">entry.line_ += DecodeSignedLeb128(&stream);</span><span style="color: black;"> <span style="color: black;">break</span>;</span><span style="color: black;"> ... </span><span style="color: black;"> <span style="color: black;">// 其他event类型处理,与局部变量、源文件<span style="color: black;">关联</span></span></span><span style="color: black;"> ... </span><span style="color: black;"> <span style="color: black;">default</span>: {</span><span style="color: black;"> <span style="color: black;">int</span> adjopcode = opcode - DBG_FIRST_SPECIAL;</span><span style="color: black;"> entry.address_ += adjopcode / DBG_LINE_RANGE;</span><span style="color: black;">entry.line_ += DBG_LINE_BASE + (adjopcode % DBG_LINE_RANGE);</span><span style="color: black;"> <span style="color: black;">break</span>;</span><span style="color: black;"> }</span><span style="color: black;"> }</span><span style="color: black;"> }</span><span style="color: black;">}</span><span style="color: black;">从上面的代码中 GetLineNumForPc <span style="color: black;">办法</span><span style="color: black;">能够</span>看出,虚拟机<span style="color: black;">经过</span>指针寻找原始行号时会遍历对应的 debugInfo。<span style="color: black;">因为</span><span style="color: black;">咱们</span>的<span style="color: black;">方法</span>中将 pcDelta <span style="color: black;">所有</span>统一为1,遍历长度会比原先长,但<span style="color: black;">因为</span>遍历中的处理极为简单,<span style="color: black;">因此</span>几乎不会<span style="color: black;">查找</span>性能<span style="color: black;">导致</span>影响。</span><span style="color: black;">GEEK TALK</span>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><strong style="color: blue;">03</strong></span></p><strong style="color: blue;">现有优化<span style="color: black;">方法</span></strong>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><strong style="color: blue;"><strong style="color: blue;"><strong style="color: blue;">丨</strong></strong></strong></span><span style="color: black;">3.1 极限优化<span style="color: black;">方法</span></span></h3><span style="color: black;">DebugInfo <span style="color: black;">做为</span>运行<span style="color: black;">没</span>关信息是<span style="color: black;">能够</span><span style="color: black;">所有</span>移除的。问题在于<span style="color: black;">倘若</span>直接移除 DebugInfo 的话,调试堆栈会<span style="color: black;">没</span>法<span style="color: black;">供给</span>准确的行号信息,图2-4 堆栈行号均会<span style="color: black;">表示</span>-1。<span style="color: black;">倘若</span>应用稳定性高、定位难度低,<span style="color: black;">能够</span><span style="color: black;">选取</span><span style="color: black;">所有</span>移除 DebugInfo。</span><span style="color: black;">Java编译器、代码缩减混淆<span style="color: black;">工具</span>都<span style="color: black;">供给</span>了相应的选项用于不生成<span style="color: black;">或</span>移除class字节码中的 DebugInfo。如图3-1所示,在 Class 字节码文件</span><span style="color: black;"></span><span style="color: black;">中,DebugInfo对应attributes区域中 SourceFile、SourceDebugExtension、LineNumberTable、LocalVariableTable 四项信息。</span><img src="https://mmbiz.qpic.cn/mmbiz_png/Ce6bSqXkduy40e47kThpibiaYUibvImzgDGz5MwFdgwNzJGIRNIbE3XwF9yl3HOrFPFbPzNysCLGXdMQm6Sics25HQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图3-1 Class文件结构</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">编译选项</span></strong></h3><span style="color: black;">-g:lines <span style="color: black;">// 生成LineNumberTable</span></span><span style="color: black;">-g:vars <span style="color: black;">// 生成LineVariableTable</span></span><span style="color: black;">-g:source <span style="color: black;">// 生成SourceFile</span></span><span style="color: black;">-g:<span style="color: black;">none</span> <span style="color: black;">// 不生成任何debugInfo</span></span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">Proguard规则</span></strong><span style="color: black;"></span></h3><span style="color: black;">-keepattributes SourceFile <span style="color: black;">// <span style="color: black;">保存</span>SourceFile</span></span><span style="color: black;">-keepattributes LineNumberTable <span style="color: black;">// <span style="color: black;">保存</span>LineNumberTable</span></span><span style="color: black;">除此之外,<span style="color: black;">亦</span><span style="color: black;">能够</span>在 transform <span style="color: black;">周期</span>利用字节码操作<span style="color: black;">工具</span>移除 DebugInfo。字节已开源的 ByteX 字节码<span style="color: black;">工具</span></span><span style="color: black;"></span><span style="color: black;">中即<span style="color: black;">运用</span>了这种<span style="color: black;">方法</span>。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><strong style="color: blue;"><strong style="color: blue;"><strong style="color: blue;">丨</strong></strong></strong></span><span style="color: black;">3.2 映射优化<span style="color: black;">方法</span></span></h3><span style="color: black;">在<span style="color: black;">实质</span><span style="color: black;">状况</span>中,应用会进行频繁地业务迭代与技术升级,高稳定性是<span style="color: black;">必须</span><span style="color: black;">连续</span><span style="color: black;">守护</span>的,<span style="color: black;">因此</span><span style="color: black;">咱们</span>不会直接移除 DebugInfo,<span style="color: black;">由于</span>那会使问题的定位成本变得<span style="color: black;">非常</span>高。</span><span style="color: black;">映射优化<span style="color: black;">方法</span>的基本<span style="color: black;">规律</span>是<span style="color: black;">保存</span> debugInfo 区域,但让 Dex 中 method 与 debug_info_item 的1对1关系变为N对1的复用关系,debug_info_item 数量减少了,体积自然会减少。<span style="color: black;">同期</span>导出 debug_info_item 复用前后的映射文件,可据此还原崩溃堆栈。下文中提到的支付宝、R8 和百度APP 的行号优化均<span style="color: black;">运用</span>了映射优化<span style="color: black;">方法</span>。</span><span style="color: black;">要做到 debug_info_item 复用,<span style="color: black;">咱们</span><span style="color: black;">首要</span><span style="color: black;">必须</span>确认 debug_info_item 的 equals 判断<span style="color: black;">规律</span>。若两个 debug_info_item 的<span style="color: black;">构成</span>部分均相同,则认为两者相等,<span style="color: black;">就可</span>复用。debug_info_item 的<span style="color: black;">构成</span>部分<span style="color: black;">包含</span><span style="color: black;">办法</span><span style="color: black;">初始</span>行号、<span style="color: black;">办法</span>参数、一系列 debug_events。<span style="color: black;">因为</span><span style="color: black;">咱们</span>关心的堆栈信息中不<span style="color: black;">包括</span><span style="color: black;">办法</span>参数,<span style="color: black;">那样</span><span style="color: black;">必须</span>统一的就<span style="color: black;">仅有</span><span style="color: black;">初始</span>行号和 debug_events。</span><span style="color: black;"><span style="color: black;">// debug_info_item 相等判断<span style="color: black;">规律</span>(伪代码)</span></span><span style="color: black;"><span style="color: black;">public</span>boolean equals(DebugInfoItem debugInfoItem) {</span><span style="color: black;"> <span style="color: black;">return</span> <span style="color: black;">this</span>.startLine == debugInfoItem.startLine</span><span style="color: black;"> && <span style="color: black;">this</span>.parameters.equals(debugInfoItem.parameters)</span><span style="color: black;"> && <span style="color: black;">this</span>.events.equals(debugInfoItem.events);</span><span style="color: black;">}</span><span style="color: black;">startLine 只是一个int值,赋值相同<span style="color: black;">就可</span>。</span><span style="color: black;">debug_events 的 equals <span style="color: black;">规律</span><span style="color: black;">亦</span>与其内容<span style="color: black;">关联</span>,即 events 数量以及<span style="color: black;">每一个</span> event 的类型与值。</span><span style="color: black;"><span style="color: black;">// debug_event 相等<span style="color: black;">规律</span>判断(伪代码)</span></span><span style="color: black;"><span style="color: black;"><span style="color: black;">public</span> boolean <span style="color: black;">equals</span>(<span style="color: black;">DebugEvent<span style="color: black;">event</span></span>)</span> {</span><span style="color: black;"> <span style="color: black;">return</span> <span style="color: black;">this</span>.type == <span style="color: black;">event</span>.type</span><span style="color: black;"> && <span style="color: black;">this</span>.<span style="color: black;">value</span> == <span style="color: black;">event</span>.<span style="color: black;">value</span>;</span><span style="color: black;">}</span><span style="color: black;">从<span style="color: black;">以上</span>的分析<span style="color: black;">能够</span><span style="color: black;">发掘</span>,想要达成 debug_info_item 复用,<span style="color: black;">必须</span><span style="color: black;">掌控</span>以下变量,使之尽可能保持相同:startLine、debug_events 数量、debug_event 类型、lineDelta、pcDelta</span><span style="color: black;">(opcode不算在内,<span style="color: black;">由于</span><span style="color: black;">能够</span>由lineDelta & pcDelta计算得到)</span><span style="color: black;">。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><strong style="color: blue;"><span style="color: black;">重载<span style="color: black;">办法</span>行号区间重叠问题</span></strong></span></h3><span style="color: black;">除了 startLine 外,其余四个变量取值是同步决定的,下文中会做<span style="color: black;">仔细</span>介绍。startLine <span style="color: black;">做为</span><span style="color: black;">办法</span><span style="color: black;">初始</span>行号,是 lineDelta 的累加基数,看似<span style="color: black;">能够</span>固定赋值,例如<span style="color: black;">所有</span><span style="color: black;">办法</span>都以1<span style="color: black;">做为</span><span style="color: black;">初始</span>映射行号。</span><span style="color: black;">但遇到重载<span style="color: black;">办法</span>时,<span style="color: black;">倘若</span>两个<span style="color: black;">办法</span>的映射后行号区间有重叠,<span style="color: black;">咱们</span>会<span style="color: black;">没</span>法确定映射后的行号应该还原至哪个<span style="color: black;">办法</span>。<span style="color: black;">原由</span>在于虚拟机解析出的堆栈中仅<span style="color: black;">运用</span><span style="color: black;">办法</span>名<span style="color: black;">做为</span><span style="color: black;">办法</span><span style="color: black;">独一</span>标识,而非<span style="color: black;">咱们</span><span style="color: black;">一般</span>认识的<span style="color: black;">办法</span>重载中 [<span style="color: black;">办法</span>名,参数类型,参数个数] 三者结合<span style="color: black;">做为</span>方法<span style="color: black;">独一</span>标识。</span><span style="color: black;">举例如下:</span><span style="color: black;"><span style="color: black;">// <span style="color: black;">办法</span>行号映射为:</span></span><span style="color: black;">com.example.myapplication.MethodOverloadSample.test():</span><span style="color: black;"> <span style="color: black;">1</span>-><span style="color: black;">21</span></span><span style="color: black;"> <span style="color: black;">2</span>-><span style="color: black;">22</span></span><span style="color: black;">com.example.myapplication.MethodOverloadSample.test(<span style="color: black;">String</span> msg):</span><span style="color: black;"> <span style="color: black;">1</span>-><span style="color: black;">34</span></span><span style="color: black;"> <span style="color: black;">2</span>-><span style="color: black;">35</span></span><span style="color: black;">...</span><span style="color: black;"><span style="color: black;">// 收集映射后<span style="color: black;">办法</span>堆栈:</span></span><span style="color: black;">...</span><span style="color: black;">at com.example.myapplication.MethodOverloadSample.test(MethodOverloadSample.java:<span style="color: black;">2</span>)</span><span style="color: black;">...</span><span style="color: black;"><span style="color: black;">因为</span>堆栈中仅<span style="color: black;">包括</span><span style="color: black;">办法</span>名,<span style="color: black;">咱们</span><span style="color: black;">没</span>法确定应该映射到行号22还是行号35。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><strong style="color: blue;"><strong style="color: blue;"><strong style="color: blue;">丨</strong></strong></strong></span><span style="color: black;">3.3 支付宝行号优化<span style="color: black;">方法</span></span></h3><span style="color: black;">支付宝介绍了两种行号优化<span style="color: black;">方法</span>。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;"><span style="color: black;">方法</span>一</span></strong></h3><span style="color: black;">(1)编译时将 debugInfo <span style="color: black;">所有</span>摘出来<span style="color: black;">做为</span> <span style="color: black;">debugI</span><span style="color: black;">nfo</span>.dex,APK中<span style="color: black;">再也不</span><span style="color: black;">包括</span> <span style="color: black;">debugI</span><span style="color: black;">nfo</span>。</span><span style="color: black;">(2)<span style="color: black;">反常</span><span style="color: black;">出现</span>时,<span style="color: black;">经过</span> hook Throwable,从其持有的 StackTrace 对象中解析得到的指令集行号并上传。</span><span style="color: black;">(3)性能平台结合<span style="color: black;">过程</span>1中的<span style="color: black;">debugI</span><span style="color: black;">nfo</span>.dex,将指令集行号转化为原始行号。</span><span style="color: black;">该<span style="color: black;">方法</span>原理是离线还原章节2.2中的流程,问题在于仅<span style="color: black;">运用</span> Throwable 场景,且<span style="color: black;">因为</span><span style="color: black;">区别</span>版本 JVM 的 StackTrace 对象结构<span style="color: black;">区别</span>,适配成本比较高。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;"><span style="color: black;">方法</span>二</span></strong></h3><span style="color: black;"><span style="color: black;">保存</span> N 个 debug_info_item,<span style="color: black;">同期</span>将其修改为<span style="color: black;">办法</span>指令集,即<span style="color: black;">经过</span> debug_info_item 获取到的 lineNumber 实质上<span style="color: black;">指的是</span>令行号,而非代码行号。</span><span style="color: black;">这种<span style="color: black;">方法</span>下变量 lineDelta == pcDelta,取值始终为1,由此 debug_event <span style="color: black;">亦</span>就确定是 specail opcodes 类型;<span style="color: black;">每一个</span> debug_info_item 中 debug_event 的数量<span style="color: black;">亦</span><span style="color: black;">能够</span><span style="color: black;">按照</span><span style="color: black;">实质</span><span style="color: black;">状况</span>人为设定,能够覆盖应用<span style="color: black;">办法</span>的指令数量<span style="color: black;">就可</span>。至此所有的变量都有了固定赋值,即 debug_info_item 做到了<span style="color: black;">办法</span>复用。</span><span style="color: black;">百度APP 与支付宝APP 的行号优化<span style="color: black;">方法</span>在整体的行号复用策略上是类似的,都是<span style="color: black;">经过</span>让<span style="color: black;">更加多</span>的<span style="color: black;">办法</span>复用同一个 debug_info_item 来达到节省包体积的效果。百度App 行号优化<span style="color: black;">方法</span>在实现重载<span style="color: black;">办法</span>、R8 行号优化等行号完全可还原方面进行了更细化的<span style="color: black;">思虑</span>和设计。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><strong style="color: blue;"><strong style="color: blue;"><strong style="color: blue;">丨</strong></strong></strong></span><span style="color: black;">3.4 R8行号优化<span style="color: black;">方法</span></span><strong style="color: blue;"><span style="color: black;"></span></strong></h3><span style="color: black;">声明了</span><span style="color: black;"> -keepattributes LineNumberTable</span><span style="color: black;">后,R8不会移除行号信息,转而启用行号优化。其对 debug_info_item 的修改<span style="color: black;">包含</span>两处:</span><strong style="color: blue;"><span style="color: black;">startLine</span></strong><span style="color: black;">:startLine 默认为1。当遇到同名<span style="color: black;">办法</span>时,后一个<span style="color: black;">办法</span>的 startLine 为前一个<span style="color: black;">办法</span>优化后 endLine+1。<span style="color: black;">原由</span>如章节3.2 中<span style="color: black;">说到</span>的同名<span style="color: black;">办法</span>行号 retrace 问题。</span><strong style="color: blue;"><span style="color: black;">lineDelta</span></strong><span style="color: black;">:lineDelta 默认为1。</span><span style="color: black;"><span style="color: black;">这般</span>修改后,一部分 debug_info_item 可复用,但<span style="color: black;">因为</span> debug_events 数量以及 pcDelta 仍不可控,复用程度<span style="color: black;">非常</span>有限。</span><span style="color: black;">R8 的行号优化映射结果如图3-2所示,其中一个<span style="color: black;">办法</span>可能对应一个或多个行号区间映射,其<span style="color: black;">原由</span>在于 lineDelta 强制为1,<span style="color: black;">因此</span>映射前后的行号区间 Delta <span style="color: black;">必要</span>保持一致。</span><img src="https://mmbiz.qpic.cn/mmbiz_png/Ce6bSqXkduy40e47kThpibiaYUibvImzgDG7CfW13yn4DZkhd2ickjwGgum3Vk2b37pZPAlZ8M34qLicsT5hnqFFyqg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图3-2 R8 行号映射</span><span style="color: black;">除此之外,R8还利用 SourceDebugExtension 还原了 kotlin inline <span style="color: black;">办法</span>的<span style="color: black;">实质</span>位置,如图3-3所示</span><img src="https://mmbiz.qpic.cn/mmbiz_png/Ce6bSqXkduy40e47kThpibiaYUibvImzgDG0WlHsvpK0QeyXEYL6icaHqaSJddZQfKy3qwicH06tUwYdKOnXqTZ6X9Q/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图3-3 R8还原 kotlin inline 行号映射</span><span style="color: black;">GEEK TALK</span>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><strong style="color: blue;">04</strong></span></p><strong style="color: blue;">百度APP Dex行号优化<span style="color: black;">方法</span></strong><span style="color: black;">百度APP的行号优化对 startLine、pcDelta、lineDelta、debug_event 数量均进行了<span style="color: black;">掌控</span>,<span style="color: black;">最后</span> debug_info_item 复用比例得到了<span style="color: black;">极重</span><span style="color: black;">提高</span>。<span style="color: black;">同期</span>百度APP 联合内部性能平台,对线上收集到的崩溃、ANR 堆栈进行行号还原。流程如图4-1所示:</span><img src="https://mmbiz.qpic.cn/mmbiz_png/Ce6bSqXkduy40e47kThpibiaYUibvImzgDGICQNkSpbC3RNpKwWGExJPzDkvEgCrBAXib7Km0g5fAgQicLCy2bMfpuw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图4-1 百度APP端到端的行号优化流程</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><strong style="color: blue;"><strong style="color: blue;"><strong style="color: blue;">丨</strong></strong></strong></span><span style="color: black;">4.1客户端行号优化</span></h3>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">debug_info_item 变量<span style="color: black;">掌控</span></span></strong></h3><span style="color: black;">(1)startLine</span><span style="color: black;"> 默认值为100000。与R8默认值为1<span style="color: black;">区别</span>,选这么大的初始值是为了避免热修复、插件中存在同名<span style="color: black;">办法</span>时<span style="color: black;">显现</span>行号重叠,<span style="color: black;">导致</span>行号还原失败。</span><span style="color: black;">理想的行号区间分布如下图所示。每成功分配一个行号区间后,<span style="color: black;">咱们</span>会立即初始化下一个行号区间的 next_startLine = ((this_startLine + this_inst_size) / default_gap + 1) * default_gap。</span><span style="color: black;">当<span style="color: black;">显现</span>同名<span style="color: black;">办法</span>时,<span style="color: black;">咱们</span>会就现有的行号区间进行比对,next_startLine <span style="color: black;">是不是</span>符合<span style="color: black;">需求</span>,<span style="color: black;">倘若</span>不符合还<span style="color: black;">必须</span>在叠加 default_gap(默认值为5000)。</span><img src="data:image/svg+xml,%3C%3Fxml version=1.0 encoding=UTF-8%3F%3E%3Csvg width=1px height=1px viewBox=0 0 1 1 version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=none stroke-width=1 fill=none fill-rule=evenodd fill-opacity=0%3E%3Cg transform=translate(-249.000000, -126.000000) fill=%23FFFFFF%3E%3Crect x=249 y=126 width=1 height=1%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图4-2 理想行号区间</span><span style="color: black;">(2)debug_event</span><span style="color: black;">除了<span style="color: black;">暗示</span><span style="color: black;">初始</span>结束的 debug_event 外,剩余<span style="color: black;">所有</span>都是 pcDelta=lineDelta=1的 special opcodes 类型。其中 debug_event 数量<span style="color: black;">按照</span><span style="color: black;">办法</span>指令数量而定,取值为所属指令分区间的上限值。</span><img src="data:image/svg+xml,%3C%3Fxml version=1.0 encoding=UTF-8%3F%3E%3Csvg width=1px height=1px viewBox=0 0 1 1 version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=none stroke-width=1 fill=none fill-rule=evenodd fill-opacity=0%3E%3Cg transform=translate(-249.000000, -126.000000) fill=%23FFFFFF%3E%3Crect x=249 y=126 width=1 height=1%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图4-3 指令数量区间与 debug_event 数量映射</span><img src="data:image/svg+xml,%3C%3Fxml version=1.0 encoding=UTF-8%3F%3E%3Csvg width=1px height=1px viewBox=0 0 1 1 version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=none stroke-width=1 fill=none fill-rule=evenodd fill-opacity=0%3E%3Cg transform=translate(-249.000000, -126.000000) fill=%23FFFFFF%3E%3Crect x=249 y=126 width=1 height=1%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图4-4 映射后的 debug_events</span><span style="color: black;">(3)pcDelta</span><span style="color: black;"> 首个 special opcodes 为0,其余为1。</span><span style="color: black;">(4)lineDelta</span><span style="color: black;">默认与 pcDelta 一致。即<span style="color: black;">经过</span> debugInfo 获取代码行号,<span style="color: black;">实质</span>拿到的是映射后的指令行号。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">行号映射</span></strong></h3><span style="color: black;">生成的行号映射表格式如下所示:</span><span style="color: black;"><span style="color: black;">类名1:</span></span><span style="color: black;"> <span style="color: black;">办法</span>描述符1:</span><span style="color: black;"> 映射后行号闭区间1 -> 原行号1</span><span style="color: black;"> 映射后行号闭区间2 -> 原行号2</span><span style="color: black;"> <span style="color: black;">办法</span>描述符1:</span><span style="color: black;"> 映射后行号闭区间1 -> 原行号1</span><span style="color: black;"> 映射后行号闭区间2 -> 原行号2</span><span style="color: black;"><span style="color: black;">类名2:</span></span><span style="color: black;"> ...</span><span style="color: black;">行号映射<span style="color: black;">暗示</span>例如下:</span><span style="color: black;">com.baidu.searchbox.Application:</span><span style="color: black;"> <span style="color: black;">void</span> onCreate(android.os.Bundle):</span><span style="color: black;"> [<span style="color: black;">1000</span><span style="color: black;">-1050</span>] -> <span style="color: black;">20</span></span><span style="color: black;"> [<span style="color: black;">1051</span><span style="color: black;">-2000</span>] -> <span style="color: black;">22</span></span><span style="color: black;"> <span style="color: black;">void</span> onCreate():</span><span style="color: black;"> [<span style="color: black;">3000</span><span style="color: black;">-3020</span>] -> <span style="color: black;">30</span></span><span style="color: black;"> [<span style="color: black;">3021</span><span style="color: black;">-3033</span>] -> <span style="color: black;">31</span></span><span style="color: black;"> <span style="color: black;">void</span> onStop():</span><span style="color: black;"> [<span style="color: black;">1000</span><span style="color: black;">-1050</span>] -> <span style="color: black;">50</span></span><span style="color: black;"> [<span style="color: black;">1051</span><span style="color: black;">-2000</span>] -> <span style="color: black;">55</span></span><span style="color: black;">com.baidu.searchbox.MainActivity:</span><span style="color: black;"> <span style="color: black;">void</span> onResume():</span><span style="color: black;"> [<span style="color: black;">1000</span><span style="color: black;">-1050</span>] -> <span style="color: black;">100</span></span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">兼容 R8 行号优化</span></strong></h3><span style="color: black;">R8 对行号信息的处理有三种<span style="color: black;">状况</span>:移除、优化、<span style="color: black;">保存</span>。处理<span style="color: black;">要求</span>如图4-5 所示。</span><img src="data:image/svg+xml,%3C%3Fxml version=1.0 encoding=UTF-8%3F%3E%3Csvg width=1px height=1px viewBox=0 0 1 1 version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=none stroke-width=1 fill=none fill-rule=evenodd fill-opacity=0%3E%3Cg transform=translate(-249.000000, -126.000000) fill=%23FFFFFF%3E%3Crect x=249 y=126 width=1 height=1%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图4-5 R8 处理行号<span style="color: black;">规律</span></span><span style="color: black;">其中 debug mode 参数由 AGP <span style="color: black;">掌控</span>传入,<span style="color: black;">日前</span><span style="color: black;">相关</span>参数是 buildType.isDebuggable。<span style="color: black;">不外</span>编译线上 release 包时是不会开启 isDebuggable 的,<span style="color: black;">因此</span>工程在启用了 R8 的<span style="color: black;">状况</span>下<span style="color: black;">仅有</span>行号移除与优化两种结果。</span><span style="color: black;">此时<span style="color: black;">咱们</span>的行号优化<span style="color: black;">工具</span>处理的对象<span style="color: black;">便是</span>R8<span style="color: black;">已然</span>映射过一次的行号了。<span style="color: black;">这儿</span>的兼容做法有两种:</span><span style="color: black;">(1)hook R8 任务,对R8 行号<span style="color: black;">保存</span>做自定义修改。这种<span style="color: black;">办法</span>工作量会比<span style="color: black;">很强</span>。</span><span style="color: black;">(2)针对R8 的映射做 retrace。流程<span style="color: black;">能够</span>是</span><span style="color: black;">(客户端) -> [百度APP行号retrace -> R8行号retrace](服务端)</span><span style="color: black;">,<span style="color: black;">亦</span><span style="color: black;">能够</span>是</span><span style="color: black;">(客户端) -> [百度APP行号retrace](服务端)</span><span style="color: black;">。<span style="color: black;">咱们</span><span style="color: black;">日前</span>采用的是后者。</span><span style="color: black;">R8 行号映射内容与混淆一同输出在 mapping.txt 中,<span style="color: black;">详细</span>参考3.4章节。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;"><span style="color: black;">工具</span><span style="color: black;">运用</span></span></strong></h3><span style="color: black;"><span style="color: black;">最后</span>行号优化<span style="color: black;">工具</span>以 gradle 插件形式接入工程,行号优化任务依托于 packageApplication 任务之前执行,处理对象为 minify 任务输出的 Dex 文件,并将优化后的 Dex 文件<span style="color: black;">做为</span> packageApplication 任务输入。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">体积优化效果</span></strong></h3><span style="color: black;">百度APP 上线行号优化前,APK体积为 123.58M,其中dex体积为 37.42M;启用行号优化后,</span><span style="color: black;">APK体积减小至120.54M,<strong style="color: blue;">优化3.04M</strong>,占dex体积~8%</span><span style="color: black;">。为了满足多个<span style="color: black;">途径</span>包共用一个行号映射文件的需求,<span style="color: black;">咱们</span><span style="color: black;">期盼</span>类内映射行号尽可能保持不变,<span style="color: black;">因此</span><span style="color: black;">选取</span>了类级别的行号区间分配。<span style="color: black;">倘若</span>在 Dex 级别进行行号区间分配,可优化<span style="color: black;">更加多</span>体积,实验<span style="color: black;">显示</span></span><span style="color: black;">可进一步优化400K</span><span style="color: black;">。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><strong style="color: blue;"><strong style="color: blue;"><strong style="color: blue;">丨</strong></strong></strong></span><span style="color: black;">4.2 性能平台行号映射还原</span></h3><span style="color: black;">百度APP 上线行号优化后,端上报的<span style="color: black;">反常</span>信息中<span style="color: black;">再也不</span>携带真正的行号,携带的行号为虚拟行号,虚拟行号并<span style="color: black;">不可</span>真正映射到<span style="color: black;">反常</span><span style="color: black;">出现</span>时<span style="color: black;">实质</span>代码所在行,给业务方排查线上问题带来了很大麻烦。<span style="color: black;">因此呢</span>性能平台<span style="color: black;">必须</span>将虚拟行号进行映射解析。将端上上报的崩溃、卡顿等<span style="color: black;">反常</span>信息中的虚拟行号<span style="color: black;">经过</span><span style="color: black;">必定</span>的解析算法 + APP发版时传入性能平台的行号映射表,<span style="color: black;">最后</span>映射成真实的行号,使的该行号能够真正映射到<span style="color: black;">反常</span><span style="color: black;">出现</span>时<span style="color: black;">实质</span>代码所在行,<span style="color: black;">最后</span><span style="color: black;">提高</span>业务方在性能平台上分析问题的能力。</span><span style="color: black;">在APP应用中,尽管<span style="color: black;">出现</span>崩溃、卡顿等<span style="color: black;">反常</span>场景的概率很低,<span style="color: black;">然则</span>在日活过亿的用户级别下,产生的<span style="color: black;">反常</span>信息<span style="color: black;">亦</span>是千万、亿级别的,<span style="color: black;">怎样</span>对全量<span style="color: black;">反常</span>信息进行实时行号映射解析是性能平台面临的首要问题。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">性能平台整体架构图</span></strong></h3><span style="color: black;">性能平台采取如下架构对全量用户产生的<span style="color: black;">反常</span>信息的行号进行映射解析,设计<span style="color: black;">重点</span>分位三个部分:流式计算处理服务、多级缓存系统、映射文件解析服务。整体的架构图如下所示:</span><img src="data:image/svg+xml,%3C%3Fxml version=1.0 encoding=UTF-8%3F%3E%3Csvg width=1px height=1px viewBox=0 0 1 1 version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=none stroke-width=1 fill=none fill-rule=evenodd fill-opacity=0%3E%3Cg transform=translate(-249.000000, -126.000000) fill=%23FFFFFF%3E%3Crect x=249 y=126 width=1 height=1%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图4-6 性能平台服务端整体架构图</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">映射文件解析服务</span></strong></h3><span style="color: black;">在进行行号映射解析的过程中,<span style="color: black;">必须</span>原始<span style="color: black;">反常</span>信息 + 行映射解析文件 + 解析算法 ->真正行号。<span style="color: black;">因此呢</span>,在APP发版时,<span style="color: black;">必须</span>采用手动</span><span style="color: black;">(性能平台上传)</span><span style="color: black;"><span style="color: black;">或</span>自动</span><span style="color: black;">(发版流水线配置)</span><span style="color: black;">的方式将行映射解析文件上传到性能平台的解析服务器中,<span style="color: black;">经过</span>映射解析服务器将数据写入到多级缓存系统中,供流式计算引擎<span style="color: black;">运用</span>。例如,原始的映射文件如图4-7,其中包含了包名、类名、<span style="color: black;">办法</span>名、映射行号闭区间、真实行号等信息。</span><img src="data:image/svg+xml,%3C%3Fxml version=1.0 encoding=UTF-8%3F%3E%3Csvg width=1px height=1px viewBox=0 0 1 1 version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=none stroke-width=1 fill=none fill-rule=evenodd fill-opacity=0%3E%3Cg transform=translate(-249.000000, -126.000000) fill=%23FFFFFF%3E%3Crect x=249 y=126 width=1 height=1%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E" style="width: 50%; margin-bottom: 20px;"><span style="color: black;">图4-7 映射文件示例</span><span style="color: black;">性能平台在将这些信息写入缓存系统时的结构(key-value)HashMap为:</span><span style="color: black;"><span style="color: black;">APP_</span>版本_<span style="color: black;">com</span><span style="color: black;">.baidu</span><span style="color: black;">.searchbox</span><span style="color: black;">.Application</span><span style="color: black;">.onCreate</span>:</span><span style="color: black;"> <span style="color: black;"></span> <span style="color: black;">-</span>> 20</span><span style="color: black;"> <span style="color: black;"></span> <span style="color: black;">-</span>> 22</span><span style="color: black;"> <span style="color: black;"></span> <span style="color: black;">-</span>> 30</span><span style="color: black;"> <span style="color: black;"></span> <span style="color: black;">-</span>> 31</span><strong style="color: blue;"><span style="color: black;">流式计算处理服务</span></strong><span style="color: black;">该部分的流程为,端上采集<span style="color: black;">反常</span>信息 -> 上报到日志中台 -> 性能平台数据汇总Bigpipe -> 性能平台<span style="color: black;">根据</span>业务分流 -> 各个子业务的Bigpipe -> 流式计算引擎进行行号解析等处理 -> 数据存储 -> 性能平台进行展示。</span><span style="color: black;">流式计算引擎进行行号解析时,会将<span style="color: black;">拜访</span>频率最热的映射文件行号的Map结构加载到算子内存中。若内存中<span style="color: black;">没</span>法命中,则去多级缓存中去<span style="color: black;">查找</span>再加载到算子的内存中。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">多级缓存系统</span></strong></h3><span style="color: black;"><span style="color: black;">针对</span><span style="color: black;">查找</span>的响应速度,数据在流式计算算子的内存中的读写速度 > Redis 等内存存储系统>列式存储系统 Table。多级缓存系统的由算子内存、Redis、Table等构建。最上层是实时流算子内存,响应速度最快,但容量受到限制,用来缓存<span style="color: black;">拜访</span>频率最高的映射文件索引,中间层是 Redis,<span style="color: black;">重点</span>存储线上的映射文件,最底层则为 Table,存储的是线上和线下场景的映射文件。<span style="color: black;">针对</span><span style="color: black;">咱们</span><span style="color: black;">全部</span>系统<span style="color: black;">来讲</span>,流式引擎算子内存中的缓存命中率高是<span style="color: black;">咱们</span><span style="color: black;">提高</span>行映射解析时效性<span style="color: black;">要紧</span><span style="color: black;">保准</span>。<span style="color: black;">因此呢</span><span style="color: black;">咱们</span>设计了如下的缓存替换策略:</span><span style="color: black;">(1)缓存具备高并发能力,能够并行的互不干扰的读写;</span><span style="color: black;">(2)缓存具备老化能力,当一个数据版本N天未被命中时,缓存将其老化清除;</span><span style="color: black;">(3)数据具备W-TinyLFU的替换策略,使得内存中的缓存为<span style="color: black;">近期</span>最频繁<span style="color: black;">拜访</span>的Key值。</span>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><strong style="color: blue;"><span style="color: black;">设计和实现中关键问题的<span style="color: black;">处理</span></span></strong></h3><span style="color: black;">(1) 数据的幂等性</span><span style="color: black;">在分布式的流式处理系统中,实时处理系统<span style="color: black;">常常</span><span style="color: black;">亦</span>会面临崩溃,重启的<span style="color: black;">状况</span>,<span style="color: black;">因此呢</span><span style="color: black;">需求</span>系统对数据的处理<span style="color: black;">拥有</span>幂等性,即精确消费一次数据的语义。在系统中,<span style="color: black;">咱们</span><span style="color: black;">经过</span>实时计算引擎中的Checkpoint机制,<span style="color: black;">保准</span>数据的消费<span style="color: black;">最少</span>一次消费。<span style="color: black;">而后</span>在存储中,<span style="color: black;">经过</span>对数据的日志ID<span style="color: black;">做为</span>数据的<span style="color: black;">独一</span>标识,即一条<span style="color: black;">反常</span>信息数据即使多次消费<span style="color: black;">亦</span>只会存储一次。<span style="color: black;">保准</span>了<span style="color: black;">全部</span>系统的幂等性<span style="color: black;">需求</span>。</span><span style="color: black;">(2) 数据的流量压力<span style="color: black;">掌控</span></span><span style="color: black;">在整体的设计中,数据的处理和数据的采集<span style="color: black;">经过</span>了中间件<span style="color: black;">信息</span>队列进行<span style="color: black;">认识</span>耦和削峰,当数据<span style="color: black;">处在</span>高峰期时,此时未能消费完的数据会<span style="color: black;">保留</span>在<span style="color: black;">信息</span>中间件的磁盘上。流量高峰的时间段都是较短的,待流量高峰期结束,数据处理模块又能将中间件中累积的数据处理完从而做到较好的压力<span style="color: black;">掌控</span>。</span><span style="color: black;">(3) 数据处理的低延时</span><span style="color: black;">采用多级缓存系统的设计,<span style="color: black;">保准</span>了每条数据的行解析映射在ms级别,使的系统的<span style="color: black;">反常</span>端上上报产生->性能平台展示解析结果的<span style="color: black;">全部</span>流程<span style="color: black;">保准</span>在了分钟级级别。</span><span style="color: black;">GEEK TALK</span>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><strong style="color: blue;">05</strong></span></p><strong style="color: blue;">总结</strong><span style="color: black;">本文<span style="color: black;">重点</span>介绍了 DebugInfo 的定位以及优化<span style="color: black;">方法</span>,其中重点讲述了<span style="color: black;">日前</span>百度APP所<span style="color: black;">运用</span>的Dex行号优化与复原<span style="color: black;">方法</span>。感谢各位阅读至此,如有问题请不吝指正。</span>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><strong style="color: blue;"> END</strong></span></p><span style="color: black;"><span style="color: black;">参考资料:</span></span><span style="color: black;"> 支付宝行号优化</span><span style="color: black;">https://juejin.cn/post/6844903712201277448</span><span style="color: black;"> Dex结构</span><span style="color: black;">https://source.android.com/devices/tech/dalvik/dex-format</span><span style="color: black;"> Class结构 </span><span style="color: black;">https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.1</span><span style="color: black;"> ProGuard规则</span><span style="color: black;">https://www.guardsquare.com/manual/configuration/attributes</span><span style="color: black;"> ByteX </span><span style="color: black;">https://github.com/bytedance/ByteX</span><span style="color: black;"> R8 </span><span style="color: black;">https://r8.googlesource.com/r8</span><span style="color: black;"><span style="color: black;">举荐</span>阅读:</span>
<h1 style="color: black; text-align: left; margin-bottom: 10px;"><a style="color: black;"><span style="color: black;">百度APP Android包体积优化实践(一)</span></a>总览</h1>
<h1 style="color: black; text-align: left; margin-bottom: 10px;"><a style="color: black;"><span style="color: black;">百度APP iOS端内存优化实践-大块内存监控<span style="color: black;">方法</span></span></a></h1>
<h1 style="color: black; text-align: left; margin-bottom: 10px;"><a style="color: black;"><span style="color: black;">百家号基于AE的视频渲染技术探索</span></a></h1>
<h1 style="color: black; text-align: left; margin-bottom: 10px;"><a style="color: black;"><span style="color: black;">百度工程师教你玩转设计模式(观察者模式)</span></a></h1>
<h1 style="color: black; text-align: left; margin-bottom: 10px;"><a style="color: black;"><span style="color: black;">Linux透明大页机制在云上大规模集群实践介绍</span></a></h1>
<h1 style="color: black; text-align: left; margin-bottom: 10px;"><a style="color: black;"><span style="color: black;">超<span style="color: black;">有效</span>!Swagger-Yapi的<span style="color: black;">奥密</span></span></a></h1><img src="data:image/svg+xml,%3C%3Fxml version=1.0 encoding=UTF-8%3F%3E%3Csvg width=1px height=1px viewBox=0 0 1 1 version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=none stroke-width=1 fill=none fill-rule=evenodd fill-opacity=0%3E%3Cg transform=translate(-249.000000, -126.000000) fill=%23FFFFFF%3E%3Crect x=249 y=126 width=1 height=1%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E" style="width: 50%; margin-bottom: 20px;"><span style="color: black;"><span style="color: black;">一键三连,好运连连,bug不见</span><span style="color: black;"> 楼主节操掉了,还不快捡起来!
页:
[1]