移动开发前线

从一个小demo看art虚拟机本地方法调用的动态绑定和静态绑定机

2017-12-13  本文已影响36人  f6f7b51e049d

最近在做一些有关art的研究工作,碰到一些有趣的东西,准备写一篇介绍art和jvm的成体系的文章,由于文章计划篇幅较大,因此先写一些知识点的介绍。

本篇文章主要由一个有趣的虚函数调用欺骗的demo小程序出发,介绍art虚拟机本地方法调用的动态绑定和静态绑定机制,以及编译器对于虚函数调用的优化,同时从这个小demo展示android N上的方法内联,由于目前网络上存在的介绍art执行类方法过程的文章都缺少本地方法调用的动态绑定和静态绑定的分析,导致很多读者看完后很难理解类方法在Marshmallow之前的运行机制,因此本文主要介绍这方面的内容。

1.本地方法调用的动态绑定和静态绑定机制以及编译优化

如图1所示,我们编写了一个demo小程序,这个demo中有个call方法接收一个ClassChildA类型参数,在方法体中,new了一个Invoker类型的对象,并将这个对象的shadow$_klass_通过反射的方式设置为了另外一个名为patch的Class类型,替换shadow$_klass_主要用于虚函数欺骗,今天暂且不表,以后再写文章介绍。

invoker对象初始化后便调用了方法a,其中方法a是一个public类型的方法,也就是一个虚方法,属于动态绑定,对于面向对象的语言,多态也就是动态绑定是三大主要特性之一,也是最核心机制,那么什么是动态绑定呢,绑定就是确定方法与类的关系,绑定分为动态绑定和静态绑定,静态绑定就是编译期就能确定方法属于哪个类,例如java中的private方法,final方法,static方法,init方法以及super调用,这些方法都是静态绑定,而interface,protected和public方法,由于可以被override(为了避免理解混乱还是英文最直接:)),这些方法需要运行时根据调用的对象来确定方法属于子类还是父类,因此这些方法被称为虚方法,属于动态绑定。

对于动态绑定的调用,android的虚拟机指令集是Invoke-virtual,虽然android使用的java的api作为开发语言,但实际上android的基础库实现和oracle并不相同,虚拟机的指令集和实现也完全不同,android虚拟机包括dalvik和art都是基于寄存器的三地址指令集架构,而jvm,hotspot采用的是基于栈的零地址指令集架构,Dalvik VM的主要设计者Dan Bornstein在Google I/O 2008上做过一个关于dalvik内部实现的演讲,介绍了dalvik这样设计的原因,有兴趣的同学可以去研究一下。

在mashmellow之前的art,app安装之后会将字节码预先编译为机器码,而在Nougat上,google为art设计了interpret,jit,和aot三种运行时,我们首先看一下虚函数调用也就是invoke-virtual指令和静态绑定例如invoke-direct指令编译成机器码分别是什么样子。

上图展示了call方法的字节码,new Invoke这条语句主要包括两条指令,一个是new-instance分配内存,主要由pAllocObject函数实现,另外一个是invoke-direct指令调用init方法进行初始化,我们首先看一下执行init这条指令的机器码

从机器码可以看出,invoke-direct指令执行的过程是,直接从dex_cache中通过固定的偏移找到了init方法,也就是说并没有用到invoker对象的class类型,属于静态绑定。

我们在看一下最后一条语句paramClassChildA.printName(this);的机器码,printName一个虚方法

从机器码可以看出,虚方法在执行过程中,也就是invoke-virtual指令编译成的机器码,是根据运行时对象获得其shadow$_klass_然后通过虚表查找的方式获得的artmethod,这与前面的invoke-direct指令不同,接下来我们看一下同为虚方法调用的Object localObject = invoker.a(paramClassChildA);这条语句的机器码

我们惊奇的发现,调用a方法机器码居然和调用printName方法的不同,而是和调用init方法的一样,并没有使用invoker对象的类型,属于静态绑定,经过研究后我们发现,art的编译器根据前面new Invoker的语句确定了invoker对象的类型,因此将a方法的调用优化成了静态绑定,为了验证这个结论,我们将call方法更改为下面代码

然后我们再看一下Object localObject = invoker.a(paramClassChildA);这条语句的机器码

果然如我们所料,变成了动态绑定,其中x24是invoker的对象指针,x25是paramClassChildA参数。

对于解释执行情况下的虚方法调用,限于篇幅限制,会在之后的系统性文章中再做介绍。

2.Android N上的方法内联

我们知道在marshmellow之前,android的编译器默认是Quick类型,nougat之后是Optimizing类型,Quick类型编译器的内联条件主要需满足以下几条:

1.App不是Debug版本的;

2.被调用方法的实现满足下列条件之一:

2.1. 空方法;

2.2. 仅返回方法参数;

2.3. 仅返回一个方法内声明的常量或null;

2.4. 从被调用方法所在类的非静态成员获取并返回获取的值;(注意,static final成员会被优化成常量,此时要参照2.3)

2.5. 仅设置了被调用方法所在类的非静态成员的值;

2.6. 仅设置了被调用方法所在类的非静态成员的值,并返回一个方法内声明的常量或null

Optimizing类型编译器的内联条件主要需满足以下几条:

1、App不是Debug版本的;

2、被调用的方法所在的类与调用者所在的类位于同一个Dex;(注意,符合Class N命名规则的多个Dex要看成同一个Dex)

3、被调用的方法的字节码条数不超过dex2oat通过--inline-max-code-units指定的值,6.x默认为100,7.x默认为32;

4、被调用的方法不含try块;

5、被调用的方法不含非法字节码;

6、对于7.x版本,被调用方法还不能包含对接口方法的调用。(invoke-interface指令)

从上面的条件对比可以看出,google在nougat之后采用了更加激进的内联策略,方法内联可以跨多级方法调用进行,最多可以达到5层。

接下来我们还是继续看call方法,我们首先看一下Invoker以及a的方法体

a方法很简单,仅仅返回一个空的字符串,在call方法中,我们将invoker的shadow$_klass_更改为了ClassChildB(patch是通过dex加载的ClassChildB),我么看一下ClassChildB及ClassChildB中的a方法(涉及到vtable的知识,ClassChildB中的a可以叫任何名字,只要保证和Invoker中的a的vtable索引相同即可),

我们通过点击call按钮来调用call方法看一下结果

第一次patch为空,调用了最后一句paramClassChildA.printName(this);ClassChildA的printName方法与ClassChildB几乎相同,只不过打印的是aa开头

接下来我们点击load按钮,将patch对象设置为ClassChildB.class,然后我们再次点击call按钮

我们惊奇的发现toast的内容,也就是Invoker的a方法的返回值,居然不是“”这个空字符串,其实这主要是我们通过shadow$_klass_将Invoker的对象类型设置为了ClassChildB,由于Invoker的a方法是一个虚方法,属于动态绑定,因此他需要使用invoker对象的class类型确定方法,所以导致虽然调用的是Invoker的a方法,但实际执行的是ClassChildB的a方法,并且运行时仅仅对Invoker的a方法进行了参数校验,并没有ClassChildB的a方法进行入参校验。

接下来我们点击数次call按钮,大约20多次吧,输出结果变成了下面这样

我们惊奇的发现,那个被取代的Invoker中的a方法,又神奇的执行了,回到刚才我们介绍的nougat之后的Optimizing类型的编译器的内联策略,我们怀疑a方法被 jit 内联了,通过前面的内容我们知道,含有try块的方法是不会被内联的,于是我们将Invoker的a方法更改为下面这样

不出所料,神奇的内联现象消失了。

通过上面的例子我们发现,在n之后的jit编译器上,jit会收集方法调用的热度,对满足内联条件的方法进行内联,这些内联策略会影响某些热修复技术,tinker的技术团队做过相应的介绍。

通过对不同版本art虚拟机以及jvm的实现进行研究,我们发现不同的java虚拟机以及不同版本的art实现差别很大,很多android上的基于native的热修复技术都利用了虚拟机的实现机制,然而art还未成为一种稳定实现的虚拟机,google对于art的不同版本经常会做比较大的改动,并且art与jvm不同的是,为了提高art的执行速度,google将dex看做一种不会更改,并且被优化后不可移植的代码,基于这个思想做了很多的编译优化,这给热修复技术带来了很大的难度,因此epic项目的作者给其最新的文章起名叫《我为Dexposed续一秒——论ART上运行时 Method AOP实现》,“续一秒”这个词我认为非常形象的描述了当前基于虚拟机实现机制的热修复或者hook技术特点,这种技术不会受到公开规范的保证,一旦art实现变化,很可能变得出现问题甚至不可用,因此instant run的团队采用了完全符合java规范的实现机制。

最后感谢RednaxelaFx大牛的博客以及知乎问答,让我学到了很多VM的基础知识

上一篇 下一篇

猜你喜欢

热点阅读