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
从这段文字能够看到几个要害字:monitor
、instruction
、catch-all
。我决定从略有形象的monitor
下手。
2.1、monitor
在网上一搜,我承认到monitor
是synchronized
的字节码(精确的说是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
这段字节码包括几个关键:
-
synchronized
要害字会生成成对的monitor-enter
和monitor-exit
指令。 - 编译器会主动刺进一段
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
通过上面试验能够看出:
-
catchall
对应于catch(Throwable)
,即捕获一切反常。 -
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
该字节码有如下关键:
- 编译器主动刺进的
catchall
依然存在。 - 编译器额外刺进了两段
try-catch
(Line5和Line14),且没有运用catchall
。
原设想synchronized
外层的try-catch
会生成一条catch
指令,并把整个monitor包裹住。但实在的作用外层try-catch
被synchronized
劈成了两段,一段在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-enter
和monitor-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
- 假如不产生任何反常,字节码会从Line1次序履行到Line11,没有任何跳转,
monitor-enter
和monitor-exit
是配对的。 - 假如code #2产生反常,那么会命中Line9的
catchall
,越过Line11的monitor-exit
,跳转到Line13的反常处理,履行Line15的monitor-exit
。monitor-enter
和monitor-exit
依然是配对的。 - 假如code #1产生反常,有两种情况:
- 产生的反常是
Exception
类型:命中Line5,那么Line3的monitor-enter
不会履行,而是跳转到Line19,不会履行monitor-exit
。 - 发证的反常不是
Exception
类型:不会命中Line5,反常应该会中止代码履行,理论上Line3及之后的一切指令都不会履行,包括Line3的monitor-enter
在内。
- 产生的反常是
根据上述剖析,能够信任:
- 假如履跋涉了
synchronized
,monitor-enter
和monitor-exit
总是配对的,monitor是安全的。 - 假如没有履跋涉
synchronized
,底子就不会进入monitor,monitor也是安全的。
综上能够得出定论:Java编译器生成的字节码是没有问题的。问题的实质可能是:
- Android机械的以为monitor内部不能有
catch
指令,只能有catchall
指令。 - Java编译劈开
try-catch
时大可把catch
指令放在monitor-enter
之前,但却放在了monitor-enter
之后。不论Java编译器的目的为何,但这个结果跟Android的校验规矩不合。
3.3、遗留问题和其他定论
还有一些问题没能深入研究:
- 为什么Coverage构建会报错,而正常构建不报错?
- 为什么把APK直接装置到手机上运行的时分
dex2oat
不会报错,只有在构建的时分报错? - Java编译器生成的字节码不能说是过错的,但为啥Android又不认可。已然Android不认可,为啥不在构建的更早环节报错呢?
在解决问题的过程中还得出了如下定论:
-
try-catch
句子和runCatching
句子在反常类型为Throwable
的时分,生成的字节码没有实质区别。 - Java和Kotlin的
try-catch
句子生成的字节码没有实质区别。
上述定论很容易验证,这里不再赘述。
关注大众号:Android老皮!!!欢迎大家来找我讨论交流