为了 KPI,对 APK 进行极限优化!,大厂 Android 研发岗面试复盘
等等。
这些方法都比较常规,在项目成熟后优化的空间也比较有限。以应用宝为例,目前(2020 年 1 月)项目代码中 Java 文件 8040 个,代码行数约 143 万行,最终生成的 release 包 9.33M。可以优化的空间极为有限,而且由于维护较差,分析已经废弃的代码和资源其实非常耗时耗力。本文的方案可以使应用宝在现有基础上立刻减少约 700k 安装包大小,收益十分可观,而且对于一个项目,?代码量越大,效果越明显?。
在这分享一份整理了 2 个月的 Android 进阶面试解析笔记文档,包括了知识点笔记和高频面试问题解析及部分知识点视频讲解给大家!为了不影响阅读,在这以图片展示部分内容于目录截图,有需要的朋友麻烦点赞后点击下面在线链接获取免费领取方式吧!
我们在开发中经常会去看 Crash 日志来定位问题,如下:
W/System.err:?java.lang.NullPointerException
W/System.err:?????at?b.a.a.a.a(Test.java:26)
W/System.err:?????at?com.tencent.androidfactory.MainActivity.onCreate(MainActivity.java:15)
W/System.err:?????at?android.app.Activity.performCreate(Activity.java:7458)
W/System.err:?????at?android.app.Activity.performCreate(Activity.java:7448)
W/System.err:?????at?android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1286)
......
然后通过 map 文件找到对应的类:
com.tencent.androidfactory.Test?->?b.a.a.a:
14:14:void?<init>()?->?<init>
23:69:void?test()?->?a
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:
评论