云真機(jī)平臺(tái)的設(shè)計(jì)與實(shí)現(xiàn)--架構(gòu)分析

世面上云真機(jī)平臺(tái)有很多,但開(kāi)源的很少篡诽,且收費(fèi)不菲,于是深挖了下實(shí)現(xiàn)原理稳衬,著手設(shè)計(jì)開(kāi)發(fā)一個(gè)符合自身定制需求的平臺(tái)霞捡。

背景

疫情期間,同事們?cè)诩疫h(yuǎn)程辦公薄疚,為保證移動(dòng)端版本的測(cè)試進(jìn)度碧信,和移動(dòng)設(shè)備的最大化利用率,基于開(kāi)源框架搭建了一套云真機(jī)系統(tǒng)街夭。
并根據(jù)應(yīng)用兼容性標(biāo)準(zhǔn)砰碴,接入了常用的Android 4.4 ~ 9.0測(cè)試機(jī),及IOS 10設(shè)備板丽。
實(shí)際運(yùn)行過(guò)程中呈枉,存在Android部分設(shè)備易掉線、IOS高版本不兼容埃碱、操作卡頓等現(xiàn)象猖辫。


TM20200311124328.png

現(xiàn)狀

在平臺(tái)搭建過(guò)程中,對(duì)比調(diào)研了現(xiàn)有一些比較知名的云真機(jī)服務(wù)平臺(tái)砚殿,如下:

體驗(yàn)了下啃憎,基本上都是基于開(kāi)源框架STF的Android遠(yuǎn)程真機(jī),支持iOS端的很少似炎,操作體驗(yàn)不是很好辛萍,并且收費(fèi)也相對(duì)較高。
深入了解了為數(shù)不多的幾個(gè)開(kāi)源方案(STF 集成 iOS羡藐、atxserver2 手機(jī)設(shè)備管理平臺(tái))的實(shí)現(xiàn)原理后贩毕,著手開(kāi)始了平臺(tái)核心組件的開(kāi)發(fā)。

架構(gòu)設(shè)計(jì)

系統(tǒng)前端采用reactjs開(kāi)發(fā)仆嗦,監(jiān)聽(tīng)用戶(hù)在設(shè)備顯示區(qū)域的鼠標(biāo)操作辉阶,通過(guò)http && websocket來(lái)與python flask服務(wù)端通信。后端將用戶(hù)的操作轉(zhuǎn)發(fā)給provider,由provider與對(duì)應(yīng)的設(shè)備進(jìn)行交互:

  • IOS調(diào)用WDA根據(jù)XCUITest封裝的http接口
  • Android調(diào)用minitouch的websocket接口

云真機(jī)系統(tǒng)架構(gòu)大部分都差不多,由于ios真機(jī)需要調(diào)用xcodebuild執(zhí)行Test Scheme睛藻,所以需要部署在mac系統(tǒng)上启上,且要保持usb連接。Android對(duì)系統(tǒng)沒(méi)有要求店印,只要有個(gè)provider去管理設(shè)備即可冈在。


ui.png

核心輪子介紹

云真機(jī)系統(tǒng)核心是設(shè)備界面同步和用戶(hù)的操作同步,了解到的方案對(duì)比如下:

界面同步框架

名稱(chēng) 安卓 蘋(píng)果 備注
minicap ? ?
ios-minicap ? ? 一臺(tái)mac只支持一臺(tái)設(shè)備
scrcpy ? ? 需二次開(kāi)發(fā)
adb ? ? 需二次開(kāi)發(fā)自定義封裝按摘,且圖片過(guò)大
idevicescreenshot ? ? 需二次開(kāi)發(fā)自定義封裝
MJPEG Server ? ?

操作同步框架

名稱(chēng) 安卓 蘋(píng)果 備注
minitouch ? ?
scrcpy ? ? 需二次開(kāi)發(fā)
webDriverAgent ? ?
adb ? ?

iOS解決方案

通過(guò)上述的框架對(duì)比包券,我最終選擇了使用 appium-webDriverAgent 作為iOS設(shè)備的遠(yuǎn)程控制驅(qū)動(dòng),他們自定義封裝的MJPEG Server炫贤,輸出的 multipart/x-mixed-replace 格式的數(shù)據(jù)流溅固,可以直接用在 <img />上。

實(shí)現(xiàn)效果

截的gif圖像有些失真了兰珍,實(shí)際很清晰侍郭。


phone1.gif

關(guān)于appium-webDriverAgent的安裝和開(kāi)發(fā)者證書(shū)配置這里不再贅述,可以參見(jiàn)Readme掠河。

啟動(dòng)WDA

要集成到服務(wù)中亮元,可以用xcodebuild命令行來(lái)啟動(dòng),我們的持續(xù)集成平臺(tái)也用的這個(gè)唠摹。

xcodebuild -project WebDriverAgent.xcodeproj \
           -scheme WebDriverAgentRunner \
           -destination 'platform=iOS Simulator,name=iPhone 6' \
           test

啟動(dòng)的是個(gè)模擬器爆捞,如果是真機(jī)把destination里的配置換成設(shè)備ID即可:-destination 'id=xxxxxxxid', 設(shè)備id可以通過(guò)Xcode或者idevice_id -l獲取勾拉。

端口轉(zhuǎn)發(fā)

如上面的啟動(dòng)wda服務(wù)后煮甥,你還需要把手機(jī)的MJPEG服務(wù)端口暴露出來(lái),默認(rèn)是9100藕赞,我們可以通過(guò)iproxy來(lái)轉(zhuǎn)發(fā)9100端口成肘,要做多設(shè)備管理,在上面xcodebuild的命令里加上MJPEG_PORT=xxxx參數(shù)來(lái)實(shí)現(xiàn)斧蜕。
iproxy轉(zhuǎn)發(fā)命令:

iproxy 9100 9100

同時(shí)因?yàn)榍岸说耐聪拗仆Ы伲倚枰逊?wù)通過(guò)nginx再次給轉(zhuǎn)發(fā)下。實(shí)際項(xiàng)目中“8100惩激、9100”端口是動(dòng)態(tài)生成入庫(kù)跟蹤的,同時(shí)會(huì)動(dòng)態(tài)生成nginx的配置文件蟹演,通過(guò)nginx -s reload去更新服務(wù)
nginx中的轉(zhuǎn)發(fā)配置:

    location deviceControllPort/ { proxy_pass http://127.0.0.1:deviceControllPort/; } //設(shè)備操作控制服務(wù)
    location deviceScreenPort/ { proxy_pass http://127.0.0.1:deviceScreenPort/; } //設(shè)備界面顯示服務(wù)

界面同步

這里我用css給ios加了設(shè)備邊框风钻,目前代碼不全,只寫(xiě)了比較通用的iphone和劉海屏的X系列酒请÷饧迹可以根據(jù)設(shè)備的大小自動(dòng)調(diào)節(jié)邊框。

      <div className={styles.phone} style={{height: windowSize.height + 24,width: windowSize.width + 24}}>
          <div className={styles["phone_bg1"]}>
            <div className={styles["phone_bg2"]}>
              <div  className={styles["phone_bg3"]}>
                <div  className={styles["phone_lh"]}>
                  <div  className={styles["phone_lh_con"]}>
                    <div className={styles["lh_tiao"]}></div>
                    <div className={styles["lh_yuan"]}></div>
                  </div>
                </div>
                <div className={styles["phone_screen"]}>
                  <img
                    style={{ height: windowSize.height, width: windowSize.width }}
                    src={screenUrl}
                    alt=""
                    onMouseDown={e => onMouseDown(e)}
                    onMouseUp={e => onMouseUp(e)}
                    // onMouseMove={e => this.handleMouseMove(e)}
                    onDragStart={e => onDragStart(e)}
                    onDragEnd={e => onDragEnd(e)}
                  />
                </div>
                <div className={styles["phone_home"]}></div>
              </div>
            </div>
          </div>
          <div className={styles["jingyin"]}></div>
          <div className={styles["yl_jia"]}></div>
          <div className={styles["yl_jian"]}></div>
          <div className={styles["suoping"]}></div>
        </div>

操作同步

因?yàn)椴僮饕彩钦{(diào)WDA接口,所以上面設(shè)置好了后布朦,設(shè)備無(wú)再做其他的設(shè)置囤萤。

關(guān)于操作設(shè)備,我們可以直接讓前端與設(shè)備通信是趴,也可以讓前端把請(qǐng)求發(fā)送server再由去調(diào)WDA涛舍。
各有利弊,前者直接通信會(huì)快點(diǎn)唆途,但安全性不好控制富雅。可以根據(jù)實(shí)際使用場(chǎng)景來(lái)設(shè)計(jì)肛搬。

現(xiàn)階段WDA的操作還是http請(qǐng)求的没佑,有精力有能力時(shí)可以轉(zhuǎn)成websocket提高效率。同樣原版的webdriveragent里點(diǎn)擊api判斷的邏輯較多温赔,參照mrx1203的修改方案蛤奢,做了些優(yōu)化,也加了些自定義的api陶贼,如控制設(shè)備旋轉(zhuǎn)屏幕等常用操作啤贩。關(guān)于使用XCEventGenerator私有api,優(yōu)化點(diǎn)擊速度的方案需慎用骇窍,不兼容Xcode10.1以上瓜晤。

主要的修改如下:


//用于遠(yuǎn)程控制,通過(guò)旋轉(zhuǎn)角度設(shè)置橫豎屏
[[FBRoute POST:@"/orientation_Control"].withoutSession respondWithTarget:self action:@selector(handleSetOrientation_Control:)],
[[FBRoute GET:@"/orientation_Control"].withoutSession respondWithTarget:self action:@selector(handleGetOrientation_Control:)],

+ (id<FBResponsePayload>)handleSetOrientation_Control:(FBRouteRequest *)request
{
  [XCUIDevice sharedDevice].orientation = [request.arguments[@"orientation"] integerValue];
  return FBResponseWithOK();
}

+ (id<FBResponsePayload>)handleGetOrientation_Control:(FBRouteRequest *)request
{
  UIDeviceOrientation orientation = [XCUIDevice sharedDevice].orientation ;
  return FBResponseWithObject( @{
                                 @"func":@"orientation_Control",
                                 @"orientation":[NSString stringWithFormat:@"%ld",(long)orientation]
                                 });
}

Android解決方案

對(duì)比了現(xiàn)有框架腹纳,我采用的是minicap做界面同步痢掠,minitouch做操作同步,服務(wù)端封裝adb命令執(zhí)行輔助操作嘲恍。推薦個(gè)將兩者結(jié)合了工具 atx-agent 這是非必須的足画,根據(jù)自己需要添加。minitouch與minicap本身也可以通過(guò)websocket與外部通信佃牛,關(guān)于詳細(xì)的實(shí)現(xiàn)原理參見(jiàn)其Readme淹辞。

界面同步

因?yàn)槭褂昧薬tx-agent,界面同步和操作同步俘侠,我只需監(jiān)聽(tīng)設(shè)備的一個(gè)端口即可象缀,默認(rèn)的設(shè)備上是7912。要做多設(shè)備集成爷速,可以通過(guò)adb forward把設(shè)備端口與服務(wù)端任意個(gè)端口進(jìn)行綁定央星,與之前的iproxy功能類(lèi)似。
通過(guò)atx-agent啟動(dòng)minicap與minitouch命令:

$ adb shell /data/local/tmp/atx-agent server -d   # 啟動(dòng) | 停止需加上--stop

轉(zhuǎn)發(fā)設(shè)備端口

$ adb forward tcp:serverPort  tcp:7912

此時(shí)可以通過(guò)http://server:port/screenshot來(lái)看到設(shè)備的一張靜態(tài)圖片了惫东,要讓它動(dòng)起來(lái)莉给,我們需要借助前端代碼實(shí)現(xiàn)毙石。

創(chuàng)建顯示組件

AndroidPhoneFrame為自定義封裝的外框組件,主要的界面同步顯示代碼為其中img標(biāo)簽颓遏,給它綁定了ref(例子為reactjs語(yǔ)法)徐矩,后面根據(jù)這個(gè)ref對(duì)其src屬性進(jìn)行編輯。

        <AndroidPhoneFrame>
            <div className={styles.deviceScreen}>
              <img
                ref={node => {
                  this.androidScreen = node;
                }}
                src={`http://server:port/screenshot?t=${new Date().getTime()}`}
                alt=""
              />
            </div>
          </AndroidPhoneFrame>

建立連接并實(shí)時(shí)刷新顯示

代碼如下叁幢,最好放在Dom加載后觸發(fā)滤灯。可以看到這里是建立了一個(gè)socket(如果沒(méi)有用atx-agent可以將地址換成minicap的服務(wù)地址)遥皂,監(jiān)聽(tīng)服務(wù)端發(fā)來(lái)blob圖片力喷,并將其更新到前面定義的顯示標(biāo)簽上。

minicap的圖片已經(jīng)被壓縮處理過(guò)了了演训,比原生adb截圖小近百倍弟孟,而且atx-agent還進(jìn)行了二次處理,因此android這種方案流暢度更好样悟。

syncDisplay = () => {
    let ws = new WebSocket('ws://server:port/minicap/broadcast')
    ws.onclose = () => {
      console.log('onclose ')
    }
    ws.onerror = function () {
      console.log('onerror')
    }
    ws.onmessage = (message) => {
      if (!this.androidScreen){
        console.log('error')
        return
      }
      if (message.data instanceof Blob) {
        let blob = new Blob([message.data], {
          type: 'image/jpeg'
        })
        let URL = window.URL || window.webkitURL
        let u = URL.createObjectURL(blob)
        this.androidScreen.src = u //更新 ref Dom
      } else {
        console.log("receive message:", message.data)
      }
    }
    ws.onopen = function () {
      console.log('onopen')
    }
  }

操作同步

用戶(hù)在網(wǎng)頁(yè)端的操作主要是鼠標(biāo)事件拂募,iOS部分沒(méi)有細(xì)化介紹,這里簡(jiǎn)單說(shuō)下窟她,因?yàn)閙initouch本身的語(yǔ)法格式要求陈症,可以看到這里把u, d, c, w這幾個(gè)事件與鼠標(biāo)mouseDown, mouseMove, mouseUp結(jié)合了起來(lái),也正是由于其特殊的實(shí)現(xiàn)方式震糖,安卓可以實(shí)現(xiàn)按住滑動(dòng)录肯,而iOS是滑動(dòng)完才會(huì)觸發(fā)事件。

syncTouchpad() {
    const element = this.androidScreen;
    let touchSync = (operation, event) => {
      var e = event;
      if (e.originalEvent) {
        e = e.originalEvent
      }
      e.preventDefault()
      let x = e.offsetX, y = e.offsetY
      let w = e.target.clientWidth, h = e.target.clientHeight
      let scaled = this.coords(w, h, x, y, this.rotation);
      ws.send(JSON.stringify({
        operation: operation, // u, d, c, w
        index: 0,
        pressure: 0.5,
        xP: scaled.xP,
        yP: scaled.yP,
      }))
      ws.send(JSON.stringify({ operation: 'c' }))
    }

    function mouseMoveListener(event) {
      touchSync('m', event)
    }

    function mouseUpListener(event) {
      touchSync('u', event)
      element.removeEventListener('mousemove', mouseMoveListener);
      document.removeEventListener('mouseup', mouseUpListener);
    }

    function mouseDownListener(event) {
      touchSync('d', event)
      element.addEventListener('mousemove', mouseMoveListener);
      document.addEventListener("mouseup", mouseUpListener)
    }

    let ws = new WebSocket("ws://server:port/minitouch")

    ws.onopen = (ret) => {
      console.log("minitouch connected")
      ws.send(JSON.stringify({ // touch reset, fix when device is outof control
        operation: "r",
      }))
      element.addEventListener("mousedown", mouseDownListener)
    }
    ws.onmessage = (message) => {
      console.log("minitouch recv", message)
    }

    ws.onclose = () => {
      console.log("minitouch closed")
      element.removeEventListener("mousedown", mouseDownListener)
    }
  }

輔助操作

由于minitouch本身只是UI的操作吊说,所以對(duì)于旋轉(zhuǎn)论咏、Home、Back等快捷操作颁井,還需要外部的輔助厅贪。我使用的是adb命令。
以旋轉(zhuǎn)屏幕為例:

$ adb shell settings put system user_rotation 1 # 0雅宾,1养涮,2,3眉抬,4對(duì)應(yīng)著0~360°贯吓,先確保自動(dòng)旋轉(zhuǎn)已關(guān)閉。

這些adb命令可以通過(guò)服務(wù)端封裝蜀变,針對(duì)被控設(shè)備 -s deviceId 調(diào)用宣决。

實(shí)現(xiàn)效果

由于安卓機(jī)型眾多,暫時(shí)就不加邊框顯示了昏苏。

phone2.gif

結(jié)語(yǔ)

穩(wěn)定可以擴(kuò)展云真機(jī)的系統(tǒng),不僅僅是一個(gè)移動(dòng)設(shè)備的管理平臺(tái),還可以結(jié)合移動(dòng)UI自動(dòng)化贤惯、移動(dòng)應(yīng)用持續(xù)集成洼专、遠(yuǎn)程調(diào)試等產(chǎn)生更多的價(jià)值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末孵构,一起剝皮案震驚了整個(gè)濱河市屁商,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颈墅,老刑警劉巖蜡镶,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異恤筛,居然都是意外死亡官还,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)毒坛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)望伦,“玉大人,你說(shuō)我怎么就攤上這事煎殷⊥蜕。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵豪直,是天一觀的道長(zhǎng)劣摇。 經(jīng)常有香客問(wèn)我,道長(zhǎng)弓乙,這世上最難降的妖魔是什么末融? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮唆貌,結(jié)果婚禮上滑潘,老公的妹妹穿的比我還像新娘。我一直安慰自己锨咙,他們只是感情好语卤,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著酪刀,像睡著了一般粹舵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上骂倘,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天眼滤,我揣著相機(jī)與錄音,去河邊找鬼历涝。 笑死诅需,一個(gè)胖子當(dāng)著我的面吹牛漾唉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播堰塌,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼赵刑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了场刑?” 一聲冷哼從身側(cè)響起般此,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎牵现,沒(méi)想到半個(gè)月后铐懊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瞎疼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年科乎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丑慎。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡喜喂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出竿裂,到底是詐尸還是另有隱情玉吁,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布腻异,位于F島的核電站进副,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏悔常。R本人自食惡果不足惜影斑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望机打。 院中可真熱鬧矫户,春花似錦、人聲如沸残邀。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)芥挣。三九已至驱闷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間空免,已是汗流浹背空另。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蹋砚,地道東北人扼菠。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓摄杂,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親循榆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子匙姜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容