Anna 0.3.2

Anna 0.3.2

coppercash维护。



Anna 0.3.2

  • 作者
  • William

Anna

Build Status codecov.io CocoaPods Compatible Platform License MIT Language

Anna提供了一个抽象层,有助于将分析代码与其它代码分离。

Anna包括两部分

  1. Anna.iOS 提供了一个名为 Analyzer 的类,类似于 MVVM 中的视图模型。它从 UIResponder(及其子类)钩子事件回调,然后将接收到的内容暴露给 Anna.Core。
  2. 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/

主要的区别从属于 DetailViewControllerNode detail 开始。在由 UIKit 提供的 响应者链 中,DetailViewController 的下一个响应者(树中的父节点)是 UINavigationController。然而,在 Anna.Core 中,detail 的父节点是 cell,这意味着用户的焦点从 cell 移动到 detail。这种行为是因为,从分析的角度来看,每个视图有用的信息通常属于用户之前关注的视图。因此,在 Anna.Core 中,从根到 Node 的每条路径实际上都是一条 聚焦路径

Analyzer

Analyzer 是对对象记录事件和与 Anna.Core 中的 Node 进行其他交互的接口。

超级-子 Analyzer 及父-子 Node

在大多数情况下,Analyzer 使用名称来识别它绑定的 NodeNode 父节点之间的键关系。该名称通常由其超 Analyzer 提供。然而,有时 Analyzer 的超级 Analyzer 无法直接访问它,在这种情况下,一个 独立 的名称也可以使用。

使用索引索引来分别为子 Analyzer 数组分配索引以表示索引关系。

超级 Analyzer 的所有者可以通过确认协议 Anna.AnalyzableObject 并实现方法 subAnalyzableKeys 来向其添加子 Analyzer。这样,子 Analyzer 就可以在关注路径中创建子 Node

如果一个 Analyzer 没有被任何其他 Analyzer 作为子 Analyzer 添加,它将查找响应者链以获取超级 Analyzer。在查找过程中,会调用 Anna.FocusPathConstituting.parentConstitutor()Anna.FocusPathConstitutionRedirecting.redirectedConstitutor() 来获取超级-子关系详细的详细信息。大多数 UIResponder 和其子类有这些方法的默认实现,但它们可以被覆盖以支持自定义行为。

根 Analyzer 及管理器

UIViewUIViewControllerUIControl 以及 Responder Chain 中的所有其他对象可以拥有一个 Analyzer,如果它们需要被分析,包括根响应者 - UIApplicationDelegate。但是,UIApplicationDelegateAnalyzer 略有不同。它是 RootAnalyzer 类,并使用一个 Manager 进行初始化。《Manager》在 Anna.iOSAnna.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 和触摸事件获取与焦点相关的事件。对于 UIButtonUITableView 的情况可以自动处理。但是,在通过 UITapGestureRecognizertouchesEnded(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 通过接收事件 appeareddisappeared 记录每个 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()