Bootstrap
BootStrap在手机端是以jar包的形式存在的,它是UiAutomator的一个测试用例(uiautomator写的case包),通过执行命令adb shell uiautomator runtest,在手机端运行。
原理简介
server.listenForever
这里首先执行UpdateStrings.loadStringsJson(),用于获取APK中String.xml。
接下来是dismissCrashAlerts,用于注册UiWatchers监听器,这个是UiAutomator提供的,主要作用:当UiObject对象无法匹配Uiselector条件,即无法查找到元素的时候,会触发已注册的监听器,以避免测试过程中意外出现的弹出框导致用例运行失败的情况。
这里注册的是Appium已提供的ANR和Crash监听器。
updateWatchers定义一个注册心跳程序,每隔1秒开始执行一遍上面注册的监听器来检查系统是否存在异常。
allowBoxAlerts为自定义的监听器,用于监听是否出现”允许|立即删除|确定”这些弹出框按钮,如果出现就进行点击。
server.accept()
接收来自客户端的发来的数据,处理后,返回结果给客户端
handleClientData
读取及处理客户端发来的数据
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是
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 params = command.params();
final Double[] coords = { Double.parseDouble(params.get("x").toString()),
Double.parseDouble(params.get("y").toString()) };
final ArrayList 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方法
总结
说了这么多废话,尝试着用序列图描述一遍吧。
参考: https://blog.csdn.net/qhshiniba/article/details/37910683