Android端代码染色原理及技术实践
- 支持Ant和Gradle打包方式,可以自由切换。
- 支持离线模式,更贴合SDK的使用场景。
- JaCoCo文档比较全面,还在持续维护,有问题便于解决。
//原始java方法
public static int Test1(int a, int b) {
int c = a + b;
int d = c + a;
return d;
}
//--------------------------我是分割线--------------------------------------------//
//jacoco处理后的方法
private static transient /* synthetic */ boolean[] $jacocoData;
public static int Test1(final int a, final int b) {
final boolean[] $jacocoInit = $jacocoInit();
final int c = a + b;
final int n;
final int d = n = c + a;
$jacocoInit[3] = true;
return n;
}
private static boolean[] $jacocoInit() {
boolean[] $jacocoData;
if (($jacocoData = TestInstrument.$jacocoData) == null) {
$jacocoData = (TestInstrument.$jacocoData =
Offline.getProbes(-6846167369868599525L,
"com/jacoco/test/TestInstrument", 4));
}
return $jacocoData;
}
ALOAD probearray
xPUSH probeid
ICONST_1
BASTORE
- 统计方法的执行情况。
- 统计分支语句的执行情况。
- 统计普通代码块的执行情况。
- 方法尾加: 能说明方法被执行过, 且说明了探针上面的方法被执行了,但是这种处理比较麻烦, 可能有多个return或者throw。
- 方法头加: 处理简单, 但只能说明方法有进去过。
public void visitInsn(final int opcode) {
switch (opcode) {
case Opcodes.IRETURN:
case Opcodes.LRETURN:
case Opcodes.FRETURN:
case Opcodes.DRETURN:
case Opcodes.ARETURN:
case Opcodes.RETURN:
case Opcodes.ATHROW:
probesVisitor.visitInsnWithProbe(opcode, idGenerator.nextId());
break;
default:
probesVisitor.visitInsn(opcode);
break;
}
}
- 无条件Jump (goto)
//源码
public static void Test4(int a) {
if(a>10){
a=a+10;
}
a=a+12;
}
//jacoco处理后的字节码
public static void Test4(int a) {
boolean[] var1 = $jacocoInit();
if (a <= 10) {
var1[11] = true;
} else {
a += 10;
var1[12] = true;
}
a += 12;
var1[13] = true;
}
//源码,if有多个条件
public static void Test5(int a,int b) {
if(a>10 || b>10){
a=a+10;
}
a=a+12;
}
//jacoco处理后的字节码。
public static void Test5(int a, int b) {
boolean[] var2;
label15: {
var2 = $jacocoInit();
if (a > 10) {
var2[14] = true;
} else {
if (b <= 10) {
var2[15] = true;
break label15;
}
var2[16] = true;
}
a += 10;
var2[17] = true;
}
a += 12;
var2[18] = true;
}
public static void Test6(int a, int b) {
boolean[] var2 = $jacocoInit();
a += b;
b = a + a;
var2[19] = true;
Test();
int var10000 = a + b;
var2[20] = true;
Test();
var2[21] = true;
}
@Override
public void visitLabel(final Label label) {
if (LabelInfo.needsProbe(label)) {
if (tryCatchProbeLabels.containsKey(label)) {
probesVisitor.visitLabel(tryCatchProbeLabels.get(label));
}
probesVisitor.visitProbe(idGenerator.nextId());
}
probesVisitor.visitLabel(label);
}
public static boolean needsProbe(final Label label) {
final LabelInfo info = get(label);
return info != null && info.successor
&& (info.multiTarget || info.methodInvocationLine);
}
boolean successor = false;//默认是false
boolean first = true; //默认是true
@Override
public void visitJumpInsn(final int opcode, final Label label) {
LabelInfo.setTarget(label);
if (opcode == Opcodes.JSR) {
throw new AssertionError("Subroutines not supported.");
}
//如果是GOTO指令,successor=false,表示前后两条指令是断开的。
successor = opcode != Opcodes.GOTO;
first = false;
}
@Override
public void visitInsn(final int opcode) {
switch (opcode) {
case Opcodes.RET:
throw new AssertionError("Subroutines not supported.");
case Opcodes.IRETURN:
case Opcodes.LRETURN:
case Opcodes.FRETURN:
case Opcodes.DRETURN:
case Opcodes.ARETURN:
case Opcodes.RETURN:
case Opcodes.ATHROW:
successor = false; //return或者throw,表示两条指令是断开的
break;
default:
successor = true; //普通指令的话,表示前后两条指令是连续的
break;
}
first = false;
}
@Override
public void visitLabel(final Label label) {
if (first) {
LabelInfo.setTarget(label);
}
if (successor) {//这里设置当前指令是不是上一条指令的继任者,
//源码中,只有这一个地方地方会触发这个条件赋值,也就是访问每个label的第一条指令。
LabelInfo.setSuccessor(label);
}
}
@Override
public void visitLineNumber(final int line, final Label start) {
lineStart = start;
}
@Override
public void visitMethodInsn(final int opcode, final String owner,
final String name, final String desc, final boolean itf) {
successor = true;
first = false;
markMethodInvocationLine();
}
private void markMethodInvocationLine() {
if (lineStart != null) {
//lineStart就是当前这个Lable
LabelInfo.setMethodInvocationLine(lineStart);
}
}
LabelInfo.java类
public static void setMethodInvocationLine(final Label label) {
create(label).methodInvocationLine = true;
}
public void visitJumpInsn(final int opcode, final Label label) {
LabelInfo.setTarget(label);//Jump语句 将Lable标记一次为true
if (opcode == Opcodes.JSR) {
throw new AssertionError("Subroutines not supported.");
}
successor = opcode != Opcodes.GOTO;
first = false;
}
//如果当设置它是否是上一条指令的后续指令时,再一次设置它为multiTarget=true,表示至少有2个来源
public static void setSuccessor(final Label label) {
final LabelInfo info = create(label);
info.successor = true;
if (info.target) {
info.multiTarget = true;
}
}
- return和throw之前插入探针。
- 复杂if语句,为统计分支覆盖情况,会进行反转成if not,再对个分支插入探针。
- 当前指令是上一条指令的连续,并且当前指令是触发方法调用,则插入探针。
- 当前指令和上一条指令是连续的,并且是有多个来源的时候,则插入探针。
- Ant脚本根节点增加JaCoCo声明。
- 引入jacocoant 自定义task。
- 在compile task完成之后,运行instrument任务,对原始classes文件进行插桩,生成新的classes文件。
- 将插桩后的classes打包成jar包,不需要混淆,就完成了染色包的构建。
<project name="Example" xmlns:jacoco="antlib:org.jacoco.ant"> //增加jacoco声明
//引入自定义task
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="path_to_jacoco/lib/jacocoant.jar"/>
</taskdef>
...
//对classes插桩
<jacoco:instrument destdir="target/classes-instr" depends="compile">
<fileset dir="target/classes" includes="**/*.class"/>
</jacoco:instrument>
</project>
testCoverageEnabled = true //开启代码染色覆盖率统计
destfile=/sdcard/jacoco/coverage.ec
/**
* 生成ec文件
*/
public static void generateEcFile(boolean isNew, Context context) {
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if(!file.exists()){
file.mkdir();
}
DEFAULT_COVERAGE_FILE = DEFAULT_COVERAGE_FILE_PATH + File.separator+ "coverage-"+getDate()+".ec";
Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE);
OutputStream out = null;
File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE);
try {
if (!mCoverageFilePath.exists()) {
mCoverageFilePath.createNewFile();
}
out = new FileOutputStream(mCoverageFilePath.getPath(), true);
Object agent = Class.forName("org.jacoco.agent.rt.RT")
.getMethod("getAgent")
.invoke(null);
out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
.invoke(agent, false));
Log.d(TAG,"写入" + DEFAULT_COVERAGE_FILE + "完成!" );
Toast.makeText(context,"写入" + DEFAULT_COVERAGE_FILE + "完成!",Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e(TAG, "generateEcFile: " + e.getMessage());
Log.e(TAG,e.toString());
} finally {
if (out == null)
return;
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
<jacoco:merge destfile="merged.exec">
<fileset dir="executionData" includes="*.exec"/>
</jacoco:merge>
<jacoco:report>
<executiondata>
<file file="jacoco.exec"/>
</executiondata>
<structure name="Example Project">
<classfiles>
<fileset dir="classes"/>
</classfiles>
<sourcefiles encoding="UTF-8">
<fileset dir="src"/>
</sourcefiles>
</structure>
<html destdir="report"/>
</jacoco:report>
Android端代码染色原理及技术实践的更多相关文章
- 短视频技术详解:Android端的短视频开发技术
在 <如何快速实现移动端短视频功能?>中,我们主要介绍了当前短视频的大热趋势以及开发一个短视频应用所涉及到的功能和业务.在本篇文章中,我们主要谈一谈短视频在Android端上的具体实现技术 ...
- 【Android端】代码打包成jar包/aar形式
Android端代码打包成jar包和aar形式: 首先,jar包的形式和aar形式有什么区别? 1.打包之后生成的文件地址: *.jar:库/build/intermediates/bundles/d ...
- 如何做好 Android 端音视频测试?
在用户眼中,优秀的音视频产品应该具有清晰.低延时.流畅.秒开.抗丢包.高音效等特征.为了满足用户以上要求,网易云信的工程师通过自建源站,在SDK端为了适应网络优化进行QoS优化,对视频编码器进行优化, ...
- Base64实现android端图片上传到server端
首先要下载Base64.java文件http://iharder.sourceforge.net/current/java/base64/ 将代码复制到project中. 然后上代码: android ...
- Android ListView分页载入(服务端+android端)Demo
Android ListView分页载入功能 在实际开发中经经常使用到,是每一个开发人员必须掌握的内容,本Demo给出了服务端+Android端的两者的代码,并成功通过了測试. 服务端使用MyEcli ...
- 融云技术分享:融云安卓端IM产品的网络链路保活技术实践
本文来自融云技术团队原创分享,原文发布于“ 融云全球互联网通信云”公众号,原题<IM 即时通讯之链路保活>,即时通讯网收录时有部分改动. 1.引言 众所周知,IM 即时通讯是一项对即时性要 ...
- 使用Jacoco统计服务端代码覆盖情况实践
一.背景 随着需求的迭代,需求增加的同时,有可能会伴随着一些功能的下线.如果不对系统已经不用的代码进行梳理并删除不需要的代码,那么就会增加系统维护成本以及理解成本.但经历比较长的迭代以及系统交接,可能 ...
- 20145212 罗天晨 《网络对抗》Exp3 Advanced 恶意代码伪装技术实践
恶意代码伪装技术实践 木马化正常软件. 啊哈--原本以为很复杂--然后我看了一下蔡野同学的博客,发现原理竟然如此简单-- 对原先生成病毒的代码稍作修改: 于是--把生成的后门软件改成骗人的名字:这里改 ...
- 20145208 蔡野《网络对抗》Exp3 Advanced 恶意代码伪装技术实践
20145208 蔡野<网络对抗>Exp3 Advanced 恶意代码伪装技术实践 木马化正常软件 思路: 在正常软件包中将原本的程序主文件(平时打开程序用的exe文件)改成dll后缀(或 ...
随机推荐
- 为什么要写博客(jekyll迁移)
layout: post title: '为什么要写博客' date: 2019-08-12 author: xiepl1997 tags: 随笔 曾经我写过不少博客,为什么没有坚持下去?不知道. 这 ...
- Ansible常用模块-yum模块
yum模块 name 必选 指定安装包名 state 执行命令 present installed removed latest absent 其中installed and present等效 ...
- 解决@ResponseBody不能和 <mvc:annotation-driven>同时使用的问题
我们都知道使用Springmvc的ajax很强大只要三步就可以实现: 1.引入jackson的maven到pom文件: <dependency> <groupId>com.fa ...
- android.content.res.Resources$NotFoundException: String resource ID #0xb
原代码: protected void convert(BaseViewHolder helper, Student item) { helper.setText(R.id.item_tv_realm ...
- 蒲公英 · JELLY技术周刊 Vol.18 关于 React 那些设计
蒲公英 · JELLY技术周刊 Vol.18 自 2011 年,Facebook 第一次在 News Feed 上采用了 React 框架,十年来 React 生态中很多好用的功能和工具在诸多设计思想 ...
- javascript 数组的组合
javascript 数组的组合 一.前言 二.数组的组合 concat()方法 push(...items) 其他方法 三.结束语 一.前言 今天在开发项目过程中,遇到了一个需求,先请求了30个数据 ...
- 用C++写一个电话通讯录
目前我也是名初学C++的菜鸟,一直在B站上看的C++的网课,这个的C++电话通讯录是我写的第一个有一些功能的代码,所以在这里想分享给初学C++的小白们,如有任何问题或是建议可以在下方评论或是私信我,下 ...
- Vue的Options
el:挂载点 与$mount有替换关系 new Vue({ el: "#app" }); new Vue({}).$mount('#app') 注:被你选为挂载点的那个元素,如果在 ...
- “大地主”IPV6的邻居发现BD
引入 因为当初设计IPv4的时候,没有考虑到网络发展的速度这么快,到今现在IPv4有很多不足,32位的 IPv4地址不够用,现在128位的IPv6能完全够用,据说可以地球上每一粒沙子都分配一个地址,而 ...
- UTF-8、GB2312、GBK编码格式详解和编码示例
UTF-8.GB2312.GBK编码格式详解 参考文章 UTF-8 使用1~4个字节对每个字符进行编码 128个ASCII字符字需要一个字节编码 带有附加符号的拉丁文.希腊文.西里尔字母.亚美尼亚语. ...