1、问题背景

我有一个SDK被集成进了SystemUI。SystemUI在周末进行Coverage构建的时分提示如下过错(省掉了一些无关的信息,仅保留了要害文字):

dex2oatd method_verifier.cc:5126] void ...c(...) failed to verify: [0x115] expected to be within a catch-all for an instruction when a monitor is held
dex2oatd method_verifier.cc:867] Had a hard failure verifying all classes, and was asked to abort in such situation.

我被告知正常构建不会报错,只有Coverage构建会报错。由于我对SystemUI的构建不了解,无法从构建过程下手进行剖析,故只能从过错信息自身着手。

2、问题剖析

dex2oat源码去剖析对我来说显然不现实,所以我聚焦在这段文字自身:

expected to be within a catch-all for an instruction when a monitor is held

从这段文字能够看到几个要害字:monitorinstructioncatch-all。我决定从略有形象的monitor下手。

2.1、monitor

在网上一搜,我承认到monitorsynchronized字节码(精确的说是smali,但区分他们在本文没有太大意义)。

假设有如下代码:

synchronized(lock) {
    data = 2
}

那么编译生成的字节码如下所示:

monitor-enter v0
const/4 v1, 0x2
:try_start_4
iput v1, p0, Lphantom/monitor/MainActivity;->data:I
:try_end_8
.catchall {:try_start_4 .. :try_end_8} :catchall_a
monitor-exit v0
return-void
:catchall_a
move-exception v1
monitor-exit v0
throw v1

这段字节码包括几个关键:

  1. synchronized要害字会生成成对的monitor-entermonitor-exit指令。
  2. 编译器会主动刺进一段try-catch。刺进try-catch的目的据说[1]是为了保障monitor在产生反常的时分不会被泄露。

归纳上面两点,synchronized要害字的字节码大概会遵从如下形式:

monitor-enter
try_start
try_end
catchall
monitor-exit

在上面的字节码中出现了catchall,正好是SystemUI报错信息中提到的要害字。

2.2、catch和catchall

为了了解catchall的特别之处,比照下面代码的字节码。

// code 1
try {
    data = 3
} catch (e: Exception) {
    //
}
// code 2
try {
    data = 4
} catch (e: Throwable) {
    //
}

上述两头代码生成的字节码如下所示:

# code 1
const/4 v0, 0x3
:try_start_1
iput v0, p0, Lphantom/monitor/JavaClass;->data:I
:try_end_3
.catch Ljava/lang/Exception; {:try_start_1 .. :try_end_3} :catch_3
# code 2
const/4 v0, 0x4
:try_start_1
iput v0, p0, Lphantom/monitor/JavaClass;->data:I
:try_end_3
.catchall {:try_start_1 .. :try_end_3} :catchall_3

通过上面试验能够看出:

  1. catchall对应于catch(Throwable),即捕获一切反常。
  2. catch对应于详细类型的反常捕获。

综上,能够推测SystemUI报错的问题同时涉及synchronized句子try-catch句子。

2.3、当代码同时涉及synchronized和try-catch的时分

结合报错信息,定位到SDK中的方法,在其中发现了这样一段代码:

try {
    // ... some other code
    synchronized (mLock) {
        // ... some code
    }
} catch (e: Exception) {
    // ... some other code
}

这段代码对应的字节码如下:

:try_start_135
# ... some other code
monitor-enter v4
:try_end_14d
.catch Ljava/lang/Exception; {:try_start_135 .. :try_end_14d} :catch_16d
:try_start_14f
# some code
:try_end_153
.catchall {:try_start_14f .. :try_end_153} :catchall_166
:try_start_153
# some code
monitor-exit v4
:try_end_158
.catch Ljava/lang/Exception; {:try_start_153 .. :try_end_158} :catch_16d
# ... some other code

该字节码有如下关键:

  1. 编译器主动刺进的catchall依然存在。
  2. 编译器额外刺进了两段try-catch(Line5和Line14),且没有运用catchall

原设想synchronized外层的try-catch会生成一条catch指令,并把整个monitor包裹住。但实在的作用外层try-catchsynchronized劈成了两段,一段在Line15,另一段在Line1014,且前一段与后一段共享相同的反常类型和反常处理。猜想是try-catch不能嵌套,但并未去证实。

根据上述推论,将源码改为:

try {
    // ... some other code
    synchronized (mLock) {
        // ... some code
    }
} catch (e: Throwable) { // <- 把Exception改为Throwable
    // ... some other code
}

得到如下字节码:

:try_start_1
# ... some other code
monitor-enter v0
:try_end_6
.catchall {:try_start_1 .. :try_end_6} :catchall_11
:try_start_8
# some code
:try_end_c
.catchall {:try_start_8 .. :try_end_c} :catchall_e
:try_start_c
monitor-exit v0
:try_end_11
.catchall {:try_start_c .. :try_end_11} :catchall_11
# ... some other code

可见Line5和Line13都变成了catchall,而其他字节码并无实质改变。

综上,能够承认当try-catch中包括synchronized的时分,try-catch指令会被synchronized劈开,且有一条catch/catchall会刺进在monitor-enter之后。

3、问题解决

3.1、修改作用及定论

经过上面的剖析,SystemUI之所以报错是因为Android以为Java编译器在monitor-entermonitor-exit之间只能有catchall指令,而catch指令是不够安全的。

根据上述推论,将SDK中的catch反常类型从Exception修改为Throwable,从头集成到SystemUI做构建,本文最初的报错没有再出现。

由此得出一条潜规矩:当内部直接包括synchronized句子的时分,catch的类型有必要是Throwable

3.2、回过头来:Java编译输出的字节码真的不够安全吗?

源代码

try {
    // ... some code #1
    synchronized(lock) {
        // ... some code #2
    }
} catch (e: Exception) {
    // ... some code #3
}

字节码:

:try_start_1
# ... some code #1
monitor-enter v0
:try_end_6
.catch Ljava/lang/Exception; {:try_start_1 .. :try_end_6} :catch_11
:try_start_8
# ... some code #2
:try_end_c
.catchall {:try_start_8 .. :try_end_c} :catchall_e
:try_start_c
monitor-exit v0
goto :goto_11
:catchall_e
move-exception v1
monitor-exit v0
throw v1
:try_end_11
.catch Ljava/lang/Exception; {:try_start_c .. :try_end_11} :catch_11
:catch_11
# ... some code #3
:goto_11
return-void
  1. 假如不产生任何反常,字节码会从Line1次序履行到Line11,没有任何跳转,monitor-entermonitor-exit是配对的。
  2. 假如code #2产生反常,那么会命中Line9的catchall,越过Line11的monitor-exit,跳转到Line13的反常处理,履行Line15的monitor-exitmonitor-entermonitor-exit依然是配对的。
  3. 假如code #1产生反常,有两种情况:
    1. 产生的反常是Exception类型:命中Line5,那么Line3的monitor-enter不会履行,而是跳转到Line19,不会履行monitor-exit
    2. 发证的反常不是Exception类型:不会命中Line5,反常应该会中止代码履行,理论上Line3及之后的一切指令都不会履行,包括Line3的monitor-enter在内。

根据上述剖析,能够信任:

  1. 假如履跋涉了synchronizedmonitor-entermonitor-exit总是配对的,monitor是安全的。
  2. 假如没有履跋涉synchronized,底子就不会进入monitor,monitor也是安全的。

综上能够得出定论:Java编译器生成的字节码是没有问题的。问题的实质可能是:

  1. Android机械的以为monitor内部不能有catch指令,只能有catchall指令。
  2. Java编译劈开try-catch时大可把catch指令放在monitor-enter之前,但却放在了monitor-enter之后。不论Java编译器的目的为何,但这个结果跟Android的校验规矩不合。

3.3、遗留问题和其他定论

还有一些问题没能深入研究:

  1. 为什么Coverage构建会报错,而正常构建不报错?
  2. 为什么把APK直接装置到手机上运行的时分dex2oat不会报错,只有在构建的时分报错?
  3. Java编译器生成的字节码不能说是过错的,但为啥Android又不认可。已然Android不认可,为啥不在构建的更早环节报错呢?

在解决问题的过程中还得出了如下定论:

  1. try-catch句子和runCatching句子在反常类型为Throwable的时分,生成的字节码没有实质区别。
  2. Java和Kotlin的try-catch句子生成的字节码没有实质区别。

上述定论很容易验证,这里不再赘述。

关注大众号:Android老皮!!!欢迎大家来找我讨论交流