Appium源码解析之二——第一个请求

源码解析

Posted by Mio4kon on 2017-06-23

前言

通过前一篇的源码分析,我们知道Appium如何开启了服务以及如何处理客户端发起的请求.

本篇教程将根据第一个请求,来看一看Appium初始化到底做了哪些事情.

创建SessionId

先看看Server的log:

在创建服务之后,收到的第一个请求就是/wd/hub/session,让我们来分析下收到请求后具体都干了哪些事情.

上回说到.收到请求后都会在 buildHandler中做请求处理.我们再来看看具体内容:

[appium-base-driver:lib/mjsonwp/mjsonwp.js]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function buildHandler (app, method, path, spec, driver, isSessCmd) {
let asyncHandler = async (req, res) => {
try {
if (driver.executeCommand) {
//[1]
driverRes = await driver.executeCommand(spec.command, ...args);
} else {
driverRes = await driver.execute(spec.command, ...args);
}
// unpack createSession response
if (spec.command === 'createSession') {
newSessionId = driverRes[0];
driverRes = driverRes[1];
}
};
}

其他代码都删了.我们只关心上面的代码.

[1]driver.executeCommand执行具体操作.操作会返回driverRes信息.而且如果spec.commandcreateSession的话返回的第一个内容就是Session.那么我们就从executeCommand入手.

上一篇说到driver中提供了各种方法.其中就有一个executeCommand方法.如下图:

看下具体代码:

[appium:lib/appium.js]

1
2
3
4
5
6
7
async executeCommand (cmd, ...args) {
if (isAppiumDriverCommand(cmd)) {
return super.executeCommand(cmd, ...args);
}
let sessionId = args[args.length - 1];
return this.sessions[sessionId].executeCommand(cmd, ...args);
}

看来主要逻辑实在父类里:

[appium-base-driver:lib/basedriver/driver.js]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async executeCommand (cmd, ...args) {
...
let nextCommand = this.curCommand.then(() => {
// if we unexpectedly shut down, we need to reject every command in
// the queue before we actually try to run it
if (this.shutdownUnexpectedly) {
return B.reject(new errors.NoSuchDriverError('The driver was unexpectedly shut down!'));
}
this.curCommandCancellable = new B((r) => { r(); }).then(() => {
//[1]
return this[cmd](...args);
}).cancellable();
return this.curCommandCancellable;
});
...
this.curCommand = nextCommand.catch(() => {});
let res = await nextCommand;
return res;
}

同样删掉了一些日志,异常检查和注释.这里的代码挺复杂的.看着有点绕.我们关注最后的调用即可.

关键代码: [1]this[cmd](...args)

这里调用本类的方法,即又跑到子类appium中了

[appium:lib/appium.js]

1
2
3
4
5
6
7
8
9
10
11
12
async createSession (caps, reqCaps) {
caps = _.defaults(_.clone(caps), this.args.defaultCapabilities);
let InnerDriver = this.getDriverForCaps(caps);
...
//创建一个InnerDriver
let d = new InnerDriver(this.args);
//[1]
let [innerSessionId, dCaps] = await d.createSession(caps, reqCaps, curSessions);
this.sessions[innerSessionId] = d;
...
return [innerSessionId, dCaps];
}

逻辑很简单,创建一个InnerDriver,然后调用InnerDriver[1]createSession方法.

看看InnerDriver是如何创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
getDriverForCaps (caps) {
// we don't necessarily have an `automationName` capability,
if (caps.automationName) {
if (caps.automationName.toLowerCase() === 'selendroid') {
// but if we do and it is 'Selendroid', act on it
return SelendroidDriver;
} else if (caps.automationName.toLowerCase() === 'uiautomator2') {
// but if we do and it is 'Uiautomator2', act on it
return AndroidUiautomator2Driver;
} else if (caps.automationName.toLowerCase() === 'xcuitest') {
// but if we do and it is 'XCUITest', act on it
return XCUITestDriver;
} else if (caps.automationName.toLowerCase() === 'youiengine') {
// but if we do and it is 'YouiEngine', act on it
return YouiEngineDriver;
}
}
if (caps.platformName.toLowerCase() === "fake") {
return FakeDriver;
}
if (caps.platformName.toLowerCase() === 'android') {
return AndroidDriver;
}
...

可见这个InnerDriver是根据我们客户端的给的参数来决定的.当我们没有传入automationName这个参数时,如果是Android手机就会使用AndroidDriver.如果automationNameuiautomator2,则最终会使用AndroidUiautomator2Driver.

这里我们只跟着一条线来分析,即: AndroidUiautomator2Driver

[appium-uiautomator2-driver:lib/driver.js]

1
2
3
4
5
6
7
async createSession (caps) {
try {
let sessionId;
[sessionId] = await super.createSession(caps);
...
return [sessionId, caps];
}

别忘了我们的目的:搞清楚sessionId到底在哪里创建的.

看来session的逻辑应该就在这里了super.createSession(caps).我们看看基类是啥吧?

class AndroidUiautomator2Driver extends BaseDriver

什么鬼.怎么createSession的逻辑跑到了BaseDriver中了? 如果你找下BaseDriver中的方法会发现并没有createSession这个方法.但是会发现一个关键的代码:

1
2
3
for (let [cmd, fn] of _.toPairs(commands)) {
BaseDriver.prototype[cmd] = fn;
}

原来它把很多方法分开写在各个文件内了.

1
2
3
4
import sessionCmds from './session';
import settingsCmds from './settings';
import timeoutCmds from './timeout';
import findCmds from './find';

让我们看看/session文件

[appium-base-driver:lib/basedriver/commands/session.js]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
commands.createSession = async function (desiredCapabilities, requiredCaps, capabilities) {
if (this.sessionId !== null) {
throw new errors.SessionNotCreatedError('Cannot create a new session ' +
'while one is in progress');
}
let caps;
if (desiredCapabilities) {
caps = desiredCapabilities;
} else {
caps = processCapabilities(capabilities, this.desiredCapConstraints, this.shouldValidateCaps);
}
caps = fixCaps(caps, this.desiredCapConstraints);
this.validateDesiredCaps(caps);
//[1]创建了session
this.sessionId = UUID.create().hex;
log.info(`Session created with session id: ${this.sessionId}`);
return [this.sessionId, caps];
};

终于.在这个方法里我们终于看到了[1]session到底是如何创建的.

StartUiAutomator2Session

那么seesion创建后呢?回到之前的ui2中:

[appium-uiautomator2-driver:lib/driver.js]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async createSession (caps) {
try {
// TODO handle otherSessionData for multiple sessions
let sessionId;
[sessionId] = await super.createSession(caps);
let serverDetails = {platform: 'LINUX',
webStorageEnabled: false,
takesScreenshot: true,
javascriptEnabled: true,
databaseEnabled: false,
networkConnectionEnabled: true,
locationContextEnabled: false,
warnings: {},
desired: this.caps};
....
//[1]
await this.startUiAutomator2Session();
return [sessionId, caps];
} catch (e) {
await this.deleteSession();
throw e;
}
}

可以看到对客户端的一些选项做各种配置.其中会调用[1]startUiAutomator2Session()这个方法.用来开启UI2.

[appium-uiautomator2-driver:lib/driver.js]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
async startUiAutomator2Session () {
logger.info(`UIAutomator2 Driver version:${version}`);
if (!this.opts.javaVersion) {
//[1]检查java版本
this.opts.javaVersion = await helpers.getJavaVersion();
}
// get device udid for this session
let {udid, emPort} = await helpers.getDeviceInfoFromCaps(this.opts);
this.opts.udid = udid;
this.opts.emPort = emPort;
//[2]创建adb实例
this.adb = await androidHelpers.createADB(this.opts.javaVersion,
this.opts.udid, this.opts.emPort, this.opts.adbPort);
//[3]设置一些设备相关的内容(device name,udid)
let appInfo = await helpers.getLaunchInfo(this.adb, this.opts);
Object.assign(this.opts, appInfo);
this.caps.deviceName = this.adb.curDeviceId;
this.caps.deviceUDID = this.opts.udid;
this.caps.platformVersion = await this.adb.getPlatformVersion();
this.caps.deviceScreenSize = await this.adb.getScreenSize();
this.caps.deviceModel = await this.adb.getModel();
this.caps.deviceManufacturer = await this.adb.getManufacturer();
//[4]初始化 UiAutomator2 Server
await this.initUiAutomator2Server();
//[5]关闭 UiAutomator2 Server
await this.uiautomator2.killUiAutomatorOnDevice();
//[6]初始化设备
await helpers.initDevice(this.adb, this.opts);
logger.debug(`Forwarding UiAutomator2 Server port ${DEVICE_PORT} to ${this.opts.systemPort}`);
await this.adb.forwardPort(this.opts.systemPort, DEVICE_PORT);
if (!this.opts.skipUnlock) {
//[7]解锁
await helpers.unlock(this, this.adb, this.caps);
} else {
logger.debug(`'skipUnlock' capability set, so skipping device unlock`);
}
if (this.opts.autoLaunch) {
//[8]配置AUT
await this.initAUT();
}
if (!this.caps.appPackage) {
this.caps.appPackage = appInfo.appPackage;
}
//[9]开启 UiAutomator2 Server
await this.uiautomator2.startSession(this.caps);
await this.ensureAppStarts();
if (this.opts.autoWebview) {
await retryInterval(20, this.opts.autoWebviewTimeout || 2000, async () => {
await this.setContext(this.defaultWebviewName());
});
}
this.jwpProxyActive = true;
}

代码还蛮多的.里面主要做了下面几件事:

  1. 检查java版本
  2. 创建adb实例
  3. 设置一些设备相关的内容(device name,udid)
  4. 初始化 UiAutomator2 Server
  5. 关闭 UiAutomator2 Server
  6. 初始化设备
  7. 解锁
  8. 配置AUT
  9. 开启 UiAutomator2 Server

来看第4条干了啥:

[appium-uiautomator2-driver:lib/driver.js]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async initUiAutomator2Server () {
this.uiautomator2 = new UiAutomator2Server({
host: this.opts.host || 'localhost',
systemPort: this.opts.systemPort,
devicePort: DEVICE_PORT,
adb: this.adb,
apk: this.opts.app,
tmpDir: this.opts.tmpDir,
appPackage: this.opts.appPackage,
appActivity: this.opts.appActivity,
});
this.proxyReqRes = this.uiautomator2.proxyReqRes.bind(this.uiautomator2);
}

创建了UiAutomator2Server的实例.然后把一个代理请求资源绑定在这个实例上.

这个代理到底是做什么的呢?我们看下图:

这个图挺老的了.不过原理还是没变的.客户端会向Appium Server下发请求.手机端同样也会安装一个Phone Server,Appium Server通过代理把之前收到客户端请求转发给手机端的Phone Server.这样就实现了三端通讯:

Client <–> `Appium Server <–> Phone

再来看看第8条 initAUT:

调用initAUT有个前提,就是客户端给的配置项autoLaunch必须为True.这里有个坑之前坑到了我.有兴趣的可以看下我之前的一篇博客: Appium踩坑之旅——小米手机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
async initAUT () {
this.apkStrings[this.opts.language] = await androidHelpers.pushStrings(
this.opts.language, this.adb, this.opts);
if (!this.opts.app) {
if (this.opts.fullReset) {
logger.errorAndThrow('Full reset requires an app capability, use fastReset if app is not provided');
}
logger.debug('No app capability. Assuming it is already on the device');
if (this.opts.fastReset) {
await helpers.resetApp(this.adb, this.opts.app, this.opts.appPackage, this.opts.fastReset);
}
}
if (!this.opts.skipUninstall) {
await this.adb.uninstallApk(this.opts.appPackage);
}
if (!this.opts.noSign) {
let signed = await this.adb.checkApkCert(this.opts.app, this.opts.appPackage);
if (!signed && this.opts.app) {
await this.adb.sign(this.opts.app, this.opts.appPackage);
}
}
if (this.opts.app) {
await helpers.installApkRemotely(this.adb, this.opts);
}
//[1]
await this.grantPermissions();
//[2]
await this.uiautomator2.installServerApk();
}

有很多配置项,大家有兴趣的可以看下各种配置具体的实现.这里有个[1]this.grantPermissions()方法,我们来看下:

1
2
3
4
5
6
7
8
9
async grantPermissions () {
if (this.opts.autoGrantPermissions) {
try {
await this.adb.grantAllPermissions(this.opts.appPackage, this.opts.app);
} catch (error) {
logger.error(`Unable to grant permissions requested. Original error: ${error.message}`);
}
}
}

竟然有个配置可以授予app的全部权限.这个在我看源码之前还真不知道.很好奇它是用什么命令申请到的权限.
,所以看了下源码:

[appium-adb:lib/tools/adb-commands.js]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
methods.grantAllPermissions = async function (pkg, apk) {
...
if (apiLevel >= 23 && targetSdk >= 23) {
const stdout = await this.shell(['pm', 'dump', pkg]);
const requestedPermissions = await this.getReqPermissions(pkg, stdout);
const grantedPermissions = await this.getGrantedPermissions(pkg, stdout);
const permissonsToGrant = requestedPermissions.filter((x) => grantedPermissions.indexOf(x) < 0);
...
for (let permission of permissonsToGrant) {
//[1]
const nextCmd = ['pm', 'grant', pkg, permission, ';'];
if (nextCmd.join(' ').length + cmdChunk.join(' ').length >= MAX_SHELL_BUFFER_LENGTH) {
cmds.push(cmdChunk);
cmdChunk = [];
}
cmdChunk = cmdChunk.concat(nextCmd);
}
...
};

原来他是通过adb shell pm grant package android.permission.READ_PHONE_STATE命令来获取权限.

所以如果你不希望运行测试case的时候弹出各种权限框可以设置autoGrantPermissions这个配置项.

initAUT中最重要的当然是最后一个方法.[2]this.uiautomator2.installServerApk(),他会在测试手机上安装Uiautomator2 Server.

到此为止第8条(配置AUT)说完了,我们该看下第9条了(开启 UiAutomator2 Server)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async startSession (caps) {
// killing any uiautomator existing processes
await this.killUiAutomatorOnDevice();
if (caps.deviceUDID) {
this.startSessionOnSpecificDeviceUsingAdbKit(caps.deviceUDID);
} else {
this.startSessionUsingAdbkit();
}
// wait 20s for UiAutomator2 to be online
await retryInterval(retries, 1000, this.jwproxy.command.bind(this.jwproxy), '/status', 'GET');
await this.jwproxy.command('/session', 'POST', {desiredCapabilities: caps});
}
async startSessionOnSpecificDeviceUsingAdbKit (deviceUDID) {
Promise.try(function (){
//开启Ui2 Server
client.shell(deviceUDID, "am instrument -w io.appium.uiautomator2.server.test/android.support.test.runner.AndroidJUnitRunner");
})
.catch(function (err) {
logger.error('Something went wrong while executing instrument test:', err.stack);
});
}

其实开启了Ui2 Server就是靠这个命令:

am instrument -w io.appium.uiautomator2.server.test/android.support.test.runner.AndroidJUnitRunner

这个命令是干什么用的呢? 你可以简单的想象成他会运行一个没有界面的服务.下面稍微简单的介绍下Instrument

Instrument介绍

官方文档:https://developer.android.com/training/testing/start/index.html

关于Instrument,它常用于应用的测试框架中.下面是官方的解释:

Base class for implementing application instrumentation code. When running with 
instrumentation turned on, this class will be instantiated for you before any 
of the application code, allowing you to monitor all of the interaction the 
system has with the application. An Instrumentation implementation is described 
to the system through an AndroidManifest.xml's <instrumentation> tag.

简单来说就是可以监视应用的生命周期.这也是为什么它适合做Android的UI测试.再来看来下官方图:

-w766

再来看看官方对应UiAutomator2的定义:

可以看出无论是Espresso还是UI Automator都是要结合Instrument的.

Uiautomator2 Server

既然要通过Instrument来启动Uiautomator2 Server,那么我们来看下UI2 Server的代码:

https://github.com/appium/appium-uiautomator2-server

或者在Appium项目下:node_modules/appium-uiautomator2-server

[appium-uiautomator2-server:app/src/androidTestServer/java/io/appium/uiautomator2/server/test/AppiumUiAutomator2Server.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RunWith(AndroidJUnit4.class)
public class AppiumUiAutomator2Server {
private static ServerInstrumentation serverInstrumentation;
private Context ctx;
/**
* Starts the server on the device
*/
@Test
public void startServer() throws InterruptedException {
if (serverInstrumentation == null) {
ctx = InstrumentationRegistry.getInstrumentation().getContext();
serverInstrumentation = ServerInstrumentation.getInstance(ctx, ServerConfig.getServerPort());
Logger.info("[AppiumUiAutomator2Server]", " Starting Server");
try {
while (!serverInstrumentation.isStopServer()) {
SystemClock.sleep(1000);
serverInstrumentation.startServer();
}
}catch (SessionRemovedException e){
//Ignoring SessionRemovedException
}
}
}
}

运行之前的am命令会执行startServer的方法.(相关配置文档)

这段代码会开启一个Socket服务来处理请求.如果大家有兴趣的可以看看这套框架的实现.这里面是Appium在使用UI2来进行控制UI控件的核心代码了.

上面说了通过am命令开启Phone Server后同样也会转发第一个请求给Phone Server,忘记的童鞋可以回到上面再看下,在startSession方法中.

1
await this.jwproxy.command('/session', 'POST', {desiredCapabilities: caps});

我们再简单的跟下uiautomator2 server的源码:

[appium-uiautomator2-server:app/src/main/java/io/appium/uiautomator2/server/ServerInstrumentation.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void startServer() throws InterruptedException, SessionRemovedException {
...
serverThread = new HttpdThread(this, this.serverPort);
serverThread.start();
}
private class HttpdThread extends Thread {
private final AndroidServer server;
private ServerInstrumentation instrumentation;
private Looper looper;
public HttpdThread(ServerInstrumentation instrumentation, int serverPort) {
this.instrumentation = instrumentation;
server = new AndroidServer(serverPort);
}
@Override
public void run() {
Looper.prepare();
looper = Looper.myLooper();
startServer();
Looper.loop();
}
...
private void startServer() {
...
server.start();
}
...
}

会开启一个AndroidServer,其代码非常简单,如下:

[appium-uiautomator2-server:app/src/main/java/io/appium/uiautomator2/server/AndroidServer.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AndroidServer {
private final int driverPort;
private final HttpServer webServer;
public AndroidServer(int port) {
driverPort = port;
webServer = new HttpServer(driverPort);
init();
Logger.info("AndroidServer created on port " + port);
}
protected void init() {
webServer.addHandler(new AppiumServlet());
}
public void start() {
webServer.start();
}
...
}

init方法中会创建一个AppiumServlet, 它就是收到各种请求后的处理.

我们第一个请求的处理自然就在NewSession文件中.

[appium-uiautomator2-server:app/src/main/java/io/appium/uiautomator2/handler/NewSession.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class NewSession extends SafeRequestHandler {
public NewSession(String mappedUri) {
super(mappedUri);
}
@Override
public AppiumResponse safeHandle(IHttpRequest request) {
String sessionID;
try {
Session.capabilities = getCapabilities(request);
//创建seesionId
sessionID = new AppiumUiAutomatorDriver().initializeSession();
Logger.info("Session Created with SessionID:" + sessionID);
} catch (JSONException e) {
Logger.error("Exception while reading JSON: ", e);
return new AppiumResponse(getSessionId(request), WDStatus.JSON_DECODER_ERROR, e);
}
return new AppiumResponse(sessionID, WDStatus.SUCCESS, "Created Session");
}
public Map<String, Object> getCapabilities(IHttpRequest request) throws JSONException {
JSONObject caps = getPayload(request).getJSONObject("desiredCapabilities");
Map<String, Object> map = new HashMap<String, Object>();
Iterator<String> keysItr = caps.keys();
while(keysItr.hasNext()) {
String key = keysItr.next();
Object value = caps.get(key);
map.put(key, value);
}
return map;
}
}

好吧其实猜都猜的到干嘛了.Uiautomator2 Server也会创建一个SeesionId用来保持Appium ServerPhone Server之间的通讯.

大家可以看到上图的通讯.这里包含了2个SeesionId,在最后会将Phone ServerSeesionId进行一次替换,然后再返回给客户端.

相关代码在proxy文件中,这里就不细说了:

[appium-base-driver:lib/jsonwp-proxy/proxy.js]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async proxyReqRes (req, res) {
let [response, body] = await this.proxy(req.originalUrl, req.method, req.body);
res.headers = response.headers;
res.set('Content-type', response.headers['content-type']);
body = util.safeJsonParse(body);
if (body && body.sessionId) {
const reqSessionId = this.getSessionIdFromUrl(req.originalUrl);
if (reqSessionId) {
log.info(`Replacing sessionId ${body.sessionId} with ${reqSessionId}`);
body.sessionId = reqSessionId;
} else if (this.sessionId) {
log.info(`Replacing sessionId ${body.sessionId} with ${this.sessionId}`);
body.sessionId = this.sessionId;
}
}
res.status(response.statusCode).send(JSON.stringify(body));
}
}

到此为止我们已经从代码的角度分析了从客户端的第一个请求Appium服务再到Uiautomator2服务的处理流程,以及各个框架之间如何衔接.