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

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

内容简介:作者简介: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结构因为需要升级出现了变化,会隐藏一些很难解释的坑在里面,务必谨慎。


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

查看所有标签

猜你喜欢:

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

HTTP Essentials

HTTP Essentials

Stephen A. Thomas、Stephen Thomas / Wiley / 2001-03-08 / USD 34.99

The first complete reference guide to the essential Web protocol As applications and services converge and Web technologies not only assume HTTP but require developers to manipulate it, it is be......一起来看看 《HTTP Essentials》 这本书的介绍吧!

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

RGB HEX 互转工具

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试