事件回顾
第一弹:你的App是否有出现过幽灵调用?
第二弹:「幽灵调用」背后的真相:一个隐藏多年的Android原生Bug
前面从一个常见的 BUG
的分析定位到是由编译器 R8
优化导致的问题,调用了不该出现的函数,若程序一直稳定运行没有发生错误,其实问题也挺严重的,因为它会因为错误的函数调用,而导致对象自身的内存被污染,例如原本一某成员是个状态量,被污染成其它状态值,导致程序按非预期路径发展。
测试程序
根据第一弹
的案例提到的B、C
类,我们设计同样的类结构来模拟该优化现像。
类的设计
typescript
public class TextureSubView extends TextureView {
public TextureSubView(@NonNull Context context) {
super(context);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
static void ini() {
Log.i("reuse", "class TextureSubView ini");
}
}
typescript
public class ImageSubView extends AppCompatImageView {
public ImageSubView(@NonNull Context context) {
super(context);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
static void ini() {
Log.i("reuse", "class ImageSubView ini");
}
}
防止未初始化
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ReuseTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
TextureSubView.ini();
ImageSubView.ini();
}
}
开启 R8 优化
java
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
debuggable false
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
kotlin
-keep class penguin.reuse.dex.TextureSubView { *; }
-keep class penguin.reuse.dex.ImageSubView { *; }
风险检测
使用 core-parser
检测功能,我们可把该复用 dex_pc_ptr
的函数检测出来。
项目详情见:基于 Core 文件的 Android 调试与分析套件
shell
# ./data/core-parser -p `pidof penguin.reuse.dex`
arduino
core-parser> space --full-check
ERROR: verify class reuse dex_pc_ptr method
[0x7180036410] public void penguin.reuse.dex.ImageSubView.onAttachedToWindow()
[0x4a05f940] public final void androidx.appcompat.widget.ContentFrameLayout.onAttachedToWindow()
ERROR: verify class reuse dex_pc_ptr method
[0x4a05f6d8] public final void androidx.appcompat.widget.ActionBarContextView.onDetachedFromWindow()
[0x71800363a0] public void penguin.reuse.dex.TextureSubView.onDetachedFromWindow()
[0x7180036430] public void penguin.reuse.dex.ImageSubView.onDetachedFromWindow()
[0x4a072f10] public final void k.D.onDetachedFromWindow()
[0x4a05f960] public final void androidx.appcompat.widget.ContentFrameLayout.onDetachedFromWindow()
java
core-parser> method 0x71800363a0 --dex
public void penguin.reuse.dex.TextureSubView.onDetachedFromWindow() [dex_method_idx=8200]
DEX CODE:
0x7253b5267c: 106f 0e51 0000 | invoke-super {v0}, void android.view.View.onDetachedFromWindow() // method@3665
0x7253b52682: 000e | return-void
java
core-parser> method 0x7180036430 --dex
public void penguin.reuse.dex.ImageSubView.onDetachedFromWindow() [dex_method_idx=8194]
DEX CODE:
0x7253b5267c: 106f 0e51 0000 | invoke-super {v0}, void android.view.View.onDetachedFromWindow() // method@3665
0x7253b52682: 000e | return-void
差异化处理
为 TextureSubView、ImageSubView 添加新的成员变量 isAttach 分别处理到会被优化的函数上。
scala
public class TextureSubView extends TextureView {
boolean isAttach = false;
...
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
isAttach = true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isAttach = false;
}
...
}
scala
public class ImageSubView extends AppCompatImageView {
boolean isAttach = false;
...
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
isAttach = true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isAttach = false;
}
...
}
arduino
core-parser> space --full-check
ERROR: verify class reuse dex_pc_ptr method
[0x4a05f840] public final void androidx.appcompat.widget.ContentFrameLayout.onDetachedFromWindow()
[0x4a05f5b8] public final void androidx.appcompat.widget.ActionBarContextView.onDetachedFromWindow()
[0x4a072f10] public final void k.D.onDetachedFromWindow()
scala
core-parser> class androidx.appcompat.widget.ContentFrameLayout -i
[0x4a018740]
public class androidx.appcompat.widget.ContentFrameLayout extends android.widget.FrameLayout {
// Implements:
android.graphics.drawable.Drawable$Callback
android.view.KeyEvent$Callback
android.view.accessibility.AccessibilityEventSource
android.view.MiRadiiChangedCallback
android.view.ViewParent
android.view.ViewManager
}
core-parser> class androidx.appcompat.widget.ActionBarContextView -i
[0x4a015ed8]
public class androidx.appcompat.widget.ActionBarContextView extends android.view.ViewGroup {
// Implements:
android.graphics.drawable.Drawable$Callback
android.view.KeyEvent$Callback
android.view.accessibility.AccessibilityEventSource
android.view.MiRadiiChangedCallback
android.view.ViewParent
android.view.ViewManager
}
core-parser> class k.D -i
[0x4a002710]
public class k.D extends android.widget.TextView {
// Implements:
android.graphics.drawable.Drawable$Callback
android.view.KeyEvent$Callback
android.view.accessibility.AccessibilityEventSource
android.view.MiRadiiChangedCallback
android.view.ViewTreeObserver$OnPreDrawListener
}
类 | 继承 | 父级函数 |
---|---|---|
ContentFrameLayout | FrameLayout | android.view.ViewGroup.onDetachedFromWindow |
ActionBarContextView | ViewGroup | android.view.ViewGroup.onDetachedFromWindow |
k.D | TextView | android.view.View.onDetachedFromWindow |
依旧存在其它来自公共库代码被优化的风险。而做了差异化处理的函数添加新成员变量,而不同类型中 field
Id 的不同二产生不同的字节码结果,因此不会被优化在一块。
php
core-parser> method 0x7180036290 --dex
public void penguin.reuse.dex.ImageSubView.onDetachedFromWindow() [dex_method_idx=8186]
DEX CODE:
0x7253c4a634: 106f 0e40 0001 | invoke-super {v1}, void android.view.View.onDetachedFromWindow() // method@3648
0x7253c4a63a: 0012 | const/4 v0, #+0
0x7253c4a63c: 105c 0e7a | iput-boolean v0, v1, Lpenguin/reuse/dex/ImageSubView;.isAttach:Z // field@3706
0x7253c4a640: 000e | return-void
php
core-parser> method 0x71800361e0 --dex
public void penguin.reuse.dex.TextureSubView.onDetachedFromWindow() [dex_method_idx=8192]
DEX CODE:
0x7253c4a82c: 106f 0e40 0001 | invoke-super {v1}, void android.view.View.onDetachedFromWindow() // method@3648
0x7253c4a832: 0012 | const/4 v0, #+0
0x7253c4a834: 105c 0e7b | iput-boolean v0, v1, Lpenguin/reuse/dex/TextureSubView;.isAttach:Z // field@3707
0x7253c4a838: 000e | return-void
无法做到完全避免复用的问题
R8 优化
在第二弹
当中,已经提到了具体的修复提交,但实际上我们暂时是无法使用的。
2025年9月18日,R8修复:Disable code deduplication for code objects containing invoke-super
2023年05月08日,R8引入:Dedup code objects on api 31 and above
AGP 版本
API 级别 | 最低 Android Studio 版本 | 最低 AGP 版本 |
---|---|---|
36.0 | Meerkat 2024.3.1 Patch 1 | 8.9.1 |
35 | Koala 功能更新 2024.2.1 | 8.6.0 |
34 | Hedgehog 2023.1.1 | 8.1.1 |
33 | Flamingo 2022.2.1 | 7.2 |
查阅相关的版本详情,而当前我测试用的版本是 agp-8.5.1
,查询分支历史应该在 agp-8.2.0
开始会有此类优化现象,也就是大家去年(2024)更新 Android Studio
软件开始导入,正式修复释放的版本应该会在 agp-9.x.x
版本。
避免优化?
kotlin
-dontshrink
-keep class penguin.reuse.dex.TextureSubView { *; }
-keep class penguin.reuse.dex.ImageSubView { *; }
arduino
core-parser> space --full-check
ERROR: verify class reuse dex_pc_ptr method
[0x7180071640] public void penguin.reuse.dex.ImageSubView.onDetachedFromWindow()
[0x71800715b0] public void penguin.reuse.dex.TextureSubView.onDetachedFromWindow()
添加 -dontshrink
参数减少更多的代码压缩,问题依旧存在,相比之前少了部分复用函数。
typescript
core-parser> method 0x4a338698 --dex
public void androidx.appcompat.widget.ActionBarContextView.onDetachedFromWindow() [dex_method_idx=23009]
DEX CODE:
0x71e92237e4: 106f 53d4 0001 | invoke-super {v1}, void android.view.View.onDetachedFromWindow() // method@21460
0x71e92237ea: 1054 7a39 | iget-object v0, v1, Lt/a;.z:Landroidx/appcompat/widget/a; // field@31289
0x71e92237ee: 0038 000a | if-eqz v0, 0x71e9223802 //+10
0x71e92237f2: 106e 5c6c 0000 | invoke-virtual {v0}, boolean androidx.appcompat.widget.a.F() // method@23660
0x71e92237f8: 1154 7a39 | iget-object v1, v1, Lt/a;.z:Landroidx/appcompat/widget/a; // field@31289
0x71e92237fc: 106e 5c6d 0001 | invoke-virtual {v1}, boolean androidx.appcompat.widget.a.G() // method@23661
0x71e9223802: 000e | return-void
core-parser>
关闭了压缩,未使用到的内容也会编译进入,因此产生不同的字节码。于是在配合差异化处理即可。
结语
当前没有很好办法去避免此优化问题,差异化确保自己编写的代码不被复用,不能保证使用的三方库代码编译优化复用的情况,在 AGP
更新前,尽量的去避免引发问题。