引子
開(kāi)陽(yáng)ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場(chǎng)景,ssl證書(shū)未來(lái)市場(chǎng)廣闊!成為創(chuàng)新互聯(lián)建站的ssl證書(shū)銷(xiāo)售渠道,可以享受市場(chǎng)價(jià)格4-6折優(yōu)惠!如果有意向歡迎電話(huà)聯(lián)系或者加微信:18982081108(備注:SSL證書(shū)合作)期待與您的合作!
繼多版本模擬器的支持工作告一段落之后,如何利用這些技術(shù)產(chǎn)生更大的價(jià)值,成為了接下來(lái)需要思考的問(wèn)題。當(dāng)然,接下來(lái)的課題就涉及到了今天的圖像對(duì)比技術(shù)。說(shuō)來(lái)有點(diǎn)內(nèi)疚,雖然也算是科班出身,只可惜大學(xué)還沒(méi)有真正理解圖像處理的價(jià)值,現(xiàn)在又要為自己的過(guò)去買(mǎi)單,看來(lái)出來(lái)混,遲早是要換的。
大環(huán)境
先談一下圖像對(duì)比在我廠(chǎng)使用的大環(huán)境,調(diào)研了幾類(lèi)產(chǎn)品,雖然不能說(shuō)很全,但是也可以略見(jiàn)一斑。
面對(duì)海量的圖片數(shù)據(jù),使用最多的就是使用全局特征及局部特征進(jìn)行去重、分類(lèi),這個(gè)主要應(yīng)用于圖片相關(guān)的部門(mén)。
還有一種需求可以歸納為測(cè)試需要,什么性能測(cè)試、競(jìng)品測(cè)試及UI類(lèi)測(cè)試,一切圍繞著相似度,來(lái)獲取我們需要的信息。
這次,我要做的就是第二類(lèi)測(cè)試需要,主要基于下面幾個(gè)使用場(chǎng)景:
第一,在移動(dòng)web自動(dòng)化方面,對(duì)于UI的驗(yàn)證還是使用selenium+webdriver去獲取WebElement,不過(guò)這種方式只能驗(yàn)證這個(gè)元素是否存在,并不能驗(yàn)證元素的樣式是否滿(mǎn)足我們的預(yù)期,同時(shí),對(duì)于selector的維護(hù)成本還是比較大的,尤其是面對(duì)一群對(duì)于可測(cè)試性毫不care的fe。
第二,說(shuō)起來(lái)遇到不靠譜的fe,對(duì)于一些開(kāi)發(fā)能力比較強(qiáng)的測(cè)試開(kāi)發(fā)來(lái)說(shuō),可以直接通過(guò)codeReview的方式確定功能的影響范圍。但是對(duì)于很多同學(xué)來(lái)說(shuō),這個(gè)要求還是比較難的。因此,也考慮到這一點(diǎn),可以通過(guò)將線(xiàn)上線(xiàn)下環(huán)境對(duì)比的方式,來(lái)獲取到UI的不同,從而為測(cè)試范圍的裁剪提供依據(jù)。
第三,在代碼合并階段,經(jīng)常出現(xiàn)某些同學(xué)把svn代碼合錯(cuò)或者漏合的現(xiàn)象,但是由于平時(shí)版本迭代較快,很多同學(xué)也只負(fù)責(zé)自己的項(xiàng)目,對(duì)用例更新不及時(shí),對(duì)最近上線(xiàn)的項(xiàng)目不了解,就可能導(dǎo)致回歸時(shí)的疏漏,造成事故?;谶@點(diǎn)考慮,只要使用圖像對(duì)比技術(shù),將線(xiàn)上與線(xiàn)下的UI進(jìn)行對(duì)比,就可以在一定程度上規(guī)避一些較明顯問(wèn)題。
大環(huán)境應(yīng)該就是這樣,接下來(lái)就該思考一下如何實(shí)現(xiàn)了。
思考及調(diào)研
初步的想法是先著手去做線(xiàn)上線(xiàn)下測(cè)試,一來(lái)收益會(huì)比較明顯,二來(lái)可以作為后續(xù)工作的基礎(chǔ)。大體的思路就是如何去獲取頁(yè)面截圖,然后如何去對(duì)比,最后如何把對(duì)比的結(jié)果展示出來(lái)。
如何截圖,在當(dāng)前情況下,并沒(méi)有認(rèn)為這個(gè)是多大的難點(diǎn),既然之前就已經(jīng)使用了selenium的截圖功能,這個(gè)就應(yīng)該可以實(shí)現(xiàn),因此就著重去考慮對(duì)比的問(wèn)題了。
對(duì)于圖像對(duì)比,第一件做的事情,是先去了解下有沒(méi)有比較相似的產(chǎn)品。在這里也感謝下老大的支持,在調(diào)研的過(guò)程中,老大給我推薦了幾個(gè)接觸過(guò)圖像對(duì)比技術(shù)的同學(xué),在跟他們的交流過(guò)程中,也漸漸有了思路。
第一個(gè)接觸的,是一個(gè)實(shí)習(xí)生MM,正在跟一個(gè)高工在做Android底圖的性能測(cè)試,提供了3中思路:RGB對(duì)比、灰度直方圖、SIFT特征提取。RGB對(duì)比,簡(jiǎn)單點(diǎn)說(shuō)就是通過(guò)對(duì)每個(gè)像素點(diǎn)進(jìn)行R、G、B三個(gè)通道的值進(jìn)行對(duì)比,從而得到整張圖的相似度,這種方式較后兩種來(lái)說(shuō)會(huì)比較精確?;叶戎狈綀D和SIFT特征提取對(duì)于整體上的匹配效果較好,但在對(duì)比粒度上會(huì)相對(duì)差一些。
第二個(gè)是網(wǎng)搜的同學(xué),提供了一個(gè)叫圖以類(lèi)聚的平臺(tái),提供對(duì)海量圖片的去重分類(lèi)服務(wù),也是使用特征提取的方式。
第三個(gè)是移動(dòng)云的同學(xué),之前是通過(guò)圖像對(duì)比技術(shù)解決Android客戶(hù)端自動(dòng)化基于不同分辨率坐標(biāo)點(diǎn)的匹配,最后因?yàn)槟承┰虮粩R置了。
第四個(gè)是在內(nèi)網(wǎng)上搜到的一個(gè)工具,是基于selenium進(jìn)行截圖的工具。
實(shí)現(xiàn)方案
在經(jīng)過(guò)充分思考之后,開(kāi)始著手與接下來(lái)的開(kāi)發(fā)工作,實(shí)現(xiàn)思路整理如下:
這塊需要說(shuō)明的是,基準(zhǔn)圖片與測(cè)試截圖的環(huán)境,需要盡可能保持一致,這樣才可以避免由于環(huán)境差異導(dǎo)致的問(wèn)題,比如IP定位。在做這個(gè)的時(shí)候,第一個(gè)想法其實(shí)是線(xiàn)上跟線(xiàn)下環(huán)境直接比,最后發(fā)現(xiàn)某些頁(yè)面還是會(huì)有一定區(qū)別,因此就采用了這種同步一套線(xiàn)上環(huán)境最為基準(zhǔn)的方式。
在獲取基準(zhǔn)圖片和測(cè)試截圖的過(guò)程中,需要保證頁(yè)面已經(jīng)加載完畢。在功能自動(dòng)化中,為了便于項(xiàng)目可測(cè)試,我們?cè)陧?yè)面中添加了monitor的標(biāo)記,當(dāng)這個(gè)標(biāo)記出現(xiàn)時(shí),我們則認(rèn)為頁(yè)面已經(jīng)加載完畢。其實(shí)這個(gè)對(duì)于大部分頁(yè)面來(lái)說(shuō)只能說(shuō)明我想要測(cè)試的元素已經(jīng)加載完了,并且已經(jīng)將事件綁定完畢,但是有一小部分頁(yè)面,比如違章查詢(xún),已經(jīng)不去遵守這個(gè)原則了。并且,頁(yè)面的加載完畢,并不能代碼所有的資源都已經(jīng)完全呈現(xiàn)出來(lái),這就導(dǎo)致需要一種機(jī)制來(lái)解決這個(gè)問(wèn)題。因此,在截圖這個(gè)流程中,就使用了我們的圖像對(duì)比技術(shù),sleep 2秒,然后截圖,隨后再跟上一張圖進(jìn)行對(duì)比,如果相似度滿(mǎn)足一定要求,則認(rèn)為頁(yè)面已經(jīng)渲染完畢。
頁(yè)面截圖完畢后,接下來(lái)就將這兩張圖片進(jìn)行對(duì)比,并記錄下來(lái)兩張圖不相似的地方,并生成對(duì)比結(jié)果圖片,方便后續(xù)對(duì)測(cè)試結(jié)果的查看。
實(shí)施——截圖
首先是獲取基準(zhǔn)圖片和測(cè)試圖片,實(shí)現(xiàn)比較簡(jiǎn)單,直接驗(yàn)證頁(yè)面的monitor元素是否已經(jīng)出現(xiàn),時(shí)間邏輯為
while(執(zhí)行耗時(shí) < 預(yù)期最大耗時(shí) && 沒(méi)有找到monitor){
if (monitor元素 != null)
找到monitor;
else
等一段時(shí)間;
}
等待2秒;
截圖;
雖然在找到monitor元素后等待了2秒,大部分頁(yè)面都可以完全呈現(xiàn),但是還是有些頁(yè)面無(wú)法加載完成,最長(zhǎng)的加載時(shí)間會(huì)達(dá)到10秒以上。如果再增大等待時(shí)間,勢(shì)必會(huì)對(duì)其他用例的執(zhí)行時(shí)間產(chǎn)生影響,并且也不能保證在低網(wǎng)速的情況下所有頁(yè)面都完全加載完畢。因此為了避免頁(yè)面不完全加載的情況,在此使用定時(shí)截圖,定時(shí)對(duì)比的方式,來(lái)保證頁(yè)面完全加載,實(shí)現(xiàn)邏輯修改如下:
上一次截圖 = null
while(執(zhí)行耗時(shí) < 預(yù)期最大耗時(shí) && 頁(yè)面沒(méi)有加載完畢){
當(dāng)前截圖 = 截圖();
if (對(duì)比相似度(當(dāng)前截圖 , 上一次截圖) > 一定相似度)
頁(yè)面加載完畢;
else{
上一次截圖 = 當(dāng)前截圖;
等待2秒;
}
}
這樣一改,就能夠保證,如果這個(gè)頁(yè)面在2秒鐘之內(nèi)沒(méi)有變化的話(huà),就認(rèn)為頁(yè)面已經(jīng)完全加載完畢了。不過(guò)也會(huì)有一個(gè)問(wèn)題,假如頁(yè)面消耗了2.01秒加載完畢,那么我們要在第三次截圖的時(shí)候才能判斷這個(gè)頁(yè)面已經(jīng)加載完畢了,也就是說(shuō)從加載完畢到程序反饋有4秒鐘的時(shí)間浪費(fèi),這樣整體執(zhí)行下來(lái),整個(gè)用例的執(zhí)行時(shí)間會(huì)有所提升,如果以一個(gè)case每次對(duì)比多3秒來(lái)計(jì)算,生成基準(zhǔn)圖和當(dāng)前圖共需要浪費(fèi)6秒的時(shí)間,如果是執(zhí)行100條用例,那么將會(huì)是10分鐘的浪費(fèi)。從時(shí)間上來(lái)看,其實(shí)并不是很長(zhǎng),但是最后還是想到了一種優(yōu)化策略:
定義截圖數(shù)組 ;
while(執(zhí)行耗時(shí) < 預(yù)期最大耗時(shí) && 頁(yè)面沒(méi)有加載完畢 && 截圖數(shù)組.length > 3){
當(dāng)前截圖 = 截圖();
if (對(duì)比相似度(當(dāng)前截圖 , 截圖數(shù)組[length-3]) > 一定相似度)
頁(yè)面加載完畢;
else{
截圖數(shù)組.add(當(dāng)前截圖);
等待0.5秒;
}
}
如此,既能夠保證兩張對(duì)比圖的時(shí)間間隔,同時(shí)也可以在0.5ms內(nèi)完成響應(yīng)。
實(shí)施——圖像對(duì)比
初步的圖像對(duì)比工作,已經(jīng)在實(shí)現(xiàn)截圖的過(guò)程中完成了。邏輯如下:
if ( 當(dāng)前圖片.width != 基準(zhǔn)圖片.width || 當(dāng)前圖片.height != 基準(zhǔn)圖片.height){
圖片不一致,返回;
}
相似像素?cái)?shù) = 0;
for(遍歷 當(dāng)前圖片.width){
for( 遍歷 當(dāng)前圖片.height){
if ( 當(dāng)前圖片元素RGB數(shù)組[x][y] - 基準(zhǔn)圖片元素RGB數(shù)組[x][y] < 色差閾值 ){
相似像素?cái)?shù)++;
}
}
}
相似度 = 相似像素?cái)?shù) / 總像素?cái)?shù);
if( 相似度 > 0.9 )
相似;
else
不相似;
已經(jīng)可以對(duì)兩張圖片的相似度進(jìn)行對(duì)比,但是在調(diào)試中發(fā)現(xiàn),由于像素點(diǎn)較多,如果只有很小的一部分有所更改,這種方式便很難發(fā)現(xiàn),對(duì)比的精確度有待提高。因此又將圖片進(jìn)行了水平和垂直的切分,將圖片切成 水平切分?jǐn)?shù)*垂直切分?jǐn)?shù)個(gè)圖塊,然后對(duì)每個(gè)圖塊進(jìn)行相似度對(duì)比,從而提高了圖片的相似度。
隨后又發(fā)現(xiàn),在截圖過(guò)程中也會(huì)存在頁(yè)面對(duì)部分樣式進(jìn)行了細(xì)微調(diào)整,比如對(duì)某個(gè)元素的向左偏了1px,對(duì)于用戶(hù)來(lái)說(shuō),是看不出來(lái)這種差別的,而我們的對(duì)比結(jié)果卻會(huì)因?yàn)檫@種原因而變得不準(zhǔn)確。圍繞著以用戶(hù)視覺(jué)為基準(zhǔn)的原則,又對(duì)當(dāng)前的算法進(jìn)行了優(yōu)化,對(duì)每個(gè)像素進(jìn)行了偏移量支持,并以圖塊為單位進(jìn)行整體偏移驗(yàn)證。
再后來(lái),面對(duì)實(shí)際的用戶(hù)需求,對(duì)于某些頁(yè)面,可能會(huì)有一些動(dòng)態(tài)文字,隨著時(shí)間的不同有所不同,比如時(shí)間類(lèi)的文字。對(duì)于用戶(hù)來(lái)說(shuō),這個(gè)是不在頁(yè)面差異的范圍內(nèi)的,但是我們的截圖會(huì)由于獲取時(shí)間不同而存在或多或少的差異。于是,有添加了對(duì)于執(zhí)行區(qū)域不進(jìn)行驗(yàn)證的功能。
實(shí)施——結(jié)果圖生成
結(jié)果圖的目的主要還是為了更快的找到頁(yè)面的差異,例如下面這張結(jié)果圖,對(duì)于頁(yè)面的不同一眼就能看出來(lái)。(右上角的不同是個(gè)人手機(jī)截圖的問(wèn)題)
分享及優(yōu)化
功能都實(shí)現(xiàn)完畢,接下來(lái)就帶給大組的同事們一次分享。在最后的Q&A階段,有一個(gè)問(wèn)題引起了后續(xù)的思考。有一位同學(xué)提到截圖的性能問(wèn)題。如果截圖的底層是經(jīng)由adb實(shí)現(xiàn),由于android sd卡I/O瓶頸,則很難在2秒的時(shí)間完成截圖、保存、傳輸?shù)絇C端這個(gè)過(guò)程。于是就讀了下selenium的截圖實(shí)現(xiàn),實(shí)現(xiàn)流程大致如下:
AndroidDriver
從這里乍一看貌似是返回了一個(gè)圖像信息的字符串
AndroidWebDriver
ViewAdapter
最優(yōu)經(jīng)由反射機(jī)制調(diào)用WebView的capturePicture方法,獲取瀏覽器返回的截圖數(shù)據(jù),經(jīng)由response返回。
在閱讀源碼之前,也對(duì)當(dāng)前截圖的耗時(shí)進(jìn)行了驗(yàn)證,平均截圖時(shí)間在1秒左右,也驗(yàn)證了這種B/S形式傳輸?shù)男室捎赼db。既然截圖會(huì)存在一定的耗時(shí),那么,對(duì)于我們現(xiàn)在的截圖功能來(lái)說(shuō),實(shí)際獲得的截圖則會(huì)比獲得完整截圖時(shí)的時(shí)間早1秒左右,同時(shí)我想到能不能去并行截圖呢?
嘗試了一下,發(fā)現(xiàn)截圖的時(shí)間反倒慢了,看了下Android webview的實(shí)現(xiàn),由于synchronized(obj)的原因,只能同時(shí)進(jìn)行一個(gè)頁(yè)面的截圖。最后采取了比較折中的方式,每0.5秒進(jìn)行一次截圖任務(wù)的派送,經(jīng)由截圖隊(duì)列將任務(wù)發(fā)送至截圖線(xiàn)程,從而降低了由于截圖耗時(shí)導(dǎo)致的無(wú)效等待時(shí)間。以下是優(yōu)化后的部分代碼。
CaptureThread,進(jìn)行截圖工作
@Override public void run() { System.out.println("截圖線(xiàn)程"+ this.id + "已啟動(dòng)"); while(true){ if(mission== null){ continue; } //獲取隊(duì)列數(shù)據(jù) String currentSessionId = String.copyValueOf(CaptureMissionManager.getInstance(this.managerId).sessionId.toCharArray()); try { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String beginTime = df.format(new Date()); System.out.println("截圖開(kāi)始時(shí)間為:"+beginTime); File tmpfile = ((TakesScreenshot)mission.getDriver()).getScreenshotAs(OutputType.FILE); // 關(guān)鍵代碼,執(zhí)行屏幕截圖,默認(rèn)會(huì)把截圖保存到temp目錄 FileUtils.copyFile(tmpfile, new File(CompareImage.captureDir + File.separator +mission.getCaptureName() + ".jpg")); //同一session時(shí),會(huì)將截圖信息保存到圖片列表 if(currentSessionId.equals(CaptureMissionManager.getInstance(this.managerId).sessionId)){ CaptureMissionManager.getInstance(this.managerId).p_w_picpathList.add(mission.getCaptureName()); //重新排序,避免由于截圖完成時(shí)間不同導(dǎo)致的判斷失誤 Collections.sort(CaptureMissionManager.getInstance(this.managerId).p_w_picpathList); System.out.println(CaptureMissionManager.getInstance(this.managerId).p_w_picpathList); } this.mission = null; this.isUsed = false; } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
CaptureMissionManager 負(fù)責(zé)截圖線(xiàn)程池管理及任務(wù)發(fā)送
public class CaptureMissionManager extends Thread{ private static HashMapmanagers = null; public BlockingQueue queue = new BlockingQueue(30); private static final int MAX_THREAD_COUNT = 1; //最大線(xiàn)程數(shù) public ArrayList p_w_picpathList = new ArrayList (); public String sessionId = ""; /** * 圖片截取線(xiàn)程池 */ public ArrayList threadPool = new ArrayList (); public CaptureMissionManager(String id){ this.updateSessionId(); //創(chuàng)建線(xiàn)程池資源 for(int i=0; i 0 ){ try { CaptureMission mission = (CaptureMission)this.queue.get(); thread.setMission(mission); thread.setUsed(true); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }