(闲来无事,做做测试..)最近弄了弄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的更多相关文章

  1. Appium 框架工作流程及原理

    目前Appium框架可以时支持android 和 ios 两个操作系统的App自动化测试,咱们分别对以下两款操作系统的工作流程进行说明. 一.appium-Android工作流 在Android端,a ...

  2. 旺财速啃H5框架之Bootstrap(五)

    在上一篇<<旺财速啃H5框架之Bootstrap(四)>>做了基本的框架,<<旺财速啃H5框架之Bootstrap(二)>>篇里也大体认识了bootst ...

  3. 旺财速啃H5框架之Bootstrap(四)

    上一篇<<旺财速啃H5框架之Bootstrap(三)>>已经把导航做了,接下来搭建内容框架.... 对于不规整的网页,要做成自适应就有点玩大了.... 例如下面这种版式的页面. ...

  4. 旺财速啃H5框架之Bootstrap(三)

    好多天没有写了,继续走起 在上一篇<<旺财速啃H5框架之Bootstrap(二)>>中已经把CSS引入到页面中,接下来开始写页面. 首先有些问题要先处理了,问什么你要学boot ...

  5. H5框架之Bootstrap(二)

    H5框架之Bootstrap(二) 突然感觉不知道写啥子,脑子里面没水了,可能是因为今晚要出去浪,哈哈~~~提前提醒大家平安夜要回家哦,圣诞节生00000000000这么多蛋....继续 上一篇的已经 ...

  6. H5框架之Bootstrap(一)

    H5框架之Bootstrap(一) 接下来的时间里,我将和大家一起对当前非常流行的前端框架Bootstrap进行速度的学习,以案例的形式.对刚开始想学习Bootstrap的同学而找不着边的就很有帮助了 ...

  7. express框架+jade+bootstrap+mysql开发用户注册登录项目

    完整的项目代码(github):https://github.com/suqinhui/express-demo express是基于Node.js平台的web应用开发框架,用express框架开发w ...

  8. RF+Appium框架自动化测试系列一之(Mac下Appium环境搭建)万事开头难

    消失了3个月,有一段时间没来园子更新博客了,各位看官见谅哈哈,消失是因为刚换了工作环境没外网,好多笔记没能及时的记录分享,以后有时间慢慢补上吧,这段时间主要接触了移动端app的自动化测试,公司为了快速 ...

  9. Web框架之Bootstrap

    一.Bootstrap简介 Bootstrap,来自Twitter的设计师Mark Otto和Jacob Thornton合作开发,是目前很受欢迎的Web前端开源框架. Bootstrap 是基于HT ...

随机推荐

  1. 【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上实现客户端自定义限流功能,基本完成了关于网关的一些自定义扩展需求,后面几篇将介绍基于IdentityServer ...

  2. 第13章 切换到混合流并添加API访问 - Identity Server 4 中文文档(v1.0.0)

    在之前的快速入门中,我们探讨了API访问和用户身份验证.现在我们想把这两个部分放在一起. OpenID Connect和OAuth 2.0组合的优点在于,您可以使用单个协议和使用令牌服务进行单次交换来 ...

  3. 微信小程序解密得到unoinid和手机号 (开放数据的校验和解密 获取手机号)

    实际测试 两种方法都可以: 第一种方法: public static string DecodeUserInfo(string encryptedData, string iv, string cod ...

  4. javaweb项目创建和虚拟主机配置

    首先点击File-àNew-àWeb [roject-à在Projcet Name里写项目名-à点击finish-à会出来一个框,选择NO,一个javaweb项目就创建好了.具体请看下图! 配置服务器 ...

  5. Array的 filter() 和 sort()

    filter() filter() 方法创建一个创建一个新数组,新数组中的元素是通过筛选原数组中的元素所得到的.筛选的方式是把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保 ...

  6. vue安装element-ui和px2rem的细节

    1.按需引入element-ui vue脚手架搭建完成之后,可以到element-ui官网进行npm 安装: npm i element-ui -S 如果是完整引入可以按照官网一步一步做即可完成:这里 ...

  7. splay详解(一)

    前言 Spaly是基于二叉查找树实现的, 什么是二叉查找树呢?就是一棵树呗:joy: ,但是这棵树满足性质—一个节点的左孩子一定比它小,右孩子一定比它大 比如说 这就是一棵最基本二叉查找树 对于每次插 ...

  8. Android 图片加载框架 Glide4.x

    概述 Glide是一个图片加载框架,使得我们可以轻松的加载和展示图片 Glide4.x新增apply()来进行设置,apply可以调用多次,但是如果两次apply存在冲突的设置,会以最后一次为准 新增 ...

  9. SQL Server 关于 Table 字典数据的查询SQL

    分享一个关于查询SQL Server Table 结构的SQL 语句. T-SQL 如下: SELECT (case when a.colorder=1 then d.name else '' end ...

  10. C++17剖析:string在Modern C++中的实现

    概述 GCC 8.2提供了两个版本的std::string:一个是基于Copy On Write的,另一个直接字符串拷贝的.前者针对C++11以前的,那时候没有移动构造,一切以效率为先,需要使用COW ...