方法调用栈混乱引起的Proguard内联学习

方法调用栈混乱引起的Proguard内联学习

首先说这个事情是怎么来的,先看一段firebase的报错。

Fatal Exception: java.lang.NullPointerException: Attempt to read from field 'java.lang.String xe4.c' on a null object reference
       at com.yocn.zyk.online.cash.bean.CashAccountUpi.getVpa(CashAccountUpi.java:31)
       at com.yocn.zyk.online.cash.dialog.CashAccountUpiDialogFragment.initView(CashAccountUpiDialogFragment.java:91)
       at com.yocn.zyk.online.coins.dialog.CoinsBaseBottomDialogFragment.onViewCreated(CoinsBaseBottomDialogFragment.java:50)
       at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:892)
...
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:499)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:965)

看起来这是一段很平常的NPE的报错,那我们来看代码的调用:

# CashAccountUpiDialogFragment.class
public class CashAccountUpiDialogFragment {
    @Override
    protected void initView() {
...
        if (!TextUtils.isEmpty(cashAccountUpi.getVpa())) { // line:91
            cashVpa.setText(cashAccountUpi.getVpa());
        }
...
    }

}

# CashAccountUpi.class
public class CashAccountUpi {

    private String vpa;

    public String getVpa() {
        return vpa; // line:31
    }

}

我们观察发现最顶层的crash发生位置CashAccountUpi.getVpa(CashAccountUpi.java:31)似乎没有可以发生的条件啊,这里怎么会发生NPE呢?反而第二行的CashAccountUpiDialogFragment.initView(CashAccountUpiDialogFragment.java:91)似乎才是真正的NPE的发生位置,到底哪儿是crash位置呢?

  1. 如果CashAccountUpi为空,代码就无法执行进CashAccountUpi#getVpa()
  2. 如果CashAccountUpi不为空,代码会执行进去,但是line31行不会报空。

这就想不通了,为什么调用栈会崩溃在一个没有任何条件的地方呢?
因为多线程的关系?但是这段代码并没有多线程来执行。并且我试了,即使有多线程执行,调用栈也不是这样的。

去吹水群问了一下,有群友给了这么个关键字,proguard的内联,之前确实不了解,专门去搜了一下,感觉确实像是这个表现,这里有一篇文章感觉很有价值插件化、热补丁中绕不开的Proguard的坑。

mapping的组成

->为分界线,表示原始名称->新名称

  • 类映射,特征:映射以:结束。
  • 字段映射,特征:映射中没有()。
  • 方法映射,特征:映射中有(),并且左侧的拥有两个数字,代表方法体的行号范围。
  • 内联,特征:与方法映射相比,多了两个行号范围,右侧的行号表示原始代码行,左侧表示新的行号。
  • 闭包,特征:只有三个行号,它与内联成对出现。
  • 注释,特征:以#开头,通常不会出现在mapping中。
什么是内联

在代码优化过程中,对某一些方法进行内联(将被内联的方法体内容Copy到调用方调用被内联方法处,是一个代码展开的过程),修改了调用方的代码结构,所以被内联的方法Copy到调用方时需要考虑带来的副作用。当Copy来的代码发生崩溃时,Java stacktrace无法体现真实的崩溃堆栈和方法调用关系,它受调用方自身代码和内联Copy的代码相互影响。

内联主要分为两类:

  1. unique method - 被调用并且只被调用一次
  2. short method - 被调用多次可能,但是这个方法code_length小于8(并非代码行数)。

满足这两种的方法才可能被内联。

case study

找到mapping文件和apk文件,现在可以使用Android Studio查看apk包的内容,一般会有好几个dex文件,首先需要找到调用者CashAccountUpi所在的dex文件,像下面的图。

dex.png

使用proguard文件keep住的会保持原来的目录结构。没有keep的就需要去mapping文件查找混淆之后的文件名字。
xe4的混淆信息如下(删掉了无关的信息):

com.yocn.zyk.online.cash.bean.CashAccountUpi -> xe4:
    java.lang.String vpa -> c
    1:1:void ():10:10 -> 
    ...
    2:2:java.lang.String getVpa():31:31 -> c
    ...
    ...

那我们其实可以看到CashAccountUpi的名字就是xe4,可以看到xe4在dex中的结构,能看到vpa字符串和getVpa()方法。
再看在dex中的CashAccountUpi类,也就是xe4

CashAccountUpi

其实这里已经可以看出一些端倪,我们在这里找不到返回值为Stringc()方法,所以基本上能判定是被内联了。这里没有更多的线索了,那我们继续看调用者CashAccountUpiDialogFragment,在mapping文件中的信息如下:

com.yocn.zyk.online.cash.dialog.CashAccountUpiDialogFragment -> ag4:
...
    1:1:void initView():83:83 -> initView
    2:3:void initView():85:86 -> initView
    4:5:void initView():88:89 -> initView
    6:6:void initView():91:91 -> initView
    7:7:java.lang.String com.yocn.zyk.online.cash.bean.CashAccountUpi.getVpa():31:31 -> initView
    7:7:void initView():91 -> initView
    8:9:void initView():91:92 -> initView
    10:10:java.lang.String com.yocn.zyk.online.cash.bean.CashAccountUpi.getVpa():31:31 -> initView

回忆一下调用者的位置,在initView中第6行调用了cashAccountUpi.getVpa(),这里也可以看出来有内联信息,CashAccountUpi.getVpa():31:31指向了initView的第6行。这里可以确定确实是实现了内联,但是感觉还是差了点什么,可以继续深入查看。

在dex查看界面找到ag4,选中并且右键,点Show Bytecode,可以看到下面的图:

查看initView的第6行:


    .line 6
    iget-object v0, p0, Lag4;->i:Lxe4;

    .line 7
    iget-object v0, v0, Lxe4;->c:Ljava/lang/String;

    .line 8
    invoke-static {v0}, Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z

这里涉及到一些Smali文件信息,iget-object是获取对象信息,p0相当于this。
Lag4;->i:Lxe4;表示获取到Lag4i,是一个Lxe4类型,放到v0中,i的定义其实可以在上面的Smali中找到:
.field public i:Lxe4;

第7行就破案了,直接调用了xe4c。这就找到根儿了,完全没有调用getVpa()方法,而是直接调用了属性c,也就是CashAccountUpi中的vpa,混淆后的名字是c

所以在此案例中,proguard把调用cashAccountUpi.getVpa()内联后为cashAccountUpi.vpa,也就是舍弃掉getVpa()方法直接访问vpa属性,所以在报错中虽然按照mapping文件中的路径行号backtrace能找到CashAccountUpi,但是执行的是内联之后的代码cashAccountUpi.vpa,所以报错原因还是因为cashAccountUpi为空。

好好利用mapping文件,能找到一些莫名其妙的问题

你可能感兴趣的:(方法调用栈混乱引起的Proguard内联学习)