写点什么

为了 KPI,对 APK 进行极限优化!,2021 年 Android 春招面试经历

用户头像
Android架构
关注
发布于: 9 小时前

16:17:void?log()?->?b


20:21:void?log1()?->?c


这里为什么要把类名和方法名用杂乱无章的字符代替呢?一方面原因就是后者?更精简,在 dex 文件中占用的空间更小,但这不是今天讨论的重点。上面 Crash 信息的后面指明了 Crash 的具体位置是第 26 行,这是不是说明了 dex 存在一些信息,指明了字节码位置到源代码位置的信息,进一步,我们是不是可以参考上面混淆之后通过 map 映射找到原来的类的做法,把字节码位置到源代码位置的信息作为 map 存在本地!!


这里需要指出 Dex 文件存在一个 debugItemInfo 的区域,记录了指令集位置到行号的信息,正是因为这些信息的存在,我们才能做单步调试等操作,这也是这个命名的由来。


以应用宝为例,130 万行代码都要保存这个映射信息的话其实占用的空间是很大的(也就是上面优化掉的那部分)。


其实,优化掉这部分信息已经有一些工具支持了:


  • Proguard 工具开启优化后默认不保留这个信息,除非设置了?-keepattributes LineNumberTable

  • facebook 的?redex[1]?也有类似功能,配置 drop_line_number

  • 最近字节跳动开源了一个 ByteX[2],也有类似能力,配置?deleteLineNumber

  • 蚂蚁金服的支付宝 App 构建优化解析:Android 包大小极致压缩[3]也直接提到了这种做法,但问题也很明显、很严重,会丢失行号信息(低版本都是-1,高版本是指令集位置),导致 Crash 无法排查,此外,每个版本也需要做兼容,但是该文章并未详细描述,本文正是填补这部分空白。


##? ?实现


首先,Java 层的 Crash 上报都是通过自定义 Thread.setDefaultUncaughtExceptionHandler 的 uncaughtException(Thread thread, Throwable throwable)?方法实现的,下面以 Android 4.4[4]的源码为例,分析下底层原理。


Throwable 的每个构建函数都有一个 fillInStackTrace();调用,具体逻辑如下:


/**


*?Records?the?stack?trace?from?the?point?where?this?method?has?been?called


*?to?this?{@code?Throwable}.?This?method?is?invoked?by?the?{@code?Throwable}?constructors.



    *?<p>This?method?is?public?so?that?code?(such?as?an?RPC?system)?which?catches


    *?a?{@code?Throwable}?and?then?re-throws?it?can?replace?the?construction-time?stack?trace


    *?with?a?stack?trace?from?the?location?where?the?exception?was?re-thrown,?by?<i>calling</i>


    *?{@code?fillInStackTrace}.



      *?<p>This?method?is?non-final?so?that?non-Java?language?implementations?can?disable?VM?stack


      *?traces?for?their?language.?Filling?in?the?stack?trace?is?relatively?expensive.


      *?<i>Overriding</i>?this?method?in?the?root?of?a?language's?exception?hierarchy?allows?the


      *?language?to?avoid?paying?for?something?it?doesn't?need.



        *?@return?this?{@code?Throwable}?instance.


        */


        public?Throwable?fillInStackTrace()?{


        if?(stackTrace?==?null)?{


        return?this;?//?writableStackTrace?was?false.


        }


        //?Fill?in?the?intermediate?representation.


        stackState?=?nativeFillInStackTrace();


        //?Mark?the?full?representation?as?in?need?of?update.


        stackTrace?=?EmptyArray.STACK_TRACE_ELEMENT;


        return?this;


        }


        其中,stackState 就包含了指令集位置信息(the intermediate representation),该对象会被传递给下面的 natvie 方法解出具体行


        《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
        浏览器打开:qq.cn.hn/FTe 免费领取
        复制代码


        号:


        /*


        *?Creates?an?array?of?StackTraceElement?objects?from?the?data?held


        *?in?"stackState".


        */


        private?static?native?StackTraceElement[]?nativeGetStackTrace(Object?stackState);


        于是我们的思路就是 Hook 住上报的位置,通过反射拿到指令集位置,在 Crash 上报前把指令集位置赋值给无意义的行号(理论上高版本在没有 debugItemInfo 时,已经默认是指令集位置而不是-1 了)。这里比较坑的是 stackState 的类型,其定义如下:


        /**


        *?An?intermediate?representation?of?the?stack?trace.??This?field?may


        *?be?accessed?by?the?VM;?do?not?rename.


        */


        private?transient?volatile?Object?stackState;


        在不同版本上,该对象的数据类型都不一样。

        [](

        )4.0(华为畅玩 4C,版本 4.4.4)


        [](

        )5.0(华为 P8 Lite,版本 5.0.2)


        [](

        )6.0(三星 GALAXY S7,版本 6.0.1)


        [](

        )7.0+(三星 GALAXY C7,版本 7.0)



        这里是第一个比较坑的地方,有的是 int 数组,有的是 long 数组,有的是 Object 数组的第一/最后一项,而且指令集位置有的在一起,有的是间隔的,确实比较坑,需要适配兼容。

        [](

        )8.0


        8.0 有一个问题,异常处理系统初始化时会执行如下逻辑:


        //?代码版本:Android8.0,文件名称:RuntimeInit.java


        protected?static?final?void?commonInit()?{


        if?(DEBUG)?Slog.d(TAG,?"Entered?RuntimeInit!");


        /*


        *?set?handlers;?these?apply?to?all?threads?in?the?VM.?Apps?can?replace


        *?the?default?handler,?but?not?the?pre?handler.


        */


        Thread.setUncaughtExceptionPreHandler(new?LoggingHandler());


        Thread.setDefaultUncaughtExceptionHandler(new?KillApplicationHandler());


        ......


        }


        其中 Thread.setUncaughtExceptionPreHandler(new LoggingHandler());?是该版本新增的,会在 uncaughtException 之前调用,LoggingHandler 会导致 Throwable#getInternalStackTrace 被调用,该方法逻辑如下:


        /**


        *?Returns?an?array?of?StackTraceElement.?Each?StackTraceElement


        *?represents?a?entry?on?the?stack.


        */


        private?StackTraceElement[]?getInternalStackTrace()?{


        if?(stackTrace?==?EmptyArray.STACK_TRACE_ELEMENT)?{


        stackTrace?=?nativeGetStackTrace(stackState);


        stackState?=?null;?//?Let?go?of?intermediate?representation.


        return?stackTrace;


        }?else?if?(stackTrace?==?null)?{


        return?EmptyArray.STACK_TRACE_ELEMENT;


        }?else?{


        return?stackTrace;


        }


        }


        因此,8.0 以上版本在 Hook 默认的 UncaughtExceptionHandler 时,stackState 信息**已经丢失了!!**我的解决办法是 反射 Hook 掉 Thread#uncaughtExceptionPreHandler 字段,使 LoggingHandler 被覆盖


        但是在 9.0 会有以下错误:


        Accessing?hidden?field?Ljava/lang/Thread;->uncaughtExceptionPreHandler:Ljava/lang/Thread$UncaughtExceptionHandler;?(dark?greylist,?reflection)


        java.lang.NoSuchFieldException:?No?field?uncaughtExceptionPreHandler?in?class?Ljava/lang/Thread;?(declaration?of?'java.lang.Thread'?appears?in?/system/framework/core-oj.jar)


        at?java.lang.Class.getDeclaredField(Native?Method)


        at?top.vimerzhao.testremovelineinfo.ExceptionHookUtils.init(ExceptionHookUtils.java:18)


        ......


        通过类似 FreeReflection 目前可以突破这个限制,因此 Android 9+ 的机型依然可以使用这个方案。


        ##? ?深入


        这里再详细介绍下底层获取行号的逻辑,首先 Throwable 会调用到一个 native 方法(这里的注释信息讲的很清楚,注意看):


        //http://androidxref.com/4.4_r1/xref/dalvik/vm/native/dalvik_system_VMStack.cpp


        /*


        *?public?static?int?fillStackTraceElements(Thread?t,?StackTraceElement[]?stackTraceElements)



          *?Retrieve?a?partial?stack?trace?of?the?specified?thread?and?return


          *?the?number?of?frames?filled.??Returns?0?on?failure.


          */


          static?void?Dalvik_dalvik_system_VMStack_fillStackTraceElements(const?u4*?args,


          JValue*?pResult) {


          Object*?targetThreadObj?=?(Object*)?args[0];


          ArrayObject*?steArray?=?(ArrayObject*)?args[1];


          size_t?stackDepth;


          int*?traceBuf?=?getTraceBuf(targetThreadObj,?&stackDepth);


          if?(traceBuf?==?NULL)


          RETURN_PTR(NULL);


          /*


          *?Set?the?raw?buffer?into?an?array?of?StackTraceElement.


          */


          if?(stackDepth?>?steArray->length)?{


          stackDepth?=?steArray->length;


          }


          dvmFillStackTraceElements(traceBuf,?stackDepth,?steArray);


          free(traceBuf);


          RETURN_INT(stackDepth);


          }


          该方法计算行信息的是 dvmFillStackTraceElements:


          //?http://androidxref.com/4.4_r1/xref/dalvik/vm/Exception.cpp


          /*


          *?Fills?the?StackTraceElement?array?elements?from?the?raw?integer


          *?data?encoded?by?dvmFillInStackTrace().



            *?"intVals"?points?to?the?first?{method,pc}?pair.


            */


            void?dvmFillStackTraceElements(const?int*?intVals,?size_t?stackDepth,?ArrayObject*?steArray) {


            unsigned?int?i;


            /?init?this?if?we?haven't?yet?/


            if?(!dvmIsClassInitialized(gDvm.classJavaLangStackTraceElement))


            dvmInitClass(gDvm.classJavaLangStackTraceElement);


            /*


            *?Allocate?and?initialize?a?StackTraceElement?for?each?stack?frame.


            *?We?use?the?standard?constructor?to?configure?the?object.


            */


            for?(i?=?0;?i?<?stackDepth;?i++)?{


            Object*?ste?=?dvmAllocObject(gDvm.classJavaLangStackTraceElement,ALLOC_DEFAULT);


            if?(ste?==?NULL)?{


            return;


            }


            Method*?meth?=?(Method*)?*intVals++;


            int?pc?=?*intVals++;


            int?lineNumber;


            if?(pc?==?-1)??????//?broken?top?frame?


            lineNumber?=?0;


            else


            lineNumber?=?dvmLineNumFromPC(meth,?pc);


            ......


            /*


            *?Invoke:


            *??public?StackTraceElement(String?declaringClass,?String?methodName,


            *??????String?fileName,?int?lineNumber)


            *?(where?lineNumber==-2?means?"native")


            */


            JValue?unused;


            dvmCallMethod(dvmThreadSelf(),?gDvm.methJavaLangStackTraceElement_init,


            ste,?&unused,?className,?methodName,?fileName,?lineNumber);


            ......


            dvmSetObjectArrayElement(steArray,?i,?ste);


            }


            }


            由此可知,默认行号可能是 0,否则通过 dvmLineNumFromPC 获取具体信息:


            //http://androidxref.com/4.4_r1/xref/dalvik/vm/interp/Stack.cpp


            /*


            *?Determine?the?source?file?line?number?based?on?the?program?counter.


            *?"pc"?is?an?offset,?in?16-bit?units,?from?the?start?of?the?method's?code.



              *?Returns?-1?if?no?match?was?found?(possibly?because?the?source?files?were


              *?compiled?without?"-g",?so?no?line?number?information?is?present).


              *?Returns?-2?for?native?methods?(as?expected?in?exception?traces).


              */


              int?dvmLineNumFromPC(const?Method*?method,?u4?relPc) {


              const?DexCode*?pDexCode?=?dvmGetMethodCode(method);

              用户头像

              Android架构

              关注

              还未添加个人签名 2021.10.31 加入

              还未添加个人简介

              评论

              发布
              暂无评论
              为了KPI,对APK进行极限优化!,2021年Android春招面试经历