干货 | 云智慧透视宝Java代码品质监控落到实处原理,云智慧java

云智慧透视宝Java代码性能监控实现原理

 一、Java平台体系及应用场景

从1995年Sun
Microsystems公司正式推出Java,到2006年时Sun公司将其开源,迄今为止已经有了20年的历史。Java本身已不仅仅只是一门面向对象的编程语言,而是由一系列计算机软件和规范形成的技术体系,这个技术体系提供了完整的跨平台开发与部署的支持,实现“一次编写、到处运行”的目的。Java已经广泛的应用于嵌入式、移动终端、企业服务器、大型机等各种场合。

Sun官方所定义的Java技术体系包括如下几个组成部分:

* Java程序设计语言

* 各种硬件平台上的Java虚拟机

* Class文件格式

* 来自商业机构和开源社区的第三方Java类库

图片 1

图:Java技术体系组件图

Java Virtual
Machine(JVM)是Java体系的基础,负责解释、编译执行.class文件形式的字节码,同时负责内存管理、热点代码检测和运行时编译优化。正是由于有了虚拟机的基础,才使Java实现了“一次编写、到处运行”。Java这20年的发展,其实更是虚拟机的发展过程。期间经历了Sun、BEA公司各自开发的虚拟机,2009年之后,ORACLE将这两家公司收购,并将这些虚拟机取长补短、合二为一。目前还是开源的虚拟机OpenJDK,可供爱好者学习研究用。

JRE部分是支持Java程序运行的标准环境。JDK是JRE的超集,包含JRE的一切,再加上工具如编译器、调试器等。

二、Java性能监控需求

对于一个企业的应用系统,大多数情况下,肯定是由多种编程语言开发的各种系统的集成。我们都非常关心系统的可用性、及时响应性、资源的消耗,比如CPU、内存、各种I/O、网路带宽等消耗情况。对于这些问题的性能瓶颈点,我们一般可以归纳为外部服务(如第三方API)、资源读写、代码异常。如果在发生这些问题时,能够及时完整的抓拍记录保留下来,那么对于我们解决问题将会提供充足的证据,解决问题会变的非常容易。

对于Java应用系统来说,JVM自身提供了相应的性能监控手段和工具,经常在出现问题后,比如内存泄漏或溢出时,我们会通过jmap命令导出堆的转储快照,利用相应的命令jhat或其他相应的第三方内存分析工具来分析对象的占用情况。

响应缓慢时,我们可能会用jstat监视命令、或jdk的可视化工具jconsole、visualvm来分析JVM的垃圾回收类型、回收频率,来推测是否是垃圾回收导致的。有可能我们还要接着分析线程转储快照,通过jstack取出线程的栈快照,来分析是否有真死锁、死循环导致的相应缓慢、资源负载高等情况。

当有问题出现时,许多开发人员可能都是比较盲目的用这些工具来试探性定位问题,而大多数情况下,这种试探会无功而返。因为这些分析工具主要是侧重Java单方面的分析,比如该系统调用第三方API,如果第三方API有问题,是无法监控到的。还有像文件、DB资源的访问也是是无法监控到的。

而且,只有对Java虚拟机机制较为熟悉的高级开发人员才能比较好的运用、理解这些工具,对于大多数普通Java开发人员来说,这些问题只会令他们束手无策。

像外部服务(如第三方API)、资源读写、代码异常这些瓶颈点,需要通过代码级别的监控才能直接、快速、有效的找到症结所在。调用第三方API的耗时、资源访问的耗时、代码抛出的非预知异常,这些常见问题代码监控完全能够监控到,并能够实时抓拍记录,一旦有问题可以快速还原事故问题现场。通过代码级别监控发现问题后,也可以在辅助利用虚拟机内置监控工具进行进一步的定位。

三、透视宝Java监控实现原理

图片 2

图:Java的执行模型

在Java的执行体系中,由.Java源码文件编译后的.class字节码文件,可以理解为中间语言。

图片 3

图:透视宝Java监控实现原理

图片 4

图:透视宝Java监控实例运行图

1、字节码load至JVM时发生了什么

* 回调函数注册完毕后,凡是当有任何的class文件即将被类加载器加载前,都

会执行回调函数transform,在此方法内实现的类改变操作。

* 实现的transform方法中,我们使用的是ASM字节码操作框架,ASM从二进制

形式的类文件中读取、分析类的信息,然后修改改变类的行为。

* transform方法的基本代码形式如下:

图片 5

2、如何实际改变类行为

* 在依赖于ASM基础之上,我们抽象出这样的业务模型

图片 6

* 常用的拦截探针

图片 7

* 常用的运行时拦截处理器

图片 8

* 支持的拦截定义过滤器规则

图片 9

* 该业务模型对应的行为

图片 10

定义拦截描述时,指定过滤拦截哪些类、哪些方法,然后,在这些行为的点上,可以埋入探针、处理器。重写visitCode、visitInsn、visitMaxs分别实现方法进入、返回、异常的相关操作改写。

图片 11

四、透视宝Java监控部署流程

1、登录云智慧透视宝官网:
,点击页面右上角导航的“免费试用”,正确填写免费试用的申请信息后会弹出下面的对话框,同时激活邮件会自动发送到你的邮箱中,按照流程注册帐号即可。

图片 12

2、注册成功后,登录透视宝,点击配置-应用,在配置页面中下载安装Smart
Agent。安装成功后,Smart
Agent会根据系统配置自动获取主机信息,大致两分钟后,您就可以在“主机→服务器”模块中查看该服务器的CPU、内存、网卡、磁盘及进程等性能数据。

图片 13

3、如果要监控应用运行时代码、主机中服务和数据库性能数据,您需要进一步安装和配置Smart
Agent提供的各种插件,这是因为Smart
Agent实现了一种开放式的插件式结构,对每个运行时代码、服务和数据库的监控都是通过相应的插件来实现的。

图片 14

Smart
Agent在安装完成后,加载过程中自动发现你的应用组件,如果没有自动监测到Java环境,也可以手动添加Java
Agent。如上图所示,点击“管理”入口,进入“插件管理”,点击页面下部的“添加服务”,选择JavaAgent后,点击“创建”。

创建完毕后,点击“ON”。(该ON操作只是初始化用户的信息,以便以后采集到的信息能够正确的回传给该用户。)

图片 15

以上都操作完后,在{smartagent的安装路径}/plugins,就会看到如下形式的

图片 16

在到{smartagent的安装路径}/plugins/JavaAgent_1442476463X1002x0/conf文件夹下,查看app.conf文件,看看该文件内的HostKey的值是否是如下类似的加密形式

图片 17

以上情况,表明JavaAgent已经下载启动初始化成功。

4、安装JavaAgent至各种应用服务器上,如tomcat\jboss\weblogic。(该操作参考官网)

5、只要启动相应服务器,然后访问您的应用url即可,该url对用的代码执行情况即可呈现给您,一旦出现缓慢问题也一目了然。如下图示意

图片 18

五、透视宝Java代码性能监控特点

在功能方面,透视宝无论是在Java,还是其他如.NET、PHP等主流语言的监控上,都包括:查看执行最慢的10个元素,包括元素执行次数、持续时长和占用时长百分比;查看HTTP请求参数,包括请求的响应状态、链接页面、具体的请求参数及返回结果;查看代码执行堆栈的详细树状信息,包括每个方法的计算时间、总耗时和被调用的次数,您能直接看到特殊标识的最慢方法;查看涉及SQL语句的总耗时排序,包括SQL执行总耗时、执行次数和具体的查询语句;第三方API调用。

图片 19

端到端性能监控示意图

在性能方面,云智慧透视宝的JavaAgent代码监控探针包,对用户的性能影响到底有多大?从安装包本身来看,它非常小,仅为1.5M。在不安装Java探针包和安装Java探针包,分别运行应用。经过测试对比,CPU使用率差值、内存消耗差值、TPS差值均在5%以内。

一、Java平台体系及应用场景 从1995年Sun
Microsystems公司正式推出Java,到2006年时Sun公司将其开源,迄…

干货 | 云智慧透视宝Java代码性能监控实现原理,云智慧java

这篇图文并茂,高端大气上档次,思维缜密的文章,一看就和我平时的风格不同。对了。这不是我写的,是我家写一手好代码,炒一手好菜的男神架构师老公的大作,曾发表于技术公号,经本人授权转载,如有技术问题,我代为请他本人解答~~

涉及知识点:APM, java Agent, plugin, bytecode, asm, InvocationHandler,
smail

【干货分享】监控宝Docker监控实现原理

分享人高驰涛(Neeke),云智慧高级架构师,PHP 开发组成员,同时也是
PECL/SeasLog 的作者。8
年研发管理经验,早期从事大规模企业信息化研发架构,09
年涉足互联网数字营销领域并深入研究架构与性能优化。2014
年加入云智慧,致力于 APM 产品的架构与研发。崇尚敏捷,高效,GettingReal。

图片 20

  2015年9月,企业级应用性能监控和管理服务商云智慧正式上线了Docker监控功能,能够实时监控Docker容器的CPU、内存、网络流量及Swap状态,让开发者和运维人员在使用Docker时清晰掌握其资源消耗状况。

图片 21

  作为国内首家实现Docker监控的SaaS厂商,监控宝Docker监控的技术原理是什么?相对国外的Docker监控产品有何优势?以下是此次分享的实录,请听Neeke细说端详:

  1、Docker监控概况

  在云时代,仍有大量物理机直接支持服务,相较于虚拟技术来讲,这种方式已经落伍很多,于是各种开源容器技术大大推进了虚拟化技术的发展。

  Docker容器相较于其他容器技术来讲,是比较新的,而且发展最为迅速。原因不用多说,背后有老大哥谷歌撑腰。国内也已经兴起了几个以Docker为核心技术的创业公司,比如云智慧的合作伙伴数人、DaoCloud,都是前景非常赞的公司。

  虽然这么火热,但关于Docker的运维一直是个痛点。

  可以说,目前全球只有两家APM厂商提供了基于SaaS的Docker运维监控,其一是美国APM厂商New
Relic,他们在6月下旬正式发布了Docker监控;另一家,则是中国APM厂商云智慧CloudWise,在继New
Relic之后的9月7日,发布上线了Docker监控。从某种意义上讲,CloudWise填补了国内Docker监控的SaaS服务空白。

  2、Docker监控的工作原理

  大家都知道,CloudWise在APM领域率先提出了端到端的一体化监控模型,并且在此模型上,发布了技术领先、便于部署和管理的SmartAgent软件架构。此次Docker监控的实现,也是基于SmartAgent的架构来完成的。

  SmartAgent以部署的快捷高效和智能化见长,整个部署过程中,用户在两分钟内便可完成。部署分为两部,首先下载、解压、启动数据发送代理SendProxy。SendProxy的作用是提供一个高效的本地数据接收队列与数据发送引擎,并且可以在局域网内进行分布式部署,使得不能上网的机器监控也可正常地通过SendProxy高效地传输到云智慧的SaaS平台。其次,下载、解压、启动DockerAgent。

  DockerAgent使用Python进行开发并完成编译,目前支持Ubuntu和CentOS。DockerAgent遵循了SmartAgent的插件规范,所以,无论监控宝或透视宝用户,都可以直接使用。

  DockerAgent有三个线程,分别是:DockerProcess \ DockerConfig \
DockerPing,以及一个对象Task。三个线程各司其职,同时受Task对象控制。Task中核心属性是任务惟一标识、任务状态以及任务频率。这些属性由DockerConfig与ClouwWise云平台定时同步。

  当任务状态正常时,DockerProcess线程开始采集数据,并遵守频率规范。DockerPing负责心跳检测,定时产生心跳数据。这些数据,都由DockerAgent交由SendProxy,并由SendProxy存储进入队列,并异步地推送至CloudWise云平台。

  前面聊到DockerAgent插件遵守了SmartAgent的插件规范,所以它像其他插件一样,包含了
bin 、conf、lib、log等目录,并存在一个启动脚本。该脚本提供了start
、stop、status等命令。

  以上是DockerAgent的介绍,后续SmartAgent的架构与插件规范将会陆续开源发布,届时热衷开源与监控的同学,都可以直接参与进来。3、DockerAgent数据采集原理

  下面我们聊一下DockerAgent采集数据的原理。DockerAgent首先会使用docker
info命令来获取docker系统信息,这些信息包含了非常有用的数据,如:
Containers, Images, Name, CPUs, Data Space Used, Data Space Total, Total
Memory。

  这些数据看似简单基础,但却可以解脱掉Docker运维同学每天重复N次的工作。其次会使用docker
version来检测docker版本,目前我们的DockerAgent仅支持1.15以上的Docker版本。

图片 22

  然后,使用dockerps命令来取得容器的运行信息和容器id,容器name,此时便可获知在此台机器上正在运行的docker容器都有哪些。

  最后,依次取得这些docker容器的性能指标。取得性能指标的方式,有部分使用docker原生接口,有部分是运行云智慧自己的算法。其中包含容器与主机的系统时区/时间;容器的cpu使用率(通过cgroup/cpuacct内该容器的cpuacct.stat取得);容器的ip;容器内运行的进程数;容器的内存指标,rss\cache\memory_limit\total_cwop等(通过cgroup/memory内该容器的
memory.stat取得);容器的网络指标(通过ifconfig/
statistics取得)。DockerAgent发布上线以后,在当天就接到了非常多热心用户的反馈。很多反馈非常好,我们也在积极地吸收和改进。为大家解决真正头疼的Docker运维、监控、管理问题。相信在很短的时间内,将迭代出更优秀、更稳定、更符合用户预期的DockerAgent,以此不仅填补国内的Docker监控空白,更会真正成为众多Docker用户、企业的伙伴,为大家解决真正头疼的Docker运维、监控问题。

  问:咱们和datadog之类docker监控有啥区别和优势?

  答:DataDog的安装部署太过繁琐。当时尝试时用了一下午才跑出来数据。DataDog的图表定义比较自由,这点是比较好的;而我们的
Docker监控最大的优势,就是零基础部署。另外,DataDog太贵,好像一个Agent要接近100人民币吧。目前CloudWise的
DockerAgent完全免费。

  问:刚才说 docherconfig 是定时与云平台同步,同步的是docker
process和docker ping采集到的数据吗?

  答:不是同步采集到的数据,是同步配置。

  问:我看讲的是通过sendproxy异步到云平台的啊,那么dockerconfing的作用是什么?

  答:DockerConfig是定时从云平台取得配置信息,采集到的数据,是由DockerProcess与DockerPing自行交由SendProxy。同步的数据其实就是Task的属性,比如任务名、任务频率、任务状态。

  问:采集数据原理是先ps命令机器上那些 docker容器,再去用docker
info获得他们的指标吗?

  答:dockerinfo是返回当前机器上整体的docker指标,然后ps取得活着的docker容器,依次取它们各自的指标。

  问:那包括了ps命令出的docker吗?ps直接就取了吗?这么说ps不仅仅是获取那些活的docker容器,还包括他们指标?

  答:ps取不到指标,取得的是活的容器并列举;然后用其他的方法取它们的指标。容器名字也是ps时列举时一起取得的。

  以上是Neeke就监控宝Docker监控的实现原理进行的分享,大家可以注册监控宝进行免费试用,有任何问题或需求请与我们联系。

分享人高驰涛(Neeke),云智慧高级架构师,PHP 开发组成员,同时也是
PECL/SeasLog 的作者。8 年研发管理…

一、Java平台体系及应用场景

从1995年Sun
Microsystems公司正式推出Java,到2006年时Sun公司将其开源,迄今为止已经有了20年的历史。Java本身已不仅仅只是一门面向对象的编程语言,而是由一系列计算机软件和规范形成的技术体系,这个技术体系提供了完整的跨平台开发与部署的支持,实现“一次编写、到处运行”的目的。Java已经广泛的应用于嵌入式、移动终端、企业服务器、大型机等各种场合。

Sun官方所定义的Java技术体系包括如下几个组成部分:
* Java程序设计语言
* 各种硬件平台上的Java虚拟机
* Class文件格式
* 来自商业机构和开源社区的第三方Java类库

图片 23

 

图:Java技术体系组件图

Java Virtual
Machine(JVM)是Java体系的基础,负责解释、编译执行.class文件形式的字节码,同时负责内存管理、热点代码检测和运行时编译优化。正是由于有了虚拟机的基础,才使Java实现了“一次编写、到处运行”。Java这20年的发展,其实更是虚拟机的发展过程。期间经历了Sun、BEA公司各自开发的虚拟机,2009年之后,ORACLE将这两家公司收购,并将这些虚拟机取长补短、合二为一。目前还是开源的虚拟机OpenJDK,可供爱好者学习研究用。

JRE部分是支持Java程序运行的标准环境。JDK是JRE的超集,包含JRE的一切,再加上工具如编译器、调试器等。

 

APM : 应用程序性能管理。 2011年时国外的APM行业 NewRelic 和 APPDynamics
已经在该领域拔得头筹,国内近些年来也出现一些APM厂商,如: 听云,
OneAPM, 博睿 云智慧,阿里百川码力。
(据分析,国内android端方案都是抄袭NewRelic公司的,由于该公司的sdk未混淆,业界良心)

二、Java性能监控需求

对于一个企业的应用系统,大多数情况下,肯定是由多种编程语言开发的各种系统的集成。我们都非常关心系统的可用性、及时响应性、资源的消耗,比如CPU、内存、各种I/O、网路带宽等消耗情况。对于这些问题的性能瓶颈点,我们一般可以归纳为外部服务(如第三方API)、资源读写、代码异常。如果在发生这些问题时,能够及时完整的抓拍记录保留下来,那么对于我们解决问题将会提供充足的证据,解决问题会变的非常容易。

对于Java应用系统来说,JVM自身提供了相应的性能监控手段和工具,经常在出现问题后,比如内存泄漏或溢出时,我们会通过jmap命令导出堆的转储快照,利用相应的命令jhat或其他相应的第三方内存分析工具来分析对象的占用情况。

响应缓慢时,我们可能会用jstat监视命令、或jdk的可视化工具jconsole、visualvm来分析JVM的垃圾回收类型、回收频率,来推测是否是垃圾回收导致的。有可能我们还要接着分析线程转储快照,通过jstack取出线程的栈快照,来分析是否有真死锁、死循环导致的相应缓慢、资源负载高等情况。

当有问题出现时,许多开发人员可能都是比较盲目的用这些工具来试探性定位问题,而大多数情况下,这种试探会无功而返。因为这些分析工具主要是侧重Java单方面的分析,比如该系统调用第三方API,如果第三方API有问题,是无法监控到的。还有像文件、DB资源的访问也是是无法监控到的。

而且,只有对Java虚拟机机制较为熟悉的高级开发人员才能比较好的运用、理解这些工具,对于大多数普通Java开发人员来说,这些问题只会令他们束手无策。

像外部服务(如第三方API)、资源读写、代码异常这些瓶颈点,需要通过代码级别的监控才能直接、快速、有效的找到症结所在。调用第三方API的耗时、资源访问的耗时、代码抛出的非预知异常,这些常见问题代码监控完全能够监控到,并能够实时抓拍记录,一旦有问题可以快速还原事故问题现场。通过代码级别监控发现问题后,也可以在辅助利用虚拟机内置监控工具进行进一步的定位。

 

能做什么:
crash监控,卡顿监控,内存监控,增加trace,网络性能监控,app页面自动埋点,等。

三、透视宝Java监控实现原理

图片 24

 

图:Java的执行模型

 

在Java的执行体系中,由.Java源码文件编译后的.class字节码文件,可以理解为中间语言。

图片 25

 

图:透视宝Java监控实现原理

 

图片 26

 

图:透视宝Java监控实例运行图

 

1、字节码load至JVM时发生了什么

* 回调函数注册完毕后,凡是当有任何的class文件即将被类加载器加载前,都

会执行回调函数transform,在此方法内实现的类改变操作。

* 实现的transform方法中,我们使用的是ASM字节码操作框架,ASM从二进制

形式的类文件中读取、分析类的信息,然后修改改变类的行为。

* transform方法的基本代码形式如下:

图片 27

 

2、如何实际改变类行为

* 在依赖于ASM基础之上,我们抽象出这样的业务模型

图片 28

 

* 常用的拦截探针

图片 29

 

* 常用的运行时拦截处理器

图片 30

 

* 支持的拦截定义过滤器规则

图片 31

 

* 该业务模型对应的行为

图片 32

 

定义拦截描述时,指定过滤拦截哪些类、哪些方法,然后,在这些行为的点上,可以埋入探针、处理器。重写visitCode、visitInsn、visitMaxs分别实现方法进入、返回、异常的相关操作改写。

图片 33

 

性能监控其实就是hook
代码到项目代码中,从而做到各种监控。常规手段都是在项目中增加代码,但如何做到非侵入式的,即一个sdk即可。

四、透视宝Java监控部署流程

1、登录云智慧透视宝官网,点击页面右上角导航的“免费试用”,正确填写免费试用的申请信息后会弹出下面的对话框,同时激活邮件会自动发送到你的邮箱中,按照流程注册帐号即可。

图片 34

 

2、注册成功后,登录透视宝,点击配置-应用,在配置页面中下载安装Smart
Agent。安装成功后,Smart
Agent会根据系统配置自动获取主机信息,大致两分钟后,您就可以在“主机→服务器”模块中查看该服务器的CPU、内存、网卡、磁盘及进程等性能数据。

图片 35

 

3、如果要监控应用运行时代码、主机中服务和数据库性能数据,您需要进一步安装和配置Smart
Agent提供的各种插件,这是因为Smart
Agent实现了一种开放式的插件式结构,对每个运行时代码、服务和数据库的监控都是通过相应的插件来实现的。

图片 36

 

Smart
Agent在安装完成后,加载过程中自动发现你的应用组件,如果没有自动监测到Java环境,也可以手动添加Java
Agent。如上图所示,点击“管理”入口,进入“插件管理”,点击页面下部的“添加服务”,选择JavaAgent后,点击“创建”。

创建完毕后,点击“ON”。(该ON操作只是初始化用户的信息,以便以后采集到的信息能够正确的回传给该用户。)

图片 37

 

以上都操作完后,在{smartagent的安装路径}/plugins,就会看到如下形式的

图片 38

 

在到{smartagent的安装路径}/plugins/JavaAgent_1442476463X1002x0/conf文件夹下,查看app.conf文件,看看该文件内的HostKey的值是否是如下类似的加密形式

图片 39

 

以上情况,表明JavaAgent已经下载启动初始化成功。

4、安装JavaAgent至各种应用服务器上,如tomcat\jboss\weblogic。(该操作参考官网

5、只要启动相应服务器,然后访问您的应用url即可,该url对用的代码执行情况即可呈现给您,一旦出现缓慢问题也一目了然。如下图示意

图片 40

 

五、透视宝Java代码性能监控特点

在功能方面,透视宝无论是在Java,还是其他如.NET、PHP等主流语言的监控上,都包括:查看执行最慢的10个元素,包括元素执行次数、持续时长和占用时长百分比;查看HTTP请求参数,包括请求的响应状态、链接页面、具体的请求参数及返回结果;查看代码执行堆栈的详细树状信息,包括每个方法的计算时间、总耗时和被调用的次数,您能直接看到特殊标识的最慢方法;查看涉及SQL语句的总耗时排序,包括SQL执行总耗时、执行次数和具体的查询语句;第三方API调用。

图片 41

 

端到端性能监控示意图

 

在性能方面,云智慧透视宝的JavaAgent代码监控探针包,对用户的性能影响到底有多大?从安装包本身来看,它非常小,仅为1.5M。在不安装Java探针包和安装Java探针包,分别运行应用。经过测试对比,CPU使用率差值、内存消耗差值、TPS差值均在5%以内。

 

|
云智慧透视宝Java代码性能监控实现原理,云智慧java
这篇图文并茂,高端大气上档次,思维缜密的文章,一看就和我平时的风格不同…

1. 如何hook

切面编程-- AOP。我们的方案是AOP的一种,通过修改app
class字节码的形式将我们项目的class文件进行修改,从而做到嵌入我们的监控代码。

图片 42androidbuilder.jpg

通过查看Adnroid编译流程图,可以知道编译器会将所有class文件打包称dex文件,最终打包成apk。那么我们就需要在class编译成dex文件的时候进行代码注入。比如我想统计某个方法的执行时间,那我只需要在每个调用了这个方法的代码前后都加一个时间统计就可以了。关键点就在于编译dex文件时候注入代码,这个编译过程是由dx执行,具体类和方法为com.android.dx.command.dexer.Main#processClass。此方法的第二个参数就是class的byte数组,于是我们只需要在进入processClass方法的时候用ASM工具对class进行改造并替换掉第二个参数,最后生成的apk就是我们改造过后的了。

类:com.android.dx.command.dexer.Main

新的难点:
要让jvm在执行processClass之前先执行我们的代码,必须要对com.android.dx.command.dexer.Main(以下简称为dexer.Main)进行改造。如何才能达到这个目的?这时Instrumentation和VirtualMachine就登场了,参考第三节。

2. hook 到哪里

一期主要是网络性能监控。如何能截获到网络数据通过调研发现目前有下面集中方案:

  • root手机,通过adb 命令进行截获。
  • 建立vpn,将所有网络请求进行截获。
  • 参考听云,newrelic等产品,针对特定库进行代理截获。

也许还有其他的方式,需要继续调研。

目前我们参考newrelic等公司产品,针对特定网络请求库进行代理的的方式进行网络数据截获。比如okhtt3,
httpclient, 等网络库。

In general, a javaagent is a JVM “plugin”, a specially crafted .jar
file, that utilizes the Instrumentation API that the JVM provides.

由于我们要修改Dexer 的Main类, 而该类是在编译时期由java虚拟机启动的,
所以我们需要通过agent来修改dexer Main类。

javaagent的主要功能如下:

  • 可以在加载class文件之前作拦截,对字节码做修改
  • 可以在运行期对已加载类的字节码做变化

JVMTI:JVM Tool
Interface,是JVM暴露出来的一些供用户扩展的接口集合。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口,这些接口可以供开发者扩展自己的逻辑。

instrument agent: javaagent功能就是它来实现的,另外instrument
agent还有个别名叫JPLISAgent(Java Programming Language Instrumentation
Services
Agent),这个名字也完全体现了其最本质的功能:就是专门为Java语言编写的插桩服务提供支持的。

两种加载agent的方式:

  • 在启动时加载,
    启动JVM时指定agent类。这种方式,Instrumentation的实例通过agent
    class的premain方法被传入。
  • 在运行时加载,JVM提供一种当JVM启动完成后开启agent机制。这种情况下,Instrumention实例通过agent代码中的的agentmain传入。

参考例子instrumentation 功能介绍(javaagent)

有了javaagent, 我们就可以在编译app时重新修改dex
的Main类,对应修改processClass方法。

如何修改class文件?
我们需要了解java字节码,然后需要了解ASM开发。通过ASM编程来修改字节码,从而修改class文件。(也可以使用javaassist来进行修改)

在介绍字节代码指令之前,有必要先来介绍 Java 虚拟机执行模型。我们知道,Java
代码是
在线程内部执行的。每个线程都有自己的执行栈,栈由帧组成。每个帧表示一个方法调用:每次
调用一个方法时,会将一个新帧压入当前线程的执行栈。当方法返回时,或者是正常返回,或者
是因为异常返回,会将这个帧从执行栈中弹出,执行过程在发出调用的方法中继续进行(这个方
法的帧现在位于栈的顶端)。

每一帧包括两部分:一个局部变量部分和一个操作数栈部分。局部变量部分包含可根据索引
以随机顺序访问的变量。由名字可以看出,操作数栈部分是一个栈,其中包含了供字节代码指令
用作操作数的值。

字节代码指令
字节代码指令由一个标识该指令的操作码和固定数目的参数组成:

  • 操作码是一个无符号字节值——即字节代码名
  • 参数是静态值,确定了精确的指令行为。它们紧跟在操作码之后给出.比如GOTO标记
    指令(其操作码的值为
    167)以一个指明下一条待执行指令的标记作为参数标记。不要
    将指令参数与指令操作数相混淆:参数值是静态已知的,存储在编译后的代码中,而
    操作数值来自操作数栈,只有到运行时才能知道。

参考:

常见指令:

  • const 将什么数据类型压入操作数栈。
  • push 表示将单字节或短整型的常量压入操作数栈。
  • ldc 表示将什么类型的数据从常量池中压入操作数栈。
  • load 将某类型的局部变量数据压入操作数栈顶。
  • store 将操作数栈顶的数据存入指定的局部变量中。
  • pop 从操作数栈顶弹出数据
  • dup 复制栈顶的数据并将复制的值也压入栈顶。
  • swap 互换栈顶的数据
  • invokeVirtual 调用实例方法
  • invokeSepcial 调用超类构造方法,实例初始化,私有方法等。
  • invokeStatic 调用静态方法
  • invokeInterface 调用接口
  • getStatic
  • getField
  • putStatic
  • putField
  • New

查看demo:Java源代码

public static void print(String param) { System.out.println("hello " + param); new TestMain().sayHello();}public void sayHello() { System.out.println("hello agent");}

字节码

// access flags 0x9 public static print(Ljava/lang/String;)V GETSTATIC java/lang/System.out : Ljava/io/PrintStream; NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "hello " INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ALOAD 0 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V NEW com/paic/agent/test/TestMain DUP INVOKESPECIAL com/paic/agent/test/TestMain.<init> ()V INVOKEVIRTUAL com/paic/agent/test/TestMain.sayHello ()V RETURN public sayHello()V GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "hello agent" INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V RETURN

由于程序分析、生成和转换技术的用途众多,所以人们针对许多语言实现了许多用于分析、
生成和转换程序的工具,这些语言中就包括 Java 在内。ASM 就是为 Java
语言设计的工具之一, 用于进行运行时类生成与转换。于是,人们设计了
ASM1库,用于处理经过编译 的 Java 类。

ASM 并不是惟一可生成和转换已编译 Java
类的工具,但它是最新、最高效的工具之一,可 从
下载。其主要优点如下:

  • 有一个简单的模块API,设计完善、使用方便。
  • 文档齐全,拥有一个相关的Eclipse插件。
  • 支持最新的 Java 版本——Java 7。
  • 小而快、非常可靠。
  • 拥有庞大的用户社区,可以为新用户􏰁供支持。
  • 源许可开放,几乎允许任意使用。

图片 43ASM_transfer.png

核心类: ClassReader, ClassWriter, ClassVisitor

参考demo:

{ // print 方法的ASM代码 mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "print", "(Ljava/lang/String;)V", null, null); mv.visitCode(); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitTypeInsn(NEW, "java/lang/StringBuilder"); mv.visitInsn; mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false); mv.visitLdcInsn; mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitVarInsn; mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitTypeInsn(NEW, "com/paic/agent/test/TestMain"); mv.visitInsn; mv.visitMethodInsn(INVOKESPECIAL, "com/paic/agent/test/TestMain", "<init>", "()V", false); mv.visitMethodInsn(INVOKEVIRTUAL, "com/paic/agent/test/TestMain", "sayHello", "()V", false); mv.visitInsn; mv.visitEnd();}{ //sayHello 的ASM代码 mv = cw.visitMethod(ACC_PUBLIC, "sayHello", "()V", null, null); mv.visitCode(); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("hello agent"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitInsn; mv.visitEnd();}

VirtualMachine有个loadAgent方法,它指定的agent会在main方法前启动,并调用agent的agentMain方法,agentMain的第二个参数是Instrumentation,这样我们就能够给Instrumentation设置ClassFileTransformer来实现对dexer.Main的改造,同样也可以用ASM来实现。一般来说,APM工具包括三个部分,plugin、agent和具体的业务jar包。这个agent就是我们说的由VirtualMachine启动的代理。而plugin要做的事情就是调用loadAgent方法。对于Android
Studio而言,plugin就是一个Gradle插件。
实现gradle插件可以用intellij创建一个gradle工程并实现Plugin< Project
>接口,然后把tools.jar(在jdk的lib目录下)和agent.jar加入到Libraries中。在META-INF/gradle-plugins目录下创建一个properties文件,并在文件中加入一行内容“implementation-class=插件类的全限定名“。artifacs配置把源码和META-INF加上,但不能加tools.jar和agent.jar。(tools.jar
在 jdk中, 不过一般需要自己拷贝到工程目录中的,
agent.jar开发完成后放到plugin工程中用于获取jar包路径)。

agent的实现相对plugin则复杂很多,首先需要提供agentmain(String args,
Instrumentation
inst)方法,并给Instrumentation设置ClassFileTransformer,然后在transformer里改造dexer.Main。当jvm成功执行到我们设置的transformer时,就会发现传进来的class根本就没有dexer.Main。坑爹呢这是。。。前面提到了,执行dexer.Main的是dx.bat,也就是说,它和plugin根本不在一个进程里。

dx.bat其实是由ProcessBuilder的start方法启动的,ProcessBuilder有一个command成员,保存的是启动目标进程携带的参数,只要我们给dx.bat带上-javaagent参数就能给dx.bat所在进程指定我们的agent了。于是我们可以在执行start方法前,调用command方法获取command,并往其中插入-javaagent参数。参数的值是agent.jar所在的路径,可以使用agent.jar其中一个class类实例的getProtectionDomain().getCodeSource().getLocation.getPath()获得。可是到了这里我们的程序可能还是无法正确改造class。如果我们把改造类的代码单独放到一个类中,然后用ASM生成字节码调用这个类的方法来对command参数进行修改,就会发现抛出了ClassDefNotFoundError错误。这里涉及到了ClassLoader的知识。

关于ClassLoader的介绍很多,这里不再赘述。ProcessBuilder类是由Bootstrap
ClassLoader加载的,而我们自定义的类则是由AppClassLoader加载的。Bootstrap
ClassLoader处于AppClassLoader的上层,我们知道,上层类加载器所加载的类是无法直接引用下层类加载器所加载的类的。但如果下层类加载器加载的类实现或继承了上层类加载器加载的类或接口,上层类加载器加载的类获取到下层类加载的类的实例就可以将其强制转型为父类,并调用父类的方法。这个上层类加载器加载的接口,部分APM使用InvocationHandler。还有一个问题,ProcessBuilder怎么才能获取到InvocationHandler子类的实例呢?有一个比较巧妙的做法,在agent启动的时候,创建InvocationHandler实例,并把它赋值给Logger的treeLock成员。treeLock是一个Object对象,并且只是用来加锁的,没有别的用途。但treeLock是一个final成员,所以记得要修改其修饰,去掉final。Logger同样也是由Bootstrap
ClassLoader加载,这样ProcessBuilder就能通过反射的方式来获取InvocationHandler实例了。(详见:核心代码例子)

上层类加载器所加载的类是无法直接引用下层类加载器所加载的类的

层次 加载器
上层 BootStrapClassLoader ProcessBuilder
下层 AppClassLoader ProcessBuilderMethodVisitor操作的自定义类

这一句话的理解:
我们的目的是通过ProcessBuilderMethodVisitor将我们的代码写入ProcessBuilder.class中去让BootStrapClassLoader类加载器进行加载,而此时,
BootStrapClassLoader是无法引用到我们自定义的类的,因为我们自定义的类是AppClassLoader加载的。

但如果下层类加载器加载的类实现或继承了上层类加载器加载的类或接口,上层类加载器加载的类获取到下层类加载的类的实例就可以将其强制转型为父类,并调用父类的方法。

层次 加载器
上层 BootStrapClassLoader Looger
下层 AppClassLoader InvocationDispatcher

这句话的理解:
这里我们可以看到自定义类InvocationDispatcher是由AppClassLoader加载的,
我们在运行RewriterAgent(AppClassLoader加载)类时,通过反射的方式将InvocationDispatcher对象放入Looger(由于引用了Looger.class,所以此时logger已经被BootStrapClassLoader加载)类的treelock对象中,即下层类加载器加载的类实现了上层类加载器加载的类;当我们通过ProcessBuilderMethodVisitor类处理ProcessBuilder.class文件时,可以通过Logger提取成员变量,插入对应的调用逻辑。当运行到ProcessBuilder时,再通过这段代码动态代理的方式调用对应的业务。可以将其强制转型为父类,并调用父类的方法
,请参考
这里详细介绍了invokeInterface 和 invokeVirtual 的区别。

实现上我们目前主要做这两种, 一种是代码调用替换,
另一种是代码包裹返回。主要是提前写好对应规则的替换代码,
生成配置文件表, 在agent中visit每一个class代码,
遇到对应匹配调用时将进行代码替换。

ProcessBuilderMethodVisitor
DexClassTransformer#createDexerMainClassAdapter InvocationDispatcher
BytecodeBuilder

public BytecodeBuilder loadInvocationDispatcher() { this.adapter.visitLdcInsn(Type.getType(TransformConstant.INVOCATION_DISPATCHER_CLASS)); this.adapter.visitLdcInsn(TransformConstant.INVOCATION_DISPATCHER_FILED_NAME); this.adapter.invokeVirtual(Type.getType(Class.class), new Method("getDeclaredField", "(Ljava/lang/String;)Ljava/lang/reflect/Field;")); this.adapter.dup(); this.adapter.visitInsn(Opcodes.ICONST_1); this.adapter.invokeVirtual(Type.getType(Field.class), new Method("setAccessible", "; this.adapter.visitInsn(Opcodes.ACONST_NULL); this.adapter.invokeVirtual(Type.getType(Field.class), new Method("get", "(Ljava/lang/Object;)Ljava/lang/Object;")); return this; }

解析

顺序 指令 描述
8 InvocationDispatcher object invokeVirtual 调用get方法返回具体实例对象
7 null ACONST_NULL null 入栈
6 Field object invokeVirtual 调用setAccessible,改为可访问的,目前栈中只剩一个对象
5 true ICONST_1 1 即为true,入栈
4 Field object dup 拷贝一份,目前栈中只剩两个对象
3 Field object invokeVirtual 调用getDeclaredField 获取treeLock存储的Field
2 treelock ldc treelock 入栈
1 Logger.class Type ldc Logger.class type 入栈

WrapMethodClassVisitor#MethodWrapMethodVisitor

private boolean tryReplaceCallSite(int opcode, String owner, String name, String desc, boolean itf) { Collection<ClassMethod> replacementMethods = this.context.getCallSiteReplacements(owner, name, desc); if (replacementMethods.isEmpty { return false; } ClassMethod method = new ClassMethod(owner, name, desc); Iterator<ClassMethod> it = replacementMethods.iterator(); if (it.hasNext { ClassMethod replacementMethod = it.next(); boolean isSuperCallInOverride = (opcode == Opcodes.INVOKESPECIAL) && !owner.equals(this.context.getClassName && this.name.equals && this.desc.equals; //override 方法 if (isSuperCallInOverride) { this.log.info(MessageFormat.format("[{0}] skipping call site replacement for super call in overriden method : {1}:{2}", this.context.getFriendlyClassName(), this.name, this.desc)); return false; } Method originMethod = new Method(name, desc); //处理init方法, 构造对象, 调用替换的静态方法来替换init。 if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { //调用父类构造方法 if (this.context.getSuperClassName() != null && this.context.getSuperClassName().equals { this.log.info(MessageFormat.format("[{0}] skipping call site replacement for class extending {1}", this.context.getFriendlyClassName(), this.context.getFriendlySuperClassName; return false; } this.log.info(MessageFormat.format("[{0}] tracing constructor call to {1} - {2}", this.context.getFriendlyClassName(), method.toString); //开始处理创建对象的逻辑 //保存参数到本地 int[] arguments = new int[originMethod.getArgumentTypes().length]; for (int i = arguments.length -1 ; i >= 0; i--) { arguments[i] = this.newLocal(originMethod.getArgumentTypes; this.storeLocal(arguments[i]); } //由于init 之前会有一次dup,及创建一次, dup一次, 此时如果执行了new 和 dup 操作树栈中会有两个对象。 this.visitInsn(Opcodes.POP); if (this.newInstructionFound && this.dupInstructionFound) { this.visitInsn(Opcodes.POP); } //载入参数到操作数栈 for (int arg : arguments) { this.loadLocal; } //使用要替换的方法,执行静态方法进行对象创建 super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc; //如果此时才调用了dup,也需要pop, (这一部分的场景暂时还没有构造出来, 上面的逻辑为通用的) if (this.newInstructionFound && !this.dupInstructionFound) { this.visitInsn(Opcodes.POP); } } else if (opcode == Opcodes.INVOKESTATIC) { //替换静态方法 this.log.info(MessageFormat.format("[{0}] replacing call to {1} with {2}", this.context.getFriendlyClassName(), method.toString(), replacementMethod.toString; super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc; } else { // 其他方法调用, 使用新方法替换旧方法的调用。 先判断创建的对象是否为null, Method newMethod = new Method(replacementMethod.getMethodName(), replacementMethod.getMethodDesc; this.log.info(MessageFormat.format("[{0}] replacing call to {1} with {2}", this.context.getFriendlyClassName(), method.toString(), replacementMethod.toString; //从操作数栈上取原始参数类型到本地变量中 int[] originArgs = new int[originMethod.getArgumentTypes().length]; for (int i = originArgs.length -1 ; i >= 0; i--) { originArgs[i] = this.newLocal(originMethod.getArgumentTypes; this.storeLocal(originArgs[i]); } //操作数栈中只剩操作对象了, 需要dup, 拷贝一份作为检查新method的第一个参数。 this.dup(); //检查操作数栈顶对象类型是否和新method的第一个参数一致。 this.instanceOf(newMethod.getArgumentTypes; Label isInstanceOfLabel = new Label(); //instanceof 结果不等于0 则跳转到 isInstanceofLabel,执行替换调用 this.visitJumpInsn(Opcodes.IFNE, isInstanceOfLabel); //否则执行原始调用 for (int arg : originArgs) { this.loadLocal; } super.visitMethodInsn(opcode, owner, name, desc, itf); Label endLabel = new Label(); //跳转到结束label this.visitJumpInsn(Opcodes.GOTO, endLabel); this.visitLabel(isInstanceOfLabel); //处理替换的逻辑 //load 参数, 第一个为 obj, 后面的为原始参数 this.checkCast(newMethod.getArgumentTypes; for (int arg: originArgs) { this.loadLocal; } super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc; //结束 this.visitLabel; } this.context.markModified(); return true; } return false; }

解析 详细见tryReplaceCallSite注释即可。

8. 验证

将生成的apk反编译,查看class
字节码。我们一般会通过JD-GUI来查看。我们来查看一下sample生成的结果:

private void testOkhttpCall() { OkHttpClient localOkHttpClient = new OkHttpClient.Builder; Object localObject = new Request.Builder().url("https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey"); if (!(localObject instanceof Request.Builder)) { localObject = ((Request.Builder)localObject).build(); if ((localOkHttpClient instanceof OkHttpClient)) { break label75; } } label75: for (localObject = localOkHttpClient.newCalllocalObject);; localObject = OkHttp3Instrumentation.newCall((OkHttpClient)localOkHttpClient, localObject)) { localObject).enqueue(new Callback() { public void onFailure(Call paramAnonymousCall, IOException paramAnonymousIOException) { } public void onResponse(Call paramAnonymousCall, Response paramAnonymousResponse) throws IOException { } }); return; localObject = OkHttp3Instrumentation.build((Request.Builder)localObject); break; } }

上面的代码估计没有几个人能够看懂,
尤其for循环里面的逻辑。其实是由于不同的反编译工具造成的解析问题导致的,所以看起来逻辑混乱,无法符合预期。

想用查看真实的结果,
我们来看下反编译后的smail。详细smail指令参考

.method private testOkhttpCall()V .locals 6 .prologue .line 35 const-string v3, "https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey" .line 36 .local v3, "url":Ljava/lang/String; new-instance v4, Lokhttp3/OkHttpClient$Builder; invoke-direct {v4}, Lokhttp3/OkHttpClient$Builder;-><init>()V invoke-virtual {v4}, Lokhttp3/OkHttpClient$Builder;->build()Lokhttp3/OkHttpClient; move-result-object v1//new OkHttpClient.Builder; 即为okhttpclient,放到 v1 中 .line 37 .local v1, "okHttpClient":Lokhttp3/OkHttpClient; new-instance v4, Lokhttp3/Request$Builder; invoke-direct {v4}, Lokhttp3/Request$Builder;-><init>()V invoke-virtual {v4, v3}, Lokhttp3/Request$Builder;->url(Ljava/lang/String;)Lokhttp3/Request$Builder; move-result-object v4 //new Request.Builder().url执行了这一段语句,将结果放到了v4中。 instance-of v5, v4, Lokhttp3/Request$Builder; if-nez v5, :cond_0 invoke-virtual {v4}, Lokhttp3/Request$Builder;->build()Lokhttp3/Request; move-result-object v2 .line 38 .local v2, "request":Lokhttp3/Request; //判断v4中存储的是否为Request.Builder类型,如果是则跳转到cond_0, 否则执行Request.Builder.build()方法,将结果放到v2中. :goto_0 instance-of v4, v1, Lokhttp3/OkHttpClient; if-nez v4, :cond_1 invoke-virtual {v1, v2}, Lokhttp3/OkHttpClient;->newCall(Lokhttp3/Request;)Lokhttp3/Call; move-result-object v0 .line 39 .end local v1 # "okHttpClient":Lokhttp3/OkHttpClient; .local v0, "call":Lokhttp3/Call; //goto_0 标签:判断v1 中的值是否为 OKHttpclient 类型, 如果是跳转为cond_1 , 否则调用OKHttpclient.newCall, 并将结果放到v0 中。 :goto_1 new-instance v4, Lcom/paic/apm/sample/MainActivity$1; invoke-direct {v4, p0}, Lcom/paic/apm/sample/MainActivity$1;-><init>(Lcom/paic/apm/sample/MainActivity;)V invoke-interface {v0, v4}, Lokhttp3/Call;->enqueue(Lokhttp3/Callback;)V .line 51 return-void //goto_1 标签: 执行 v0.enqueue(new Callback;并return; .line 37 .end local v0 # "call":Lokhttp3/Call; .end local v2 # "request":Lokhttp3/Request; .restart local v1 # "okHttpClient":Lokhttp3/OkHttpClient; :cond_0 check-cast v4, Lokhttp3/Request$Builder; invoke-static {v4}, Lcom/paic/agent/android/instrumentation/okhttp3/OkHttp3Instrumentation;->build(Lokhttp3/Request$Builder;)Lokhttp3/Request; move-result-object v2 goto :goto_0 //cond_0:标签: 执行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build, 并将结果放到v2中,并goto 到 goto_0 .line 38 .restart local v2 # "request":Lokhttp3/Request; :cond_1 check-cast v1, Lokhttp3/OkHttpClient; .end local v1 # "okHttpClient":Lokhttp3/OkHttpClient; invoke-static {v1, v2}, Lcom/paic/agent/android/instrumentation/okhttp3/OkHttp3Instrumentation;->newCall(Lokhttp3/OkHttpClient;Lokhttp3/Request;)Lokhttp3/Call; move-result-object v0 goto :goto_1 //cond_1 标签: 执行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall, 并将结果放到v0中, goto 到goto_1 .end method

解析后的伪代码

String v3 = "https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey";object v1 = new OkhttpClient.Builder;object v4 = new Reqeust.Builder;object v2 ;object v0 ;if (v4 instanceof Request.Builder) { cond_0: v2 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build; } else { v2 = (Request.Builder)v4.build();}goto_0:if (v1 instanceof OkHttpClient) { cond_1: v0 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall;} else { v0 = v1.newCall; // v0 is Call}goto_1:v4 = new Callback();v0.enqueue;return;

查看伪代码, 符合预期结果。验证完毕。

相关文章