关于「幽灵调用」一事第三弹:完结?

298 阅读4分钟

事件回顾

第一弹:你的App是否有出现过幽灵调用?
第二弹:「幽灵调用」背后的真相:一个隐藏多年的Android原生Bug

前面从一个常见的 BUG 的分析定位到是由编译器 R8 优化导致的问题,调用了不该出现的函数,若程序一直稳定运行没有发生错误,其实问题也挺严重的,因为它会因为错误的函数调用,而导致对象自身的内存被污染,例如原本一某成员是个状态量,被污染成其它状态值,导致程序按非预期路径发展。

测试程序

根据第一弹的案例提到的B、C类,我们设计同样的类结构来模拟该优化现像。

类的设计

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");
    }
}
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");
    }
}

防止未初始化

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 优化

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'
    }
}
-keep class penguin.reuse.dex.TextureSubView { *; }
-keep class penguin.reuse.dex.ImageSubView { *; }

风险检测

使用 core-parser 检测功能,我们可把该复用 dex_pc_ptr 的函数检测出来。
项目详情见:基于 Core 文件的 Android 调试与分析套件

# ./data/core-parser -p `pidof penguin.reuse.dex`
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()
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
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 分别处理到会被优化的函数上。

public class TextureSubView extends TextureView {
    boolean isAttach = false;
...
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        isAttach = true;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isAttach = false;
    }
...
}
public class ImageSubView extends AppCompatImageView {
    boolean isAttach = false;
...
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        isAttach = true;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isAttach = false;
    }
...
}
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()
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
}
继承父级函数
ContentFrameLayoutFrameLayoutandroid.view.ViewGroup.onDetachedFromWindow
ActionBarContextViewViewGroupandroid.view.ViewGroup.onDetachedFromWindow
k.DTextViewandroid.view.View.onDetachedFromWindow

依旧存在其它来自公共库代码被优化的风险。而做了差异化处理的函数添加新成员变量,而不同类型中 field Id 的不同二产生不同的字节码结果,因此不会被优化在一块。

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
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 版本

Android Gradle 插件 8.​13 版本说明

API 级别最低 Android Studio 版本最低 AGP 版本
36.0Meerkat 2024.3.1 Patch 18.9.1
35Koala 功能更新 2024.2.18.6.0
34Hedgehog 2023.1.18.1.1
33Flamingo 2022.2.17.2

查阅相关的版本详情,而当前我测试用的版本是 agp-8.5.1,查询分支历史应该在 agp-8.2.0 开始会有此类优化现象,也就是大家去年(2024)更新 Android Studio 软件开始导入,正式修复释放的版本应该会在 agp-9.x.x 版本。

避免优化?

-dontshrink
-keep class penguin.reuse.dex.TextureSubView { *; }
-keep class penguin.reuse.dex.ImageSubView { *; }
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 参数减少更多的代码压缩,问题依旧存在,相比之前少了部分复用函数。

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 更新

R8 官方文档
R8 如何优化我们的代码(1) -- 减少类的加载

编译 r8.jar

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$PATH:$HOME/depot_tools"

git clone https://r8.googlesource.com/r8
cd r8
tools/gradle.py r8

替换 r8.jar

pluginManagement {
    buildscript {
        dependencies {
            classpath(files("<path>/r8.jar"))
        }
    }
}

重新编译此测试程序,在使用 core-parser 进行检测已经不存在风险了。

core-parser> space --full-check
core-parser> 
core-parser> class penguin.reuse.dex.ImageSubView -m | grep onDetachedFromWindow
    [0x7180032b50] public void penguin.reuse.dex.ImageSubView.onDetachedFromWindow()
core-parser> method 0x7180032b50 --dex
public void penguin.reuse.dex.ImageSubView.onDetachedFromWindow() [dex_method_idx=7210]
DEX CODE:
  0x7253b1ea9c: 106f 06ae 0000           | invoke-super {v0}, void android.view.View.onDetachedFromWindow() // method@1710
  0x7253b1eaa2: 000e                     | return-void
core-parser> class penguin.reuse.dex.TextureSubView -m | grep onDetachedFromWindow
    [0x7180032ac0] public void penguin.reuse.dex.TextureSubView.onDetachedFromWindow()
core-parser> method 0x7180032ac0 --dex
public void penguin.reuse.dex.TextureSubView.onDetachedFromWindow() [dex_method_idx=7216]
DEX CODE:
  0x7253b1ec0c: 106f 06ae 0000           | invoke-super {v0}, void android.view.View.onDetachedFromWindow() // method@1710
  0x7253b1ec12: 000e                     | return-void
core-parser>

结语

当前没有很好办法去避免此优化问题,差异化确保自己编写的代码不被复用,不能保证使用的三方库代码编译优化复用的情况,在 AGP 未更新前,只能是尽量的去避免引发问题,或者自己编译对应版本的 R8 来替换。