在Appium执行日志流程分析这里已经讲解到Bootstrap的重要作用,今天就来切洋葱切一下它,看看它的真实面目:
源码地址:https://github.com/appium-boneyard/appium-android-bootstrap/tree/master/bootstrap,下载后导入Eclipse如下
1.Bootstrap类(在io.appium.android.bootstrap包下)
public class Bootstrap extends UiAutomatorTestCase {
public void testRunServer() {
Find.params = getParams();
//disableAndroidWatchers、acceptSslCerts默认是false
boolean disableAndroidWatchers = Boolean.parseBoolean(getParams().getString("disableAndroidWatchers"));
boolean acceptSSLCerts = Boolean.parseBoolean(getParams().getString("acceptSslCerts"));
SocketServer server;
try {
//启动Socket服务,监听4724端口
server = new SocketServer(4724);
server.listenForever(disableAndroidWatchers, acceptSSLCerts);
} catch (final SocketServerException e) {
Logger.error(e.getError());
System.exit(1);
}}
}
2.listenForever方法(在io.appium.android.bootstrap包下SocketServer类中)
public void listenForever(boolean disableAndroidWatchers, boolean acceptSSLCerts) throws SocketServerException {
//可以在Appium日志里面找到这些内容的输出
Logger.debug("Appium Socket Server Ready");
UpdateStrings.loadStringsJson();
if (disableAndroidWatchers) {
Logger.debug("Skipped registering crash watchers.");
} else {
//dismissCrashAlerts注册监听ANR与Crash异常,如果有异常会弹出一个系统对话框
dismissCrashAlerts();final TimerTask updateWatchers = new TimerTask() {
@Override
public void run() {
try {
//check()方法就是获取系统UI的对话框,如果得到则返回true
watchers.check();
} catch (final Exception e) {
}
}
};
//定时器每隔0.1S检查一次
timer.scheduleAtFixedRate(updateWatchers, 100, 100);
}if (acceptSSLCerts) {
Logger.debug("Accepting SSL certificate errors.");
acceptSSLCertificates();
}try {
//接受client发来的消息,注意一下,这里的client实际上是Appium Server,相对于设备端的Socker服务它就是client
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");
}
}
3.loadStringsJson方法(在io.appium.android.bootstrap.handler包下UpdateStrings类中)
public static boolean loadStringsJson() {
Logger.debug("Loading json...");
try {
//这个/data/local/tmp/strings.json,是由Android应用的String.xml转化而来,这个肯定会存在,Appium如果无法将String.xml转化,会形成一个空的json文件放在/data/local/tmp/目录下
String filePath = "/data/local/tmp/strings.json";
final File jsonFile = new File(filePath);
// json will not exist for apks that are only on device
// 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.apkStrings = new JSONObject(jsonString);
Logger.debug("json loading complete.");
} catch (final Exception e) {
Logger.error("Error loading json: " + e.getMessage());
return false;
}
return true;
}
从这里开始进入代码,这个方法主要是解析将String.xml文件转化成的String.json文件,然后后面具体用来干什么,在Appium现在的使用中并没有看到,如果后面涉及到再补充,继续往下。
4.回到listenForever的方法中
if (disableAndroidWatchers) {
Logger.debug("Skipped registering crash watchers.");
} else {
//dismissCrashAlerts注册监听ANR与Crash异常,如果有异常会弹出一个系统对话框
dismissCrashAlerts();final TimerTask updateWatchers = new TimerTask() {
@Override
public void run() {
try {
//check()方法就是获取系统UI的对话框,如果得到则返回true
watchers.check();
} catch (final Exception e) {
}
}
};
//定时器每隔0.1S检查一次
timer.scheduleAtFixedRate(updateWatchers, 100, 100);
}
首先disableAndroidWatchers的值默认是false所以走else语句块,else语句块的内容dismissCrashAlerts()方法是注册ANR、Crash两种异常错误监听(代码就不贴了);然后通过TimerTask对象的scheduleAtFixedRate()方法实现订单检查;watchers.check()方法是获取当前窗口中是否有系统的alertDialog对象,有责抛出异常。通过这里可以理解disableAndroidWatchers是作为这个注册监听的开关了。
5.继续在listenForever方法中往下走
try {
//接受client发来的消息,注意一下,这里的client实际上是Appium Server,相对于设备端的Socker服务它就是client
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");
}
接收client发来的数据和返回的响应数据,然后往下看handleClientData()方法
6.handleClientData方法与listenForever在同一个类里
private void handleClientData() throws SocketServerException {
try {
input.setLength(0); // clearString 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);
}
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() + ")");
}
}
把client获取的数据读出来赋值给input序列并转为String类型,然后往下执行getCommand()方法
private AndroidCommand getCommand(final String data) throws JSONException,
CommandTypeException {
return new AndroidCommand(data);
}
getCommand()方法返回的是AndroidCommand对象
public AndroidCommand(final String jsonStr) throws JSONException,
CommandTypeException {
json = new JSONObject(jsonStr);
setType(json.getString("cmd"));
}
在这里把String转换成了JSON,并且调用setType()方法设置了cmdType的值,代码如下
public void setType(final String stringType) throws CommandTypeException {
if (stringType.equals("shutdown")) {
cmdType = AndroidCommandType.SHUTDOWN;
} else if (stringType.equals("action")) {
cmdType = AndroidCommandType.ACTION;
} else {
throw new CommandTypeException("Got bad command type: " + stringType);
}
}
7.继续回到handleClientData方法往下执行runCommand方法
runCommand方法也是与handleClientData方法在同一个类
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 NoSuchElementException e) {
res = new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
} catch (final Exception e) { // Here we catch all possible exceptions and return a JSON Wire Protocol UnknownError
// This prevents exceptions from halting the bootstrap app
Logger.debug("Command returned error:" + 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();
}
通过AndroidCommand对象cmd的commandType()方法取出值比较,主要这个值在上面的方法中调用setType的时候已经设置过属性了。如果是aciton则运行else if代码块,这里说一下SHUTDOWN,是Appium服务发出一个DELETE请求或者60S都没有发命令请求过来时则为SHUTDOWN,其他情况都为ACTION。往下执行到execute()方法。
8.execute方法(在io.appium.android.bootstrap包的AndroidCommandExecutor中)
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!");
}
}
首先map.containsKey()方法代码如下
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
主要是判断入参的值是否为空,所以接下来看看入参的command.action()方法做了什么
public String action() throws JSONException {
if (isElementCommand()) {
return json.getString("action").substring(8);
}
return json.getString("action");
}
isElementCommand()方法代码
public boolean isElementCommand() {
if (cmdType == AndroidCommandType.ACTION) {
try {
return json.getString("action").startsWith("element:");
} catch (final JSONException e) {
return false;
}
}
return false;
}
主要是判断一下client客户端传过来的数据中action字段,如果action的值是以element:开头,则取出action字段的第8位开始后面值,不是以element:开头则直接取出action的值,然后判断是否为空,如果有值则为true。然后执行map.get(command.action()).execute(command);
在execute方法上面初始化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("getRect", new GetRect());
map.put("wake", new Wake());
map.put("pressBack", new PressBack());
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.put("source", new Source());
map.put("compressedLayoutHierarchy", new CompressedLayoutHierarchy());
map.put("configurator", new ConfiguratorHandler());
}
如取出click的key,则调用new Click().excute()方法。
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();
Point coords = new Point(Double.parseDouble(params.get("x").toString()),
Double.parseDouble(params.get("y").toString()) );try {
coords = PositionHelper.getDeviceAbsPos(coords);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final InvalidCoordinatesException e) {
return new AndroidCommandResult(WDStatus.INVALID_ELEMENT_COORDINATES,
e.getMessage());
}final boolean res = UiDevice.getInstance().click(coords.x.intValue(), coords.y.intValue());
return getSuccessResult(res);
}
}
整个Bootstrap的执行很粗的大概走了下来,剩下后面的就是把click等各方法由Bootstrap转为UiAutomator能识别的代码去执行啦。下次再补上,不然这篇文章我都看不下去了,太长了。