深入Android Runtime: 指令优化与Java方法调用

栏目: Android · 发布时间: 6年前

内容简介:作者简介:dc, 天天P图AND工程师先做一个小试验:apk的代码如下:

作者简介:dc, 天天P图AND工程师

做一个小试验

先做一个小试验: 在apk的activity中放一个Button和一个TextView,点击Button让结果显示在TextView上。

apk的代码如下:

public class MainActivity extends AppCompatActivity {

    Button button;
    TextView textView;    @Override
    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.text);
        button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {            @Override
            public void onClick(View v) {

                Test test = new Test();
                String s = test.getValue();
                textView.setText(s);

            }
        });
    }
}

其中Test类的代码如下:

public class Test {    
    public String getValue() {        
        return "this is method getValued";
    }
}

试着思考下,文本框显示的结果会是什么?

第1次结果:

如果运行正常,结果会如下(本次测试全部在Android AOSP N上执行):

this is method getValued

进一步试验

接下来,再进一步试验。 我们给apk的PathClassLoader的ClassPath最前面注入一个dex,这个dex仅包含一个class,和之前的Test的包名+类名一致,如下:

public class Test {    public String getValue(){        return "this is method getValue from dex";
    }    public String abc(){        return "this is method abc !!!";
    }
}

这是最简单的热修复原理,猜想一下,这次的结果是什么?

第2次结果

这次的结果会是什么呢?

实际上,在debug版本上,我们能够得到正确的结果:

而在release版本上,结果并不是我们想象的这样,结果如下:

现象解释

为什么会出现这样的现象:明明调用的是getValue方法,为什么返回的是abc方法的结果呢? 要解释这个现象,我们需要对Android虚拟机执行代码的原理有一定的了解。

当我们将 Java 代码编译成apk时,编译器会用javac将java文件转成class文件,再通过dx将class文件转成dex文件(如果是jack&jill编译器,不会有class生成的过程)。 apk安装时候,PMS会通过installd唤起dex2oat进程对apk进行优化。 当我们启动系统时候,虚拟机先加载BootClassLoader,再加载SystemClassLoader,分别将BOOTCLASSPATH和SYSTEMSERVERCLASSPATH中对应jar包中的class加载起来,。

apk启动时,将会创建一个PathClassLoader,将apk相关及其依赖的library中的class加载到内存。 如果我们往PathClassLoader的clssapath中最开始注入新的jar/dex,在运行时PathClassLoader就会优先加载前面的jar/dex,从而覆盖apk本身的类实现类的替换。

但是我们通常不会注意到虚拟机的机制。

在安装apk时,如果apk是debug版本,会被强制以解释方式执行,此时执行的是字节码,我们看到的字节码是这样的:

即invoke-virtual+methodID的方式执行。这个methodID是存储在apk自身的dex中的,每个dex中都有一个String表和Method表(当然还有Class表等其他表)。 通过String表,可以查到某个index对应的String是什么;通过method表,可以拿到methodID对应的StringID,然后再到String表中查到方法名称。 虚拟机通过方法名称,再从已加载cache中查找方法,如果方法没找到,就从classpath加载并resolve,最终找到对应的method。

那么正常debug版本解释执行时,这个过程是没有任何问题的,包括使用新的类覆盖了旧的类的时候,仍然可以通过自身编译时就决定的methodID拿到正确的方法名,也就可以获取到正确的method并执行。

但是release版本的时候,dex会被优化的。dex2oat根据系统prop中的配置决定进行何种程度的优化,在AOSP N上,默认配置如下:

interpret-only模式的优化,实际上只是dalvik指令级的优化,并不会生成机器码(其他speed之类的优化模式会产生部分机器码,everything模式是完全编译,将所有字节码均优化成机器码),而是会对invoke-virtual这样的指令进行quicken优化,变成invoke-virtual-quick。 优化的目的,是将methodID的查找变成vtable的查找。methodID是dex全局的查找,相比vtable在class内部的查找,效率要高很多,毕竟一个dex中很可能有几万个method,而一个class中的method通常只有几个到几十个。

interpret-only的优化,是基于一个前提,编译时不仅能获取到class的名称,还能获取到class的定义。 因为我们是动态加载了dex,这个dex只有在classloader加载dex时才会被发现,dex2oat编译时只知道apk自身中的class的存在。

dex2oat进行interpret-only优化时,编译依赖是原先的method,导致生成的vtable索引为原先Test类中的方法索引。但是运行的时候,新的Test类由于加上了一个abc的方法,android中的各种String表、method表、vtable等都是按照字母表顺序进行排序,导致abc方法排在Test方法之前,这样原先的vtable索引查到的method就变成了abc方法。

由于vtable索引的变化,就出现了明明是调用的Test方法,可结果跑的是abc方法的奇特现象。

如果我们进行verify-none模式的编译(不进行quicken优化,或者其他能编译成机器码的模式),让其以解释模式运行,就不会有问题。但是如果apk在Manifest中设置了android:vmSafeMode=”true” ,那么无论是否使用了其他模式进行强制编译,apk会始终以interpret-only方式编译,导致问题一直存在。 比如我们使用speed编译,日志中依然是interpret-only:

总结

在进行apk热修复、插件化、动态加载的时候,会经常多个jar/dex包含相同的class,如果class结构因为需要升级出现了变化,会隐藏一些很难解释的坑在里面,务必谨慎。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

风向

风向

何宝宏 / 人民邮电出版社 / 2019-1 / ¥68.00元

★这是处于不断变化的互联网时代,行业从业者与非专业从业者都应阅读的解惑之书。 ★揭示互联网思想和精神的“内核”,帮助更多人了解互联网基因。 ★看清人工智能、区块链、大数据、云计算等技术发展的规律和机会。 ★为投资者、创业者提供方向,为广大技术从业者了解技术,为就业择业者提供建议和参考。 ★中国信通院院长刘多、腾讯云总裁邱跃鹏做序推荐。 ★中国工程院院士邬贺铨、中国科学......一起来看看 《风向》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码