💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
(闲来无事,做做测试..)最近弄了弄appium,感觉挺有意思,就深入研究了下。 看小弟这篇文章之前,先了解一下appium的架构,对你理解有好处,推荐下面这篇文章:[testerhome](http://wenku.baidu.com/link?url=FbswfHp-YmkQKxrTAO61u9OVXp7aBA8TE5YN0hHSV8VkTXGTp1NkK_HbeuwFl1RJ8N3bRxRGlq3TUAq_wf06tv9wEDlUt8Fl8fhoxwD6MHa) appium是开源项目,可以获得源码:[appium-master](https://github.com/appium/appium) 在eclipse中用maven导入会发现有2个项目:bootstrap和sauce_appium_junit。 sauce_appium_junit是一些测试用例的集合,帮助学习的。bootstrap就是appium架构中放在手机端的一个服务器。就从它开始吧。 ## bootstrap结构 如图所示为bootstrap的项目结构 ![](https://box.kancloud.cn/2016-01-08_568f4d1802c85.jpg) ## 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是形式的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来执行按下操作。就不具体讲了,后面会挨个说一遍的。 # 总结 说了这么多废话,尝试着用序列图描述一遍吧。 ![](https://box.kancloud.cn/2016-01-08_568f4d1834fa6.jpg)