macaca app inspector 源码分析(一)

新手一枚,权当是学习。同时也了解app-inspector的实现原理。

一. 从入口说起

我们在使用app-inspector时候通过命令行 类似于: app-inspector -u 192.168.56.101:5555 进行启动,那我们就来看看这个过程是怎么进行的。

我们从app-inspector 看起

co(update, {
  pkg: pkg,
  callback: init
});

这里使用了npm-update的库,主要是获取到当前npm库的最新版本。{pkg: pkg, callback: init} 即为update的参数,同时callback用于后续的回调使用。再回到init方法中:

function init(error, data) {
  if (data && data.version && pkg.version !== data.version) {
    printInfo([`version ${pkg.version} is outdate`, `run: npm i -g ${pkg.name}@${data.version}`]);
  }

  if (program.versions) {
    console.info(`${EOL} ${chalk.grey(pkg.version)} ${EOL}`);
    process.exit(0);
  }

  if (!program.udid) {
    program.help();
    process.exit(0);
  }

  _.merge(options, _.getConfig(program));
  co(Inspector, options).catch(e => {
    console.log(e);
  });
}

首先判断获取的最新的版本是否与当前的版本一致了。如果不通就如下图所示:
这里写图片描述
再来合并默认的options参数以及我们通过命令行传参的内容,传递给inspector方法做为参数。

module.exports = function *(options) {
  try {
    yield parseOptions(options);
    //初始化以及启动服务端
    const server = new Server(options);
    yield server.start();
    const url = `http://${_.ipv4}:${options.port}`;
    logger.debug(`server start at: ${url}`);
    // 初始化设备
    yield initDevice(options);
    global.serverStarted = true;
    logger.info(`inspector start at: ${chalk.white(url)}`);

    if (!options.silent) {
	  //初始化完成后,若参数中未带有slient的话,则打开浏览器
      yield openBrowser(url);
    }
  } catch (e) {
    console.log(e);
  }
}

这里的初始化设备的过程,主要是通过我们传参的uuid进行判断是android还是iOS的设备通过判断来启动对应不同的wd。

function *initDevice(options) {
  const udid = options.udid;
  const isIOS = _.getDeviceInfo(udid).isIOS;

  if (isIOS) {
    yield require('./ios').initDevice(udid);
  } else {
    yield require('./android').initDevice(udid);
  }
}

到这里,服务就算启动完成了,下来就是具体的路由的处理了。

二. 从路由看

module.exports = function(app) {
 rootRouter
    .get('/', function *(next) {
      const isIOS = _.getDeviceInfo(this._options.udid).isIOS;

      if (global.serverStarted) {
        if (isIOS) {
          yield require('./ios').dumpXMLAndScreenShot();
        } else {
          yield require('./android').dumpXMLAndScreenShot();
        }
      }
      this.body = yield render('index.html', {
        data: {
          title: pgk.name,
          version: pgk.version,
          isIOS: isIOS,
          serverStarted: global.serverStarted
        }
      });
    });

  app
    .use(rootRouter.routes());
};

从package.json可以看出nodejs的后台使用的是koa的框架。虽然对这个不太了解。但是至少能够看出来。这个业务逻辑的一些处理,以上的判断逻辑其实也差不多。当浏览器进行访问首页的时候,判断是否为iOS设备,来获取到对应设备的xml的文件, 再返回对应的页面回去。上边的this._options 是在服务端初始化的时候就赋值好的,可以理解是个成员变量吧。

这里主要再来看看android端的dumpXMLAndScreenShot()的处理,因为对iOS不对了解,所以暂时先分析android这边吧。

exports.dumpXMLAndScreenShot = function *() {
  yield adb.shell(`touch ${ADB.ANDROID_TMP_DIR}/macaca-dump.xml`);
  var result = yield uiautomator.sendCommand('/wd/hub/session/:sessionId/source', 'get', null);
  var xml = result.value;
  xml = xml.replace(/content-desc=\"\"/g, 'content-desc="null"');
  const tempDir = path.join(__dirname, '..', '.temp');
  _.mkdir(tempDir);
  const xmlFilePath = path.join(tempDir, 'android.json');
  const hierarchy = xml2map.tojson(xml).hierarchy;

  logger.info(`Dump Android XML success, save to ${xmlFilePath}`);

  //这个方法的主要作用就是给最外层的节点增杰一个bounds,以及将遍历里面所有的节点将bounds全部转换为数组类型
  const adaptor = function(node) {
    if (node.bounds) {
      const bounds = node.bounds.match(/[\d\.]+/g);

      // [ x, y, width, height]
      node.bounds = [
        ~~bounds[0],
        ~~bounds[1],
        bounds[2] - bounds[0],
        bounds[3] - bounds[1],
      ];
    } else {
      var width = 0;
      var height = 0;
      node.node = node.node.length ? node.node : [node.node];
      node.node.forEach(item => {
        const bounds = item.bounds.match(/[\d\.]+/g);

        if (bounds[2] - bounds[0] > width) {
          width = bounds[2] - bounds[0];
        }

        if (bounds[3] - bounds[1] > height) {
          height = bounds[3] - bounds[1];
        }
      });
      node.class = 'MacacaAppInspectorRoot';
      node.bounds = [0, 0, width, height];
    }

    if (node.node) {
      node.nodes = node.node.length ? node.node : [node.node];
      node.nodes.forEach(adaptor);
      delete node.node;
    }
    return node;
  };

  var data = adaptor(hierarchy);
  //将结果数据写入到android.json文件中
  fs.writeFileSync(xmlFilePath, JSON.stringify(data), 'utf8');
  //删除原先的图片,截屏并拉取到本地
  const remoteFile = `${ADB.ANDROID_TMP_DIR}/screenshot.png`;
  const cmd = `/system/bin/rm ${remoteFile}; /system/bin/screencap -p ${remoteFile}`;
  yield adb.shell(cmd);
  const localPath = path.join(tempDir, 'android-screenshot.png');
  yield adb.pull(remoteFile, localPath);
};

这里的逻辑有点复杂,这里主要的作用就是将android端那边传送的XML进行解析,接着由浏览器进行渲染。

浏览器部分还没细看 ,下来重新写一篇。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值