1. 程式人生 > >appium框架之bootstrap

appium框架之bootstrap

ava alert common chl tostring arr static ide ntc

(閑來無事,做做測試..)最近弄了弄appium,感覺挺有意思,就深入研究了下。

看小弟這篇文章之前,先了解一下appium的架構,對你理解有好處,推薦下面這篇文章:testerhome

appium是開源項目,可以獲得源碼:appium-master

在eclipse中用maven導入會發現有2個項目:bootstrap和sauce_appium_junit。

sauce_appium_junit是一些測試用例的集合,幫助學習的。bootstrap就是appium架構中放在手機端的一個服務器。就從它開始吧。

bootstrap結構


如圖所示為bootstrap的項目結構

bootstrap作用


bootstrap在appium中是以jar包的形式存在的,它實際上是一個uiautomator寫的case包,通過PC端的命令可以在手機端執行。

bootstrap源碼分析


首先程序的入口為Bootstrap類。所以從該類開始一步一步解釋這個項目

Bootstrap.java

package io.appium.android.bootstrap;

import io.appium.android.bootstrap.exceptions.SocketServerException;

import com.android.uiautomator.testrunner.UiAutomatorTestCase;

/**
* The Bootstrap class runs the socket server. uiautomator開發的腳本,可以直接在pc端啟動
*/
public class Bootstrap extends UiAutomatorTestCase {

public void testRunServer() {
SocketServer server;
try {
// 啟動socket服務器,監聽4724端口。
server = new SocketServer(4724);
server.listenForever();
} catch (final SocketServerException e) {
Logger.error(e.getError());
System.exit(1);
}

}
}

該類繼承自UiAutomatorTestCase。所以它才能通過adb shell uiautomator runtest AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap被執行。
該類很簡單,就是啟動線程,監聽4724端口,該端口與appium通信。

然後走server.listenForever()方法。

SocketServer.java

/**
* Listens on the socket for data, and calls {@link #handleClientData()} when
* it‘s available.
*
* @throws SocketServerException
*/
public void listenForever() throws SocketServerException {
Logger.debug("Appium Socket Server Ready");
//讀取strings.json文件的數據
UpdateStrings.loadStringsJson();
// 註冊兩種監聽器:AND和Crash
dismissCrashAlerts();
final TimerTask updateWatchers = new TimerTask() {
@Override
public void run() {
try {
// 檢查系統是否有異常
watchers.check();
} catch (final Exception e) {
}
}
};
// 計時器,0.1秒後開始,每隔0.1秒執行一次。
timer.scheduleAtFixedRate(updateWatchers, 100, 100);

try {
client = server.accept();
Logger.debug("Client connected");
in = new BufferedReader(new InputStreamReader(client.getInputStream(),
"UTF-8"));
out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),
"UTF-8"));
while (keepListening) {
// 獲取客戶端數據
handleClientData();
}
in.close();
out.close();
client.close();
Logger.debug("Closed client connection");
} catch (final IOException e) {
throw new SocketServerException("Error when client was trying to connect");
}
}

該方法中首先調用UpdateStrings.loadStringsJson();該方法如下:

UpdateStrings

/**
* strings.json文件保存的是apk的strings.xml裏的內容,在Bootstrap啟動前由appium服務器解析並push到設備端的
*
* @return
*/
public static boolean loadStringsJson() {
Logger.debug("Loading json...");
try {
final String filePath = "/data/local/tmp/strings.json";
final File jsonFile = new File(filePath);
// json will not exist for apks that are only on device
// 你的case必須寫明apk的路徑,如果啟動設備上已有的應用而case中沒有app路徑,此時json文件是不存在的
// because the node server can‘t extract the json from the apk.
if (!jsonFile.exists()) {
return false;
}
final DataInputStream dataInput = new DataInputStream(
new FileInputStream(jsonFile));
final byte[] jsonBytes = new byte[(int) jsonFile.length()];
dataInput.readFully(jsonBytes);
// this closes FileInputStream
dataInput.close();
final String jsonString = new String(jsonBytes, "UTF-8");
// 將讀取出來的信息賦給Find類中的屬性,以做後用
Find.apkStrings = new JSONObject(jsonString);
Logger.debug("json loading complete.");
} catch (final Exception e) {
Logger.error("Error loading json: " + e.getMessage());
return false;
}
return true;
}

然後回到ServerSocket類的listenForever(),此時執行到dismissCrashAlerts();該方法作用是註冊一些監聽器,觀察是否有彈出框或者AND和crash的異常。

public void dismissCrashAlerts() {
try {
new UiWatchers().registerAnrAndCrashWatchers();
Logger.debug("Registered crash watchers.");
} catch (final Exception e) {
Logger.debug("Unable to register crash watchers.");
}
}

此時listenForever()方法裏執行到註冊心跳程序,每隔0.1秒開始執行一遍上面註冊的監聽器來檢查系統是否存在異常。

final TimerTask updateWatchers = new TimerTask() {
@Override
public void run() {
try {
// 檢查系統是否有異常
watchers.check();
} catch (final Exception e) {
}
}
};
// 計時器,0.1秒後開始,每隔0.1秒執行一次。
timer.scheduleAtFixedRate(updateWatchers, 100, 100);

然後啟動數據通道,接受客戶端發來的數據和返回結果給客戶端。

client = server.accept();
Logger.debug("Client connected");
in = new BufferedReader(new InputStreamReader(client.getInputStream(),
"UTF-8"));
out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),
"UTF-8"));

接下來就是最重要的方法handleClientData();到此listenForever()方法的主要作用就完成了。現在來看handleClientData()方法做了啥。

/**
* When data is available on the socket, this method is called to run the
* command or throw an error if it can‘t.
*
* @throws SocketServerException
*/
private void handleClientData() throws SocketServerException {
try {
input.setLength(0); // clear

String res;
int a;
// (char) -1 is not equal to -1.
// ready is checked to ensure the read call doesn‘t block.
while ((a = in.read()) != -1 && in.ready()) {
input.append((char) a);
}
final String inputString = input.toString();
Logger.debug("Got data from client: " + inputString);
try {
final AndroidCommand cmd = getCommand(inputString);
Logger.debug("Got command of type " + cmd.commandType().toString());
res = runCommand(cmd);
Logger.debug("Returning result: " + res);
} catch (final CommandTypeException e) {
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage())
.toString();
} catch (final JSONException e) {
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR,
"Error running and parsing command").toString();
}
out.write(res);
out.flush();
} catch (final IOException e) {
throw new SocketServerException("Error processing data to/from socket ("
+ e.toString() + ")");
}
}

該方法中讀取客戶端發來的數據,利用getCommand()方法獲得AndroidCommand對象,然後執行runCommand()方法,獲取直接的結果。那麽該方法的作用就轉移到了runCommand()。所以現在就來看runCommand()方法是啥意思啦。


/**
* When {@link #handleClientData()} has valid data, this method delegates the
* command.
*
* @param cmd
* AndroidCommand
* @return Result
*/
private String runCommand(final AndroidCommand cmd) {
AndroidCommandResult res;
if (cmd.commandType() == AndroidCommandType.SHUTDOWN) {
keepListening = false;
res = new AndroidCommandResult(WDStatus.SUCCESS, "OK, shutting down");
} else if (cmd.commandType() == AndroidCommandType.ACTION) {
try {
res = executor.execute(cmd);
} catch (final Exception e) {
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
}
} else {
// this code should never be executed, here for future-proofing
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR,
"Unknown command type, could not execute!");
}
return res.toString();
}
}

該方法首先做了判斷,判斷命令數據哪種類型,主要有關機命令和動作命令,我們主要關註動作命令,因為動作有很多種。所以來關註第一個else if中的AndroidCommandExecutor.execute()方法。主線又轉移到了該方法中了,切去瞅一眼。
AndroidCommandExecutor.java


/**
* Gets the handler out of the map, and executes the command.
*
* @param command
* The {@link AndroidCommand}
* @return {@link AndroidCommandResult}
*/
public AndroidCommandResult execute(final AndroidCommand command) {
try {
Logger.debug("Got command action: " + command.action());

if (map.containsKey(command.action())) {
return map.get(command.action()).execute(command);
} else {
return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND,
"Unknown command: " + command.action());
}
} catch (final JSONException e) {
Logger.error("Could not decode action/params of command");
return new AndroidCommandResult(WDStatus.JSON_DECODER_ERROR,
"Could not decode action/params of command, please check format!");
}
}

該方法中終於要執行命令的實體啦

if (map.containsKey(command.action())) {
return map.get(command.action()).execute(command);
} else {
return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND,
"Unknown command: " + command.action());
}


關鍵是上面這幾行代碼,調用了map.get(command.action()).execute(command).看來要想弄懂這個命令的意思,肯定得知道map裏存放的對象是哪些,那麽在該類中找到map的初始化代碼:


static {
map.put("waitForIdle", new WaitForIdle());
map.put("clear", new Clear());
map.put("orientation", new Orientation());
map.put("swipe", new Swipe());
map.put("flick", new Flick());
map.put("drag", new Drag());
map.put("pinch", new Pinch());
map.put("click", new Click());
map.put("touchLongClick", new TouchLongClick());
map.put("touchDown", new TouchDown());
map.put("touchUp", new TouchUp());
map.put("touchMove", new TouchMove());
map.put("getText", new GetText());
map.put("setText", new SetText());
map.put("getName", new GetName());
map.put("getAttribute", new GetAttribute());
map.put("getDeviceSize", new GetDeviceSize());
map.put("scrollTo", new ScrollTo());
map.put("find", new Find());
map.put("getLocation", new GetLocation());
map.put("getSize", new GetSize());
map.put("wake", new Wake());
map.put("pressBack", new PressBack());
map.put("dumpWindowHierarchy", new DumpWindowHierarchy());
map.put("pressKeyCode", new PressKeyCode());
map.put("longPressKeyCode", new LongPressKeyCode());
map.put("takeScreenshot", new TakeScreenshot());
map.put("updateStrings", new UpdateStrings());
map.put("getDataDir", new GetDataDir());
map.put("performMultiPointerGesture", new MultiPointerGesture());
map.put("openNotification", new OpenNotification());
}

豁然開朗,該map是<String,CommandHandler>形式的map。value值對應的都是一個個的對象,這些對象都繼承與CommandHandler,裏面都有execute方法,該方法就是根據命令的不同調用不同的對象來執行相關代碼獲取結果。從map的定義可以看出,appium可以操作手機的命令還不少,我用過的有scrollTo,updateStrings,getDataDir等,上面還有截圖、打開通知欄、按下等還沒用過,但通過這些命令你也可以了解appium可以做哪些事。


繼承CommandHandler的對象有很多,我挑一個來講講它具體是幹嘛的,其他的我以後會挨個講,就挑click吧。

加入現在傳過來的命令後綴是click的話,那麽它會調用Click對象的execute方法。

Click.java

package io.appium.android.bootstrap.handler;

import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;

import java.util.ArrayList;
import java.util.Hashtable;

/**
* This handler is used to click elements in the Android UI.
*
* Based on the element Id, click that element.
*
*/
public class Click extends CommandHandler {

/*
* @param command The {@link AndroidCommand}
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (command.isElementCommand()) {
try {
final AndroidElement el = command.getElement();
el.click();
return getSuccessResult(true);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
} else {
final Hashtable<String, Object> params = command.params();
final Double[] coords = { Double.parseDouble(params.get("x").toString()),
Double.parseDouble(params.get("y").toString()) };
final ArrayList<Integer> posVals = absPosFromCoords(coords);
final boolean res = UiDevice.getInstance().click(posVals.get(0),
posVals.get(1));
return getSuccessResult(res);
}
}
}

該類就一個execute方法這根獨苗,execute方法中會先判斷傳入的參數對象是坐標值還是元素值,如果是元素值那麽直接調用AndroidElement中的click方法,一會我們再去看這個方法。如果是坐標的話,它會幹什麽呢。它會調用UiDevice的click方法,用過UiAutomator的人都知道它是uiautomator包中的類。所以說appium在api16以上的機器上使用的uiautomator機制。貌似有人覺得這好像easy了點。那好吧,我們再分析一個touchDown命令,如果傳過來的命令後綴是touchDown,那麽它會調用TouchDown對象的execute方法。

map.put("touchDown", new TouchDown());

這個類裏面的execute方法就有點意思啦。

TouchDown.java

package io.appium.android.bootstrap.handler;

import com.android.uiautomator.common.ReflectionUtils;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.Logger;

import java.lang.reflect.Method;

/**
* This handler is used to perform a touchDown event on an element in the
* Android UI.
*
*/
public class TouchDown extends TouchEvent {

@Override
protected boolean executeTouchEvent() throws UiObjectNotFoundException {
printEventDebugLine("TouchDown");
try {
final ReflectionUtils utils = new ReflectionUtils();
final Method touchDown = utils.getControllerMethod("touchDown", int.class,
int.class);
return (Boolean) touchDown.invoke(utils.getController(), clickX, clickY);
} catch (final Exception e) {
Logger.debug("Problem invoking touchDown: " + e);
return false;
}
}
}

該方法裏用到了反射,調用uiautomator裏的隱藏api來執行按下操作。就不具體講了,後面會挨個說一遍的。


總結


說了這麽多廢話,嘗試著用序列圖描述一遍吧。

技術分享圖片

appium框架之bootstrap