为了 KPI,对 APK 进行极限优化!,2021 年 Android 春招面试经历
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 方法解出具体行
号:
/*
*?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);
评论