Anna
Anna提供了一个抽象层,有助于将分析代码与其它代码分离。
Anna包括两部分
- Anna.iOS 提供了一个名为
Analyzer
的类,类似于 MVVM 中的视图模型。它从UIResponder
(及其子类)钩子事件回调,然后将接收到的内容暴露给 Anna.Core。 - Anna.Core 位于 JavaScript 中。它为每个暴露的
UIResponder
分配一个唯一的路径,该路径由Node
组成。当Node
上发生事件时,它运行注册的任务以“挖掘”在Node
(及其路径)上发生的事情。
最后将结果回传给 Anna.iOS,其中结果可以上传到远程服务器或某些分析服务提供商。
如何使用
基本用法
例如,如果我们想跟踪来自表格视图控制器中单元格的 did-select 事件,我们需要注册这样的任务
/* in MasterViewController.js */
match(
'master/tableView/cell/did-select',
(node) => { return { action: 'selected', id: node.path }; }
);
将 MasterViewController
及其表格视图暴露给 Anna.Core
import Anna
class MasterViewController: UITableViewController, AnalyzableObject {
lazy var analyzer: Analyzing = { Analyzer.analyzer(with: self) }()
static let subAnalyzableKeys: Set<String> = [#keyPath(tableView)]
deinit {
self.analyzer.detach()
}
override func viewDidLoad() {
super.viewDidLoad()
self.analyzer.enable(naming: "master")
}
}
class AnalyzableTableView : UITableView, Analyzable {
lazy var analyzer: Analyzing = { Analyzer.analyzer(with: self) }()
deinit {
self.analyzer.detach()
}
}
接收结果
class AppDelegate: UIResponder, UIApplicationDelegate, Delegate {
func manager(_ manager :Manager, didSend result :Any) {
print(result)
//
// Supposed to output
// { // action = selected;
// id = "/master/tableView/cell";
// }
}
}
响应链 & 焦点路径
一个典型的应用程序结构,它包含主视图控制器与详细视图控制器,在导航控制器内部导航,可能看起来像这样
AppDelegate/
├── Anna.RootAnalyzer/
└── UIWindow/
└── UINavigationController/
├── MasterViewController/
│ ├── Anna.Analyzer("master")/
│ └── UITableView/
│ ├── Anna.Analyzer("table")/
│ └── TableViewCell/
│ └── Anna.Analyzer("cell", #)/
└── DetailViewController/
├── Anna.Analyzer("detail")/
└── UIButton/
└── Anna.Analyzer("button")/
我们可以注意到,上述树中的某些 Node
具有作为实例成员的 Analyzer
。根据分配给 Analyzer
的名称,在 Anna.Core 中,这个树会生成另一棵略有不同的树
/
└── master/
└── table/
└── cell/
└── delail/
└── button/
主要的区别从属于 DetailViewController
的 Node
detail 开始。在由 UIKit
提供的 响应者链 中,DetailViewController
的下一个响应者(树中的父节点)是 UINavigationController
。然而,在 Anna.Core 中,detail 的父节点是 cell,这意味着用户的焦点从 cell 移动到 detail。这种行为是因为,从分析的角度来看,每个视图有用的信息通常属于用户之前关注的视图。因此,在 Anna.Core 中,从根到 Node
的每条路径实际上都是一条 聚焦路径。
Analyzer
Analyzer
是对对象记录事件和与 Anna.Core 中的 Node
进行其他交互的接口。
超级-子 Analyzer 及父-子 Node
在大多数情况下,Analyzer
使用名称来识别它绑定的 Node
和 Node
父节点之间的键关系。该名称通常由其超 Analyzer
提供。然而,有时 Analyzer
的超级 Analyzer
无法直接访问它,在这种情况下,一个 独立 的名称也可以使用。
使用索引索引来分别为子 Analyzer
数组分配索引以表示索引关系。
超级 Analyzer
的所有者可以通过确认协议 Anna.AnalyzableObject
并实现方法 subAnalyzableKeys
来向其添加子 Analyzer
。这样,子 Analyzer
就可以在关注路径中创建子 Node
。
如果一个 Analyzer
没有被任何其他 Analyzer
作为子 Analyzer
添加,它将查找响应者链以获取超级 Analyzer
。在查找过程中,会调用 Anna.FocusPathConstituting.parentConstitutor()
和 Anna.FocusPathConstitutionRedirecting.redirectedConstitutor()
来获取超级-子关系详细的详细信息。大多数 UIResponder
和其子类有这些方法的默认实现,但它们可以被覆盖以支持自定义行为。
根 Analyzer 及管理器
UIView
、UIViewController
、UIControl
以及 Responder Chain 中的所有其他对象可以拥有一个 Analyzer
,如果它们需要被分析,包括根响应者 - UIApplicationDelegate
。但是,UIApplicationDelegate
的 Analyzer
略有不同。它是 RootAnalyzer
类,并使用一个 Manager
进行初始化。《Manager》在 Anna.iOS 和 Anna.Core 之间充当端口。它从 Anna.iOS 接收事件,并通过代理方法将计算结果返回。
Manager
首先运行初始化器中第一个参数 moduleURL
指向的模块。其行为可以通过dependency
(第二个参数)的属性进行更改。
属性 | 描述 |
---|---|
debug |
设置为 true 以自动重新加载任务。 |
coreJSModuleURL |
CoreJS 节点模块的位置。 |
fileManager |
通过它,CoreJS 能够访问文件系统。这在编写测试用例时方便地注入模拟值。 |
standardOutput |
通过它,CoreJS 能够访问标准输出。 |
而 Manager
返回的结果可以在 Manager.delegate
中接收。在 Manager.delegate
中的方法异步调用,因此如果不需要在主线程上调用它们,请通过 Manager.delegateQueue
进行配置。
方法 | 描述 |
---|---|
manager(_, didSend result) |
在计算出结果时调用。 |
manager(_, didCatch error) |
在发生未处理的错误时调用。 |
焦点标记
为了构建一个合适的焦点路径,Anna.iOS 必须知道哪个 Analyzer
(以及它绑定的 Node
)被聚焦。从 Responder Chain 和触摸事件获取与焦点相关的事件。对于 UIButton
和 UITableView
的情况可以自动处理。但是,在通过 UITapGestureRecognizer
、touchesEnded(with event)
和其他自定义情况检测到触摸事件时,需要手动处理聚焦标记。为了标记一个聚焦的 Analyzer
,我们需要在该 Analyzer
上调用 markFocused()
。
与 Analyzer 相关的方法
有几种类别的 Analyzer,它们都符合 Analyzing
协议。
在 Analyzing
上可用的方法
方法 | 描述 |
---|---|
enable(naming name) |
使 Analyzer 能够挂钩其代理,并以独立名称开始记录事件。 |
setSubAnalyzer(_ sub, for key) |
与另一个 Analyzer 建立键关系。当当前 Analyzer 启用时,子 Analyzer 将自动启用。 |
setSubAnalyzers(_ subs, for key) |
与其他 Analyzer 建立索引关系。当前 Analyzer 启用时,子 Analyzer 将自动启用。 |
record(_ event) |
在 Analyzer 上按名称记录事件。 |
update(_ value, for keyPath) |
在 Analyzer 上记录一个 值更新 事件。 |
observe(_ observee, for keyPath) |
开始观察对象的键路径。当值发生变化时,将记录一个 值更新 事件。 |
detach() |
终止所有挂钩和观察。通常在 deinit 中调用。 |
markFocused() |
标记 Analyzer 以便集中,这样新启用的 Analyzer 就可以在正确的集中路径上。 |
分析模块
Manager.init
中的参数 moduleURL
指向我们的分析模块的入口(一个节点模块)。它找到 Anna.Core 模块的位置,传入 task 模块的位置,并返回配置后的构建器给 Anna.iOS。一个经典的分析模块实现看起来像这样
/* In
* analytic.bundle/
* └── index.js
*/
module.exports = require('../anna.bundle').configured({
task: (__dirname + '/task')
});
任务注册
要注册的任务位于参数 task
所指向的模块中。模块中的 index.js
文件在创建任何关注路径的 Node
之前都会被 require
。其他文件根据创建关注路径 Node
的对象命名空间进行 require
。例如,当 DetailViewController
创建关注路径 Node
时,将会 require
DetailViewController.js
。因此,所有包含在 DetailViewController.js
中的任务都将被注册。
要注册在 Node
上触发特定事件时的任务,只需调用
match(
`focus/path/to/node/event`,
(node) => { return node.path; }
);
match
是在任务加载过程之前由 Anna.Core 注入的全局函数。第一个参数包含两个部分。最后一个组成部分之前的部分确定我们关心的 Node
类型。最后一个组成部分是我们的关心的事件。挖掘 函数(第二个参数)从 Node
中返回挖掘结果。
CoreJS
在 Anna.Manager
内部有一个名为 CoreJS 的小型 NodeJS 环境。它非常小,只实现了基本 require
函数(包括缓存)。CoreJS 以 require
入口模块开始,其 URL 通过 Manager.init
的第一个参数传入。Anna.Core 只是一个在入口模块中 require
的普通模块,其构造函数被分配给 exports
,最后返回给 Anna.iOS。
焦点路径节点
在 Anna.Core 中,Node
对象维护其上下文的引用,主要是它的父 Node
和所有祖先 Node
,这样就可以轻松知道用户焦点如何移动到当前 Node
。
从更抽象的角度来看,焦点路径代表用户过去与应用程序的交互。而“过去”本身是分析的对象。
在 Node
上可用的属性和函数
属性 | 描述 |
---|---|
nodeName |
Node 的名称。 |
index |
索引。 |
path |
从根到 Node 的路径。 |
parentNode |
当前 Node 的父 Node 。 |
ancestor(distance) |
当前 Node 的祖先 Node 。如果 distance 是 0,则引用当前 Node 。 |
latestEvent() |
在 Node 上发生的最新事件。 |
latestValue(keyPath) |
更新到 Node 的最新值,由 keyPath 识别。 |
isVisible() |
如果此 Node 上发生的最新 可见性事件 是 出现 并非 消失。 |
firstDisplayedEvent() |
在此 Node 上发生的第一次 出现 事件。 |
valueFirstDisplayedEvent(keyPath) |
第一次使值可见的事件。如果值已存在(不是 undefined 也不是 null ),则可以是 出现 事件;如果 Node 已可见,则可以是 更新 事件。 |
特殊事件
可见性
Anna.Core 通过接收事件 appeared 和 disappeared 记录每个 Node
的可见性。这两个事件是由挂钩 UIKit
对象生成的。例如,
UIView
报告 appeared,当它有一个父视图,位于窗口中并且未隐藏时。UITableViewCell
在调用代理的tableView(_ tableView, willDisplay cell, forRowAt indexPath)
时报告 appeared。UIViewController
在调用其viewDidAppeared
时报告 appeared。
基于可见性,可以执行诸如分析数据可见性等任务。
更新与观察
可以通过调用 update(_ value, for keyPath)
来跟踪一个 Node
上的值变化。通过关键字路径可以区分同一属性上的多个值变化。
如果我们想跟踪一个属性在其值变化时始终保持跟踪,可以调用 observe(_ observee, for keyPath)
。
调试
当 Anna 的行为不符合预期,尽管认为一切都是正确配置的,有时会感到沮丧。在这种情况下,我们可以调用 Manager.logSnapshot
来将标记文本记录到 Manager.dependency.standardOutput
,以便了解底层状态的所有细节。"快照" 包含
- 当前注册的所有聚焦路径
Node
- 注册在
Nodes
上的任务 - 在
Nodes
上最近发生的十个事件
一个 "快照" 的示例
<__root__ id="105553117049152" class="ana-node" createdAt="1530341056867">
<master id="105827995931584" class="ana-node" createdAt="1530341056870">
<ana-appeared class="ana-event" time="1530341056875" />
<ana-disappeared class="ana-event" time="1530341063401" />
<tableView id="105827995931808" class="ana-node" createdAt="1530341056872">
<cell id="105553117261952/0" class="ana-node" createdAt="1530341062226" index="0">
<match>
<branches length="1">
<did-select />
</branches>
</match>
<ana-updated key-path="text" value="2018-06-30 06:44:22 +0000" class="ana-event" time="1530341062227" />
<ana-appeared class="ana-event" time="1530341062227" />
<did-select class="ana-event" time="1530341062861" />
要调用 Manager.logSnapshot
而不向代码库添加额外的行,请暂停应用程序并输入 lldb 命令
e -l swift -- import MyApp; ((UIApplication.shared.delegate as! AppDelegate).analyzer as! RootAnalyzer).manager.logSnapshot()