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

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

原文发布于微信公众号 - 天天P图攻城狮(ttpic_dev)

原文发表时间:2018-10-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏刘望舒

Android系统源码分析-JNI

序言 因为在接下来的源码分析中将涉及大量的Java和Native的互相调用。当然对于我们的代码分析没有什么影响,但是,这样一个黑盒子摆在面前,对于其实现原理还...

3267
来自专栏技术小黑屋

系统剖析Android中的内存泄漏

作为Android开发人员,我们或多或少都听说过内存泄漏。那么何为内存泄漏,Android中的内存泄漏又是什么样子的呢,本文将简单概括的进行一些总结。

1073
来自专栏码匠的流水账

聊聊spring cloud gateway的ForwardedHeadersFilter

本文主要研究一下spring cloud gateway的ForwardedHeadersFilter

982
来自专栏刘望舒

Android内存优化(三)避免可控的内存泄漏

前言 内存泄漏向来都是内存优化的重点,它如同幽灵一般存于我们的应用当中,有时它不会现身,但一旦现身就会让你头疼不已。因此,如何避免、发现和解决内存泄漏就变得尤为...

21910
来自专栏Flutter入门到实战

模仿安卓源码,手写过时的方法兼容低版本

我们经常会使用getColor(R.color.XXX)获取颜色的资源文件,但是在安卓6.0开始,这个方法被标注为过时,推荐使用两个参数的方法替代,如下图所示:

1032
来自专栏郭霖

Android Volley完全解析(三),定制自己的Request

经过前面两篇文章的学习,我们已经掌握了Volley各种Request的使用方法,包括StringRequest、JsonRequest、ImageRequest...

2356
来自专栏JAVA高级架构

适配器模式(Adapter)

1003
来自专栏Android源码框架分析

Android Studio Profiler Memory (内存分析工具)的简单使用及问题

Memory Profiler 是 Android Studio自带的内存分析工具,可以帮助开发者很好的检测内存的使用,在出现问题时,也能比较方便的分析定位问题...

4093
来自专栏Java呓语

DataBinding·常用注解说明

Observable接口提供给开发者添加/移除监听者的机制。为了使开发更便捷,我们创建了BaseObservable类,它已经实现了Observable接口中的...

1414
来自专栏求索之路

Android源码设计模式解析与实战笔记

1.单一职责原则:比如说一个ImageLoader,需要加载图片的缓存图片,此时如果将这两个功能都放在一个类中,就违反了这个原则, 我们需要将不同的功能用类精...

4645

扫码关注云+社区

领取腾讯云代金券