跳到主要内容

架构设计

技术栈
  • 目标平台:Android 13 (API 33) / Android 15 (API 35)
  • 核心实现:C++ (NDK)
  • API 封装:Lua 5.4
  • 底层服务:Android AccessibilityService

整体架构

┌─────────────────────────────────────────────────────────────────────────┐
│ 用户脚本 (Lua) │
│ │
│ local btn = node.app():text("登录"):get() │
│ if btn then btn:click() end │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ Lua 绑定层 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ NodeModule │ │ Query │ │ Query │ │ Element │ │
│ │ (模块入口) │ │ (只负责查询) │ │ (WebView内) │ │ (执行动作) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ C++ 核心层 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ QueryEngine │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ Constraints │ │ Matcher │ │ Executor │ │ │
│ │ │ (约束条件集) │ │ (匹配引擎) │ │ (动作执行器) │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ElementWrapper │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ Properties │ │ Relations │ │ Actions │ │ │
│ │ │ (属性访问) │ │ (关系遍历) │ │ (动作执行) │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ Android AccessibilityService │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ AccessibilityNodeInfo │ │ performAction() │ │
│ │ (控件节点信息) │ │ (执行动作) │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

核心类设计

1. NodeModule (模块入口)

// C++ 头文件
class NodeModule {
public:
// Lua 绑定入口
static int luaopen(lua_State* L);

// 创建 App 查询器
static int app(lua_State* L);

// 创建 Web 查询器
static int web(lua_State* L);

// 获取控件树快照
static int dump(lua_State* L);

// 等待条件
static int wait(lua_State* L);
};

Lua 接口:

local node = require("new_node")

-- 返回 AppQuery 对象
local query = node.app()

-- 返回 WebViewQuery 对象
local query = node.webview()

-- 获取当前界面控件树
local tree = node.dump()

-- 等待条件成立
node.waitUntil(function()
return node.app():text("完成"):exists()
end, 5000)

2. Query (查询器基类)

class Query {
protected:
std::vector<Constraint> constraints_; // 约束条件列表
int timeout_ms_ = 0; // 等待超时
int interval_ms_ = 100; // 轮询间隔
AccessibilityNodeInfo* root_ = nullptr; // 查找起点

public:
// 添加约束(返回 Query,支持链式调用)
Query& addConstraint(Constraint c);

// 配置方法
Query& timeout(int ms);
Query& interval(int ms);

// 终结方法(执行查找)
Element* get(); // 返回第一个匹配或 nullptr
std::vector<Element*> all(); // 返回所有匹配
bool exists(); // 是否存在
int count(); // 匹配数量

// 注意:Query 不包含动作方法,动作在 Element 上执行
};

3. AppQuery (App 查询器)

class AppQuery : public Query {
public:
// 属性约束
AppQuery& id(const std::string& id);
AppQuery& text(const std::string& text);
AppQuery& textContains(const std::string& text);
AppQuery& textMatches(const std::string& regex);
AppQuery& desc(const std::string& desc);
AppQuery& type(const std::string& type);

// 布尔约束
AppQuery& clickable(bool value = true);
AppQuery& enabled(bool value = true);
AppQuery& focused(bool value = true);
AppQuery& checked(bool value = true);

// 数值约束
AppQuery& index(int n);
AppQuery& depth(int n);
AppQuery& childCount(int n);

// 关系约束
AppQuery& parent(Query& q); // 父控件满足条件
AppQuery& child(Query& q); // 子控件满足条件
AppQuery& sibling(Query& q); // 兄弟控件满足条件

protected:
// 实现查找逻辑
Element* doFind() override;
};

4. WebViewQuery (WebView 查询器)

// WebView 查询器:先定位 WebView 控件,再在其内部查找
class WebViewQuery : public Query {
private:
int webviewIndex_ = 1; // 第几个 WebView(1 开始)

public:
WebViewQuery(int index = 1);

// 继承通用约束(与 AppQuery 相同)
WebViewQuery& text(const std::string& text);
WebViewQuery& textContains(const std::string& text);
WebViewQuery& id(const std::string& id);
WebViewQuery& type(const std::string& type);
WebViewQuery& clickable(bool value = true);

// 注意:没有 CSS/XPath,因为 AccessibilityService 不支持

protected:
Element* doFind() override;
};

5. Element (控件对象)

class Element {
private:
AccessibilityNodeInfo* nodeInfo_;

public:
// 属性访问
std::string getText();
std::string getDesc();
std::string getType();
std::string getId();
Rect getBounds();
bool isClickable();
bool isEnabled();
bool isChecked();

// 关系遍历
Element* parent();
std::vector<Element*> children();
Element* child(int index);
Element* next();
Element* prev();

// 在此控件内创建查询器
Query query();

// 动作执行
bool click();
bool longClick();
bool input(const std::string& text);
bool scroll(Direction dir);
bool focus();
bool select();
};

约束系统设计

// 约束类型枚举
enum class ConstraintType {
// 字符串约束
ID,
TEXT,
TEXT_CONTAINS,
TEXT_MATCHES,
DESC,
TYPE,

// 布尔约束
CLICKABLE,
ENABLED,
FOCUSED,
CHECKED,
VISIBLE,

// 数值约束
INDEX,
DEPTH,
CHILD_COUNT,

// 关系约束
HAS_PARENT,
HAS_CHILD,
HAS_SIBLING,

// 移除了 CSS_SELECTOR 和 XPATH,因为 AccessibilityService 不支持
};

// 约束结构
struct Constraint {
ConstraintType type;
std::string strValue;
int intValue;
bool boolValue;
Query* relatedQuery; // 关系约束使用
};

匹配引擎设计

class Matcher {
public:
// 单个控件匹配
static bool match(Element* element, const std::vector<Constraint>& constraints);

// 遍历控件树查找
static Element* findFirst(
AccessibilityNodeInfo* root,
const std::vector<Constraint>& constraints,
int timeoutMs
);

static std::vector<Element*> findAll(
AccessibilityNodeInfo* root,
const std::vector<Constraint>& constraints
);

private:
// 具体匹配逻辑
static bool matchString(const std::string& value, const Constraint& c);
static bool matchRegex(const std::string& value, const std::string& pattern);
static bool matchRelation(Element* element, const Constraint& c);
};

动作执行器设计

class Executor {
public:
// 基础动作
static bool click(Element* element);
static bool longClick(Element* element);
static bool input(Element* element, const std::string& text);
static bool clear(Element* element);

// 滚动动作
static bool scroll(Element* element, Direction dir, int distance = 0);
static bool scrollTo(Element* container, Element* target);

// 焦点动作
static bool focus(Element* element);
static bool clearFocus(Element* element);

// 选择动作
static bool select(Element* element);
static bool deselect(Element* element);

private:
// 底层 AccessibilityService 调用
static bool performAction(AccessibilityNodeInfo* node, int action);
static bool performAction(AccessibilityNodeInfo* node, int action, Bundle* args);
};

Lua 绑定设计

模块注册

static const luaL_Reg node_funcs[] = {
// 手势
{"tap", Node_tap},
{"longTap", Node_longTap},
{"swipe", Node_swipe},
{"pinch", Node_pinch},
{"gesture", Node_gesture},

// 全局动作
{"back", Node_back},
{"home", Node_home},
{"recents", Node_recents},
{"notifications", Node_notifications},
{"quickSettings", Node_quickSettings},
{"powerDialog", Node_powerDialog},
{"splitScreen", Node_splitScreen},
{"lockScreen", Node_lockScreen},

// 查询入口
{"app", NodeModule::app},
{"webview", NodeModule::webview},

// 工具方法
{"root", NodeModule::root},
{"focused", NodeModule::focused},
{"dump", NodeModule::dump},
{"waitUntil", NodeModule::waitUntil},
{"isReady", NodeModule::isReady},
{"currentPackage", NodeModule::currentPackage},
{"currentActivity", NodeModule::currentActivity},

{NULL, NULL}
};

int NodeModule::luaopen(lua_State* L) {
// 注册模块函数
luaL_newlib(L, node_funcs);

// 注册 Element 元表
registerElementMetatable(L);

return 1;
}

AppQuery 绑定

static const luaL_Reg appquery_methods[] = {
// 约束方法
{"id", AppQuery_id},
{"text", AppQuery_text},
{"textContains", AppQuery_textContains},
{"textStartsWith", AppQuery_textStartsWith},
{"textEndsWith", AppQuery_textEndsWith},
{"textMatches", AppQuery_textMatches}, // Lua 模式匹配
{"idContains", AppQuery_idContains},
{"idStartsWith", AppQuery_idStartsWith},
{"idEndsWith", AppQuery_idEndsWith},
{"idMatches", AppQuery_idMatches},
{"desc", AppQuery_desc},
{"descContains", AppQuery_descContains},
{"descStartsWith", AppQuery_descStartsWith},
{"descEndsWith", AppQuery_descEndsWith},
{"descMatches", AppQuery_descMatches}, // Lua 模式匹配
{"hint", AppQuery_hint},
{"hintContains", AppQuery_hintContains},
{"hintStartsWith", AppQuery_hintStartsWith},
{"hintEndsWith", AppQuery_hintEndsWith},
{"hintMatches", AppQuery_hintMatches},
{"type", AppQuery_type},
{"pkg", AppQuery_pkg},
{"clickable", AppQuery_clickable},
{"longClickable", AppQuery_longClickable},
{"enabled", AppQuery_enabled},
{"editable", AppQuery_editable},
{"focusable", AppQuery_focusable},
{"focused", AppQuery_focused},
{"checkable", AppQuery_checkable},
{"checked", AppQuery_checked},
{"selected", AppQuery_selected},
{"scrollable", AppQuery_scrollable},
{"visible", AppQuery_visible},
{"index", AppQuery_index},
{"depth", AppQuery_depth},
{"childCount", AppQuery_childCount},

// 配置方法
{"timeout", AppQuery_timeout},
{"interval", AppQuery_interval},
{"limit", AppQuery_limit},

// 终结方法
{"get", AppQuery_get},
{"all", AppQuery_all}, // 返回空表而非 nil
{"exists", AppQuery_exists},
{"count", AppQuery_count},
{"waitUntilGone", AppQuery_waitUntilGone}, // 等待消失

// 快捷方法(自动处理 nil)
{"tryClick", AppQuery_tryClick},
{"tryLongClick", AppQuery_tryLongClick},
{"tryInput", AppQuery_tryInput},
{"tryClear", AppQuery_tryClear},
{"tryFocus", AppQuery_tryFocus},

// 调试方法
{"debug", AppQuery_debug},
{"__tostring", AppQuery_tostring},

{NULL, NULL}
};

Element 绑定

static const luaL_Reg element_methods[] = {
// 属性访问
{"__index", Element_index}, // 支持 elem.text, elem.bounds 等

// 关系遍历
{"parent", Element_parent},
{"children", Element_children}, // 返回空表而非 nil
{"child", Element_child},
{"next", Element_next},
{"prev", Element_prev},
{"sibling", Element_sibling},

// 在此控件内查找
{"query", Element_query}, // 返回 Query

// 动作方法(返回 boolean, string?)
{"click", Element_click},
{"longClick", Element_longClick},
{"doubleClick", Element_doubleClick},
{"input", Element_input},
{"append", Element_append},
{"clear", Element_clear},
{"focus", Element_focus},
{"blur", Element_blur},
{"scrollUp", Element_scrollUp},
{"scrollDown", Element_scrollDown},
{"scrollLeft", Element_scrollLeft},
{"scrollRight", Element_scrollRight},
{"scrollForward", Element_scrollForward},
{"scrollBackward", Element_scrollBackward},
{"scrollIntoView", Element_scrollIntoView},
{"select", Element_select},
{"deselect", Element_deselect},
{"expand", Element_expand},
{"collapse", Element_collapse},
{"dismiss", Element_dismiss},
{"copy", Element_copy},
{"cut", Element_cut},
{"paste", Element_paste},
{"setSelection", Element_setSelection},

// 实用方法
{"isValid", Element_isValid},
{"refresh", Element_refresh},
{"screenshot", Element_screenshot},
{"dump", Element_dump},
{"toString", Element_toString},
{"__tostring", Element_tostring},

{NULL, NULL}
};

执行流程

查找并点击(标准写法)

用户代码: local btn = node.app():text("登录"):get()
if btn then btn:click() end

1. node.app()
→ 创建 AppQuery 对象
→ 返回 AppQuery userdata

2. :text("登录")
→ AppQuery::text() 被调用
→ 添加 TEXT 约束到 constraints_
→ 返回 self (链式调用)

3. :get()
→ 执行查找,遍历 AccessibilityNodeInfo 树
→ 返回 Element userdata 或 nil

4. btn:click()
→ Element::click() 被调用
→ 调用 performAction(ACTION_CLICK)
→ 返回 (true, nil) 或 (false, "error message")

快捷写法

用户代码: node.app():text("登录"):tryClick()

1-2. 同上

3. :tryClick()
→ 内部调用 get()
→ 如果找到,调用 click()
→ 返回 true/false

获取控件属性

用户代码: local btn = node.app():text("登录"):get()
print(btn.text, btn.bounds.width)

1. :get()
→ 执行查找,返回 Element userdata

2. btn.text
→ Element.__index 被调用
→ 返回 AccessibilityNodeInfo.getText()

3. btn.bounds.width
→ 返回 Rect 对象
→ Rect.__index 返回 width

内存管理

// Element 垃圾回收
int Element_gc(lua_State* L) {
Element* elem = (Element*)luaL_checkudata(L, 1, "Element");
if (elem->nodeInfo_) {
// 释放 AccessibilityNodeInfo
elem->nodeInfo_->recycle();
elem->nodeInfo_ = nullptr;
}
return 0;
}

// Query 不持有 nodeInfo,无需特殊处理

线程安全

class NodeModule {
private:
static std::mutex accessibilityMutex_;

public:
// 所有 AccessibilityService 调用都需要加锁
static AccessibilityNodeInfo* getRootNode() {
std::lock_guard<std::mutex> lock(accessibilityMutex_);
return service_->getRootInActiveWindow();
}
};