日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

導(dǎo)讀

高德地圖開放平臺(tái)產(chǎn)品不斷迭代,代碼邏輯越來越復(fù)雜,現(xiàn)有的測(cè)試流程不能保證完全覆蓋所有業(yè)務(wù)代碼,測(cè)試不到的代碼及分支,會(huì)存在一定的風(fēng)險(xiǎn)。為了保證測(cè)試全面覆蓋,需要引入代碼覆蓋率做為測(cè)試指標(biāo),需要對(duì)SDK代碼進(jìn)行染色,測(cè)試結(jié)束后可生成代碼覆蓋率報(bào)告,作為發(fā)版前的一項(xiàng)重要卡點(diǎn)指標(biāo)。本文小結(jié)了Android端代碼染色原理及技術(shù)實(shí)踐。

JaCoCo工具

JaCoCo有以下優(yōu)點(diǎn):

  • 支持Ant和Gradle打包方式,可以自由切換。
  • 支持離線模式,更貼合SDK的使用場(chǎng)景。
  • JaCoCo文檔比較全面,還在持續(xù)維護(hù),有問題便于解決。

JaCoCo主要是通過ASM技術(shù)對(duì)JAVA字節(jié)碼進(jìn)行處理和插樁,ASM和Java字節(jié)碼技術(shù)不是本文重點(diǎn),感興趣的朋友可以自行了解。下面重點(diǎn)介紹JaCoCo的插樁原理。

Jacoco探針

由于Java字節(jié)碼是線性的指令序列,所以JaCoCo主要是利用ASM處理字節(jié)碼,在需要的地方插入一些特殊代碼。

我們通過Test1方法觀察一下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;
}

 

可以看出代碼中插入了多個(gè)Boolean數(shù)組賦值,自動(dòng)添加了jacocoInit方法和jacocoData數(shù)組聲明。

JaCoCo統(tǒng)計(jì)覆蓋率就是標(biāo)記Boolean數(shù)組, 只要執(zhí)行過的代碼,就對(duì)相應(yīng)角標(biāo)的Boolean數(shù)組進(jìn)行賦值, 最后對(duì)Boolean進(jìn)行統(tǒng)計(jì)即可得出覆蓋率,這個(gè)數(shù)組官方的名字叫探針 (Probe)。

探針是由以下四行字節(jié)碼組成,探針不改變?cè)摯a的行為,只記錄他們是否已被執(zhí)行,從理論上講,可以在每行代碼都插入一個(gè)探針,但是探針本身需要多個(gè)字節(jié)碼指令,這將增加幾倍的類文件的大小和執(zhí)行速度,所以JaCoCo有一定的插樁策略。

ALOAD    probearray
xPUSH    probeidICONST_1BASTORE

 

探針插樁策略

探針的插入需要遵循一定策略,大體可分成以下三個(gè)策略:

  • 統(tǒng)計(jì)方法的執(zhí)行情況。
  • 統(tǒng)計(jì)分支語(yǔ)句的執(zhí)行情況。
  • 統(tǒng)計(jì)普通代碼塊的執(zhí)行情況。

方法的執(zhí)行情況

這個(gè)比較容易處理, 在方法頭或者方法尾加就可以了。

  • 方法尾加: 能說明方法被執(zhí)行過, 且說明了探針上面的方法被執(zhí)行了,但是這種處理比較麻煩, 可能有多個(gè)return或者throw。
  • 方法頭加: 處理簡(jiǎn)單, 但只能說明方法有進(jìn)去過。

通過分析源碼,發(fā)現(xiàn)JaCoCo是在方法結(jié)尾處插入探針,retrun和throw之后都會(huì)加入探針。

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;
    }  }

 

分支的執(zhí)行情況

Java字節(jié)碼通過Jump指令來控制跳轉(zhuǎn),分為有條件Jump和無(wú)條件Jump。

  • 無(wú)條件Jump (goto)

這種一般出現(xiàn)在continue, break 中, 由于在任何情況下都執(zhí)行無(wú)條件跳轉(zhuǎn),因此在GOTO指令之前插入探針。

官方文檔中介紹

Android端代碼染色原理及技術(shù)實(shí)踐

 

示例代碼

Android端代碼染色原理及技術(shù)實(shí)踐

 

有條件Jump (if-else)

這種經(jīng)常出現(xiàn)于if等有條件的跳轉(zhuǎn)語(yǔ)句,JaCoCo會(huì)對(duì)if語(yǔ)句進(jìn)行反轉(zhuǎn),將字節(jié)碼變成if not的邏輯結(jié)構(gòu)。

為什么要對(duì)if進(jìn)行反轉(zhuǎn)?下面示例將說明原因。

Test4方法是一個(gè)普通的單條件if語(yǔ)句,可以看到JaCoCo將>10的條件反轉(zhuǎn)成<=10,為什么要進(jìn)行反轉(zhuǎn)而不是直接在原有if后面增加else塊呢?繼續(xù)往下看復(fù)雜一點(diǎn)的情況。

//源碼    
public static void Test4(int a) {
        if(a>10){
            a=a+10;
        }
        a=a+12;
    }
?
//jacoco處理后的字節(jié)碼
    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;
    }

 

Test5方法是一個(gè)多條件的if語(yǔ)句,可以看出來將兩個(gè)組合條件拆分成單一條件,并進(jìn)行反轉(zhuǎn)。

這樣做的好處:可以完整統(tǒng)計(jì)到每個(gè)條件分支的執(zhí)行情況,各種條件都會(huì)插入探針,保證了完整的覆蓋,而反轉(zhuǎn)操作再配合GOTO指令可以更簡(jiǎn)單的插入探針,這里可以看出JaCoCo的處理非常巧妙。

//源碼,if有多個(gè)條件
    public static void Test5(int a,int b) {
        if(a>10 || b>10){
            a=a+10;
        }
        a=a+12;
    }
?
//jacoco處理后的字節(jié)碼。
    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;
    }

 

可以通過測(cè)試報(bào)告看出來,標(biāo)記為黃色代表分支執(zhí)行情況覆蓋不完整,標(biāo)記為綠色代表分支所有條件都執(zhí)行完整了。

Android端代碼染色原理及技術(shù)實(shí)踐

 


Android端代碼染色原理及技術(shù)實(shí)踐

 

代碼塊的執(zhí)行情況

理論上只要在每行代碼前都插入探針即可, 但這樣會(huì)有性能問題。JaCoCo考慮到非方法調(diào)用的指令基本都是按順序執(zhí)行的, 因此對(duì)非方法調(diào)用的指令不插入探針, 而對(duì)方法調(diào)用的指令之前都插入探針。

Test6方法內(nèi)在調(diào)用Test方法前都插入了探針。

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;
    }

 

源碼解析

通過上面的示例,我們暫時(shí)通過表面現(xiàn)象理解了探針插入策略。知其然不知其所以然,我們通過源碼分析論證一下JaCoCo的真實(shí)邏輯,看看JaCoCo是如何通過ASM,來實(shí)現(xiàn)探針插入策略的。

源碼MethodProbesAdapter.java類中,通過needsProbe方法判斷Lable前面是否需要插入探針。

@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);
  }

 

下面看一下needsProbe方法,主要的限制條件有三個(gè)successor、multiTarget、methodInvocationLine。

public static boolean needsProbe(final Label label) {
    final LabelInfo info = get(label);
    return info != null && info.successor
        && (info.multiTarget || info.methodInvocationLine);  }

 

先看到successor屬性。顧名思義,表示當(dāng)前的Lable是否是前一條Lable的繼任者,也就是說當(dāng)前指令和上一條指令是否是連續(xù)的,兩條指令中間沒有插入GOTO或者return.

LabelFlowAnalyzer.java類中,對(duì)每行指令進(jìn)行流程分析,對(duì)successor屬性賦值。

boolean successor = false;//默認(rèn)是false
  boolean first = true; //默認(rèn)是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; //普通指令的話,表示前后兩條指令是連續(xù)的
      break;
    }    first = false;
  }?  @Override  public void visitLabel(final Label label) {    if (first) {
      LabelInfo.setTarget(label);    }    if (successor) {//這里設(shè)置當(dāng)前指令是不是上一條指令的繼任者,
            //源碼中,只有這一個(gè)地方地方會(huì)觸發(fā)這個(gè)條件賦值,也就是訪問每個(gè)label的第一條指令。
      LabelInfo.setSuccessor(label);    }  }

 

再看一下methodInvocationLine屬性,當(dāng)ASM訪問到visitMethodInsn方法的時(shí)候,就標(biāo)記當(dāng)前Lable代表調(diào)用一個(gè)方法,將methodInvocationLine賦值為True

@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就是當(dāng)前這個(gè)Lable
      LabelInfo.setMethodInvocationLine(lineStart);
    }
  }
?
  LabelInfo.java類
  public static void setMethodInvocationLine(final Label label) {
    create(label).methodInvocationLine = true;
  }

 

再看一下multiTarget屬性,它表示當(dāng)前指令是否可能從多個(gè)來源跳轉(zhuǎn)過來。源碼在下面。

當(dāng)執(zhí)行到一條Jump語(yǔ)句時(shí),第二個(gè)參數(shù)表示要跳轉(zhuǎn)到的Label,這時(shí)就會(huì)標(biāo)記一次來源,后續(xù)分析流到了該Lable,如果它還是一條繼任者指令,那么就將它標(biāo)記為多來源指令。

public void visitJumpInsn(final int opcode, final Label label) {
    LabelInfo.setTarget(label);//Jump語(yǔ)句 將Lable標(biāo)記一次為true
    if (opcode == Opcodes.JSR) {
      throw new AssertionError("Subroutines not supported.");
    }    successor = opcode != Opcodes.GOTO;    first = false;
  }?//如果當(dāng)設(shè)置它是否是上一條指令的后續(xù)指令時(shí),再一次設(shè)置它為multiTarget=true,表示至少有2個(gè)來源
public static void setSuccessor(final Label label) {    final LabelInfo info = create(label);    info.successor = true;
    if (info.target) {
      info.multiTarget = true;
    }  }

 

特殊問題解答

有了前面對(duì)源碼的分析,再來看一些特殊情況。

問:else塊結(jié)尾為什么會(huì)插入探針?

答:L3的來源有兩處,一處是GOTO來的,一處是L1順序執(zhí)行來的,使得multiTarget = true條件成立,所以在L3之前插入探針,表現(xiàn)在Java代碼中就是在else塊結(jié)尾增加了探針。

Android端代碼染色原理及技術(shù)實(shí)踐

 

問:為什么case 1條件里第一個(gè)Test方法前不插入探針?

答:L1上一條是指GOTO指令,使得successor = false,所以該方法調(diào)用前無(wú)需插入探針。

Android端代碼染色原理及技術(shù)實(shí)踐

 

探針插樁結(jié)論

通過以上分析得出結(jié)論,代碼塊中探針的插入策略:

  • return和throw之前插入探針。
  • 復(fù)雜if語(yǔ)句,為統(tǒng)計(jì)分支覆蓋情況,會(huì)進(jìn)行反轉(zhuǎn)成if not,再對(duì)個(gè)分支插入探針。
  • 當(dāng)前指令是上一條指令的連續(xù),并且當(dāng)前指令是觸發(fā)方法調(diào)用,則插入探針。
  • 當(dāng)前指令和上一條指令是連續(xù)的,并且是有多個(gè)來源的時(shí)候,則插入探針。

構(gòu)建SDK染色包

利用JaCoCo提供的Ant插件,在原有打包腳本上進(jìn)行修改。

  • Ant腳本根節(jié)點(diǎn)增加JaCoCo聲明。
  • 引入jacocoant 自定義task。
  • 在compile task完成之后,運(yùn)行instrument任務(wù),對(duì)原始classes文件進(jìn)行插樁,生成新的classes文件。
  • 將插樁后的classes打包成jar包,不需要混淆,就完成了染色包的構(gòu)建。
<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>
?    ...    //對(duì)classes插樁    <jacoco:instrument destdir="target/classes-instr" depends="compile">
      <fileset dir="target/classes" includes="**/*.class"/>
    </jacoco:instrument>
?</project>

 

測(cè)試工程配置

將生成的染色包放入測(cè)試工程lib庫(kù)中,測(cè)試工程build.gradle配置中開啟覆蓋率統(tǒng)計(jì)開關(guān)。

官方gradle插件默認(rèn)自帶JaCoCo支持,需要開啟開關(guān)。

testCoverageEnabled = true //開啟代碼染色覆蓋率統(tǒng)計(jì)

 

收集覆蓋率報(bào)告的方式有兩種,一種是用官方文檔里介紹的:配置jacoco-agent.properties文件,放Demo的resources資源目錄下。

Android端代碼染色原理及技術(shù)實(shí)踐

 

文件配置生成覆蓋率產(chǎn)物的路徑,然后測(cè)試完Demo,在終止JVM也就是退出應(yīng)用的時(shí)候,會(huì)自動(dòng)將覆蓋率數(shù)據(jù)寫入,這種方式不方便對(duì)覆蓋率文件命名自定義,多輪測(cè)試產(chǎn)物不明確。

destfile=/sdcard/jacoco/coverage.ec

 

另一種方式是利用反射技術(shù):反射調(diào)用jacoco.agent.rt.RT類的getExecutionData方法,獲取上文中探針的執(zhí)行數(shù)據(jù),將數(shù)據(jù)寫入sdcard中,生成ec文件。這段代碼可以在應(yīng)用合適位置觸發(fā),推薦退出之前調(diào)用。

/**
     * 生成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();?            }        }    }

 

覆蓋率報(bào)告生成

JaCoCo支持將多個(gè)ec文件合并,利用Ant腳本即可。

<jacoco:merge destfile="merged.exec">
    <fileset dir="executionData" includes="*.exec"/>
</jacoco:merge>

 

將ec文件從手機(jī)導(dǎo)出,配合插樁前的classes文件、源碼文件(可選),配置Ant腳本中,就可以生成html格式的覆蓋率報(bào)告。

<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>

 

熟悉Java字節(jié)碼技術(shù)、ASM框架、理解JaCoCo插樁原理,可以有各種手段玩轉(zhuǎn)SDK,例如在不修改源碼的情況下,在打包階段可以動(dòng)態(tài)插入和刪除相應(yīng)代碼,完成一些特殊需求。

參考連接

https://www.jacoco.org/jacoco/trunk/doc/index.html

本文作者:高德技術(shù)

本文地址:https://www.cnblogs.com/amap_tech/p/13672746.html

分享到:
標(biāo)簽:Android
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定