Appium Android Bootstrap源碼分析之啟動運行
來源:程序員人生 發布時間:2014-11-18 08:16:10 閱讀次數:3237次
通過前面的兩篇文章《Appium Android Bootstrap源碼分析之控件AndroidElement》和《Appium
Android Bootstrap源碼分析之命令解析履行》我們了解到了Appium從pc端發送過來的命令是如何定位到命令相干的控件和如何解析履行該命令。那末我們剩下的問題就是bootstrap是怎樣啟動運行的,我們會通過本篇文章的分析來論述這個問題,和把之前學習的相干的類給串起來看它們是怎樣互動的。
1.啟動方式
Bootstrap的啟動是由Appium從pc端通過adb發送命令來控制的:
從上面的調試信息我們可以看到AppiumBootstrap.jar是通過uiautomator這個命令作為1個測試包,它指定的測試類是io.appium.android.bootstrap.Bootstrap這個類。大家如果看了本人之前的文章《UIAutomator源碼分析之啟動和運行》的話應當對uiautomator的啟動原理很熟習了。
- 啟動命令:uiautomator runtest AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap
那末我們進入到Bootstrap這個類看下它是怎樣實現的:
public class Bootstrap extends UiAutomatorTestCase {
public void testRunServer() {
SocketServer server;
try {
server = new SocketServer(4724);
server.listenForever();
} catch (final SocketServerException e) {
Logger.error(e.getError());
System.exit(1);
}
}
}
從代碼中可以看到,這個類是繼承與UiAutomatorTestCase的,這樣它就可以被uiautomator作為測試用例類來履行了。
這個類只有1個測試方法testRunServer,所有事情產生的源頭就在這里:
- 創建1個sockethttp://www.vxbq.cn/server/并監聽4724端口,Appium在pc端就是通過連接這么端口來把命令發送過來的
- 循環監聽獲得Appium從pc端發送過來的命令數據,然落后行相應的處理
2. 創建sockethttp://www.vxbq.cn/server/并初始化Action到CommandHandler的映照
我們先看下SocketServer的構造函數:
public SocketServer(final int port) throws SocketServerException {
keepListening = true;
executor = new AndroidCommandExecutor();
try {
server = new ServerSocket(port);
Logger.debug("Socket opened on port " + port);
} catch (final IOException e) {
throw new SocketServerException(
"Could not start socket server listening on " + port);
}
}
它做的第1個事情是先去創建1個AndroidCommandExecutor的實例,大家應當還記得上1篇文章說到的這個類里面保存了1個靜態的很重要的action到命令處理類CommandHandler的實例的映照表吧?如果沒有看過的請先去看下。
建立好這個靜態映照表以后,構造函數下1步就似乎去創建1個ServerSocket來給Appium從PC端進行連接通訊了。
3.獲得并履行Appium命令數據
Bootstrap在創建好sockethttp://www.vxbq.cn/server/后,下1步就是調用SocketServer的listenForever的方法去循環讀取處理appium發送出來的命令數據了:
public void listenForever() throws SocketServerException {
Logger.debug("Appium Socket Server Ready");
...
try {
client = server.accept();
Logger.debug("Client connected");
in = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF⑻"));
out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), "UTF⑻"));
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");
}
...
}
首先調用server.accept去接受appium的連接要求,連接上后就去初始化用于讀取socket的BufferedReader和BufferredWriter這兩個類的實例,最落后入到handleClicentData來進行真實的數據讀取和處理
private void handleClientData() throws SocketServerException {
try {
input.setLength(0); // clear
String res;
int a;
// (char) ⑴ is not equal to ⑴.
// ready is checked to ensure the read call doesn't block.
while ((a = in.read()) != ⑴ && in.ready()) {
input.append((char) a);
}
String inputString = input.toString();
Logger.debug("Got data from client: " + inputString);
try {
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() + ")");
}
}
- 通過剛才建立的socket讀取對象去讀取appium發送過來的數據
- 把取得的的json命令字串發送給getCommand方法來實例化我們的AndroidCommand這個類,然后我們就能夠通過這個解析器來取得我們想要的json命令項了
private AndroidCommand getCommand(final String data) throws JSONException,
CommandTypeException {
return new AndroidCommand(data);
}
- 調用runCommand方法來使用我們在第2節構造ServerSocket的時候實例化的AndroidComandExecutor對象的execute方法來履行命令,這個命令終究會通過上面的AndroidCommand這個命令解析器的實例來取得appium發送過來的action,然后根據map調用對應的CommandHandler來處理命令。而如果命令是控件相干的,比如獲得1個控件的文本信息GetText,處理命令類又會繼續去AndroidElementHash保護的控件哈希表獲得到對應的控件,然后再通過UiObject把命令發送出去等等..不清楚的請查看上篇文章
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);
} ...
}
- 通過上面建立的socket寫對象把返回信息寫到socket發送給appium
4.控件是如何加入到控件哈希表的
大家可能奇怪,怎樣全部運行流程都說完了,提到了怎樣去控件哈希表獲得1個控件,但怎樣沒有看到把1個控件加入到控件哈希表呢?其實大家寫腳本的時候給1個控件發送click等命令的時候都需要先取找到這個控件,比如:
WebElement el = driver.findElement(By.name("Add note"));
這里的finElement其實就是1個命令,獲得控件并寄存到控件哈希表就是由它對應的CommandHandler實現類Find來完成的。
可以看到appium過來的命令包括幾項,有我們之間碰到過的,也有無碰到過的:
- cmd:指定是1個action
- action:指定這個action是1個find命令
- params
- strategy:指定選擇子的策略是根據空間名name來進行查找
- selector: 指定選擇子的內容是"Add note"
- context: 指定空間哈希表中目標控件的鍵值id,這里為空,由于該控件我們之前沒有用過
- multiple: 表明你腳本代碼用的是findElements還是findElement,是不是要獲得多個控件
Find重寫父類的execute方法有點長,我們把它breakdown1步1步來看.
- 第1步:取得控件的選擇子策略,以便隨著通過該策略來建立uiautomator的UiSelector
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
final Hashtable<String, Object> params = command.params();
// only makes sense on a device
final Strategy strategy;
try {
strategy = Strategy.fromString((String) params.get("strategy"));
} catch (final InvalidStrategyException e) {
return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage());
}
...
}
appium支持的策略有以下幾種,這其實在我們寫腳本中findElement常常會指定:
public enum Strategy {
CLASS_NAME("class name"),
CSS_SELECTOR("css selector"),
ID("id"),
NAME("name"),
LINK_TEXT("link text"),
PARTIAL_LINK_TEXT("partial link text"),
XPATH("xpath"),
ACCESSIBILITY_ID("http://www.vxbq.cn/access/ibility id"),
ANDROID_UIAUTOMATOR("-android uiautomator");
- 第2步:獲得appium發過來的選擇子的其他信息如內容,控件哈希表鍵值,是不是是符合選擇子等
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
final Hashtable<String, Object> params = command.params();
...
final String contextId = (String) params.get("context");
final String text = (String) params.get("selector");
final boolean multiple = (Boolean) params.get("multiple");
...
}
- 第3步,在取得1樣的選擇子的信息后,就能夠根據該選擇子信息建立真實的UiSelector選擇子列表了,這里用列表應當是斟酌到今后的復合選擇子的情況,當前我們并沒有用到,全部列表只會有1個UiSelector選擇子
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
...
try {
Object result = null;
List<UiSelector> selectors = getSelectors(strategy, text, multiple);
...
}
...
}
- 第4步:組建好選擇子UiSelector列表后,Find會根據你是findElement還是findElement,也就是說是查找1個控件還是多個控件來查找控件,但是不管是多個還是1個,終究都是調用fetchElement這個方法來取查找的
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
...
try {
Object result = null;
List<UiSelector> selectors = getSelectors(strategy, text, multiple);
if (!multiple) {
for (final UiSelector sel : selectors) {
try {
Logger.debug("Using: " + sel.toString());
result = fetchElement(sel, contextId);
} catch (final ElementNotFoundException ignored) {
}
if (result != null) {
break;
}
}
}else {
List<AndroidElement> foundElements = new ArrayList<AndroidElement>();
for (final UiSelector sel : selectors) {
// With multiple selectors, we expect that some elements may not
// exist.
try {
Logger.debug("Using: " + sel.toString());
List<AndroidElement> elementsFromSelector = fetchElements(sel, contextId);
foundElements.addAll(elementsFromSelector);
} catch (final UiObjectNotFoundException ignored) {
}
}
if (strategy == Strategy.ANDROID_UIAUTOMATOR) {
foundElements = ElementHelpers.dedupe(foundElements);
}
result = elementsToJSONArray(foundElements);
}
...
}
而fetchElement終究調用的控件哈希表類的getElements:
private ArrayList<AndroidElement> fetchElements(final UiSelector sel, final String contextId)
throws UiObjectNotFoundException {
return elements.getElements(sel, contextId);
}
AndroidElementHash的這個方法我們在前1篇文章《
Appium
Android Bootstrap源碼分析之控件AndroidElement》已分析過,我們今天再來復習1下.
從Appium發過來的控件查找命令大方向上分兩類:
- 1. 直接基于Appium Driver來查找,這類情況下appium發過來的json命令是不包括控件哈希表的鍵值信息的
WebElement addNote = driver.findElement(By.name("Add note"));
WebElement el = driver.findElement(By.className("android.widget.ListView")).findElement(By.name("Note1"));
以上的腳本會先嘗試找到Note1這個日記的父控件ListView,并把這個控件保存到控件哈希表,然后再根據父控件的哈希表鍵值和子控件的選擇子找到想要的Note1:

AndroidElementHash的這個getElement命令要做的事情就是針對這兩點來根據不同情況取得目標控件的
-
-
-
-
-
-
-
-
-
-
-
public AndroidElement getElement(final UiSelector sel, final String key)
-
throws ElementNotFoundException {
-
AndroidElement baseEl;
-
baseEl = elements.get(key);
-
UiObject el;
-
-
if (baseEl == null) {
-
el = new UiObject(sel);
-
} else {
-
try {
-
el = baseEl.getChild(sel);
-
} catch (final UiObjectNotFoundException e) {
-
throw new ElementNotFoundException();
-
}
-
}
-
-
if (el.exists()) {
-
return addElement(el);
-
} else {
-
throw new ElementNotFoundException();
-
}
-
}
- 如果是第1種情況就直接通過選擇子構建UiObject對象,然后通過addElement把UiObject對象轉換成AndroidElement對象保存到控件哈希表
- 如果是第2種情況就先根據appium傳過來的控件哈希表鍵值取得父控件,再通過子控件的選擇子在父控件的基礎上查找到目標UiObject控件,最后跟上面1樣把該控件通過addElement把UiObject控件轉換成AndroidElement控件對象保存到控件哈希表
以下就是把控件添加到控件哈希表的addElement方法
public AndroidElement addElement(final UiObject element) {
counter++;
final String key = counter.toString();
final AndroidElement el = new AndroidElement(key, element);
elements.put(key, el);
return el;
}
5. 小結
- Appium的bootstrap這個jar包和里面的o.appium.android.bootstrap.Bootstrap類是通過uiautomator作為1個uiautomator的測試包和測試方法類啟動起來的
- Bootstrap測試類繼承于uiautomator可使用的UiAutomatorTestCase
- bootstrap會啟動1個socket server并監聽來自4724端口的appium的連接
- 1旦appium連接上來,bootstrap就會不停的去獲得該端口的appium發送過來的命令數據進行解析和履行處理,然后把結果寫到該端口返回給appium
- bootstrap獲得到appium過來的json字串命令后,會通過AndroidCommand這個命令解析器解析出命令action,然后通過AndroidCommandExecutor的action到CommandHandler的map把action映照到真實的命令處理類,這些類都是繼承與CommandHandler的實現類,它們都要重寫該父類的execute方法來終究通過UiObject,UiDevice或反射取得UiAutomator沒有暴露出來的QueryController/InteractionController來把命令真實的在安卓系統中履行
- appium獲得控件大概有兩類,1類是直接通過Appium/Android Driver取得,這1種情況過來的appium查找json命令字串是沒有帶控件哈希表的控件鍵值的;另外1種是根據控件的父類控件在控件哈希表中的鍵值和子控件的選擇子來取得,這類情況過來的appium查找json命令字串是既提供了父控件在控件哈希表的鍵值又提供了子控件的選擇子的
- 1旦獲得到的控件在控件哈希表中不存在,就需要把這個AndroidElement控件添加到該哈希表里面
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈