Отслеживание событий на стороне клиента в общих чертах делится на три категории:
- Отслеживание через код
- Визуальное отслеживание
- Автоматическое отслеживание
В этой статье в основном рассматриваются идеи и процесс реализации автоматического отслеживания.
1. Отслеживание через код
Отслеживание через код означает вызов интерфейса в каждой нужной точке с прямой отправкой данных события. Большинство сторонних сервисов аналитики предоставляют api для такого отслеживания, что довольно удобно.
Но возникает проблема: код отслеживания разбросан по всей бизнес-логике и тесно с ней связан. При изменении экранов или бизнес-процессов непонятно, что делать со старыми событиями и нужны ли новые. По мере роста числа событий сопровождение разрозненного кода отслеживания превращается в кошмар. Макросы и фабричные классы могут упростить код, но принципиально ничего не меняют.
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 埋点
APE(kEvent4001);
}
2. Визуальное отслеживание
При визуальном отслеживании точки сбора настраиваются через визуальный инструмент. Клиент автоматически разбирает конфигурацию и отправляет события, обеспечивая так называемое «отслеживание без следов». Один из характерных примеров с открытым исходным кодом — Mixpanel.
3. Автоматическое отслеживание
Автоматическое отслеживание, также называемое отслеживанием всех событий, не означает полного отсутствия инструментирования. Собираются и отправляются все события, а серверу остаётся отфильтровать полезные данные.
Для дальнейшей оптимизации сервер может отправлять файл конфигурации, чтобы клиент сам фильтровал события.
Ради KPI я заново изобрёл колесо на основе этой идеи и создал WKTrackingData.
Здесь я записываю некоторые проблемы и идеи, возникшие при реализации WKTrackingData.
Идея реализации проста. Код организован так:

Здесь WKTrackingDataManager отвечает за всё управление данными и настройку отслеживания событий.

WKTrackingDataViewPathHelper создаёт значения event_path.
AOP — модуль, отвечающий за отслеживание каждого события.
Подробности использования доступны на GitHub.
Как однозначно идентифицировать событие?
Здесь два вопроса: как представить событие и как обеспечить его уникальность?
Представление события
События представляются с помощью event_path. Для объекта responder, вызвавшего событие, основная идея — пройти по цепочке респондентов и построить её path.
Получившийся path респондента выглядит так:
"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
Однозначная идентификация события
В некоторых бизнес-сценариях один и тот же button или другой элемент управления представляет разные бизнес-события при изменении его свойств.
Например, button на главном экране может показывать Нажмите, чтобы войти до входа в аккаунт, а затем Подтвердите личность после входа, если проверка личности ещё не завершена.
Для того же button дерево представлений не изменилось, поэтому созданный path респондента остаётся прежним.
Похожие случаи — включённое и выключенное состояние UISwitch, свойство UISegmentedControl с именем indexSelect, UIStepper, а также взаимодействия в UITableView или UICollectionView с cell.
Для таких случаев WKTrackingData строит event_path, выборочно добавляя свойства самого элемента управления. Получившийся event_path тогда выглядит так:
"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate#currentTitle=Button#state=1#enabled=1#selected=0";
Расширения для бизнес-логики
Рассмотрим другой сценарий: карусель banner на главном экране.
Для каждого места banner сервер передаёт изображение и целевой url, а позиции можно настраивать.
Здесь у элемента banner с каждым index нет фиксированной страницы назначения. Событие на позиции 0 нельзя определить только по event_path.
Чтобы однозначно идентифицировать событие, нужно добавить параметры бизнес-логики, например url.
WKTrackingData также поддерживает расширение бизнес-параметров, позволяя вызывающей стороне добавлять собственные параметры:

Элементы управления, которые не нужно отслеживать, можно исключить через wk_ignoreTracking:
self.slider.wk_ignoreTracking = YES;
Другие проблемы
Для отслеживания разных событий WKTrackingData использует аспектно-ориентированный подход и напрямую подменяет реализации методов через runtime.
Но для отслеживания UIAlertView, UIActionSheet, UITableView и UICollectionView нужно подменять методы их delegate.
Если
delegate classуже реализует метод, достаточно подменить его.Если
delegate classего не реализует, а событие всё же нужно отслеживать, необходимо вручную добавить реализацию соответствующегоdelegate method.
- (void)wk_swizzleInstanceSelector:(SEL)origSel_ fromClass:(Class)fromClass replaceSelector:(SEL)replaceSel_ originNotImp:(SEL)notImpSel_ {
Method originalMethod = class_getInstanceMethod([self class], origSel_);
if (originalMethod) {
[self wk_swizzleInstanceSelector:origSel_ fromClass:fromClass replaceSelector:replaceSel_];
} else {
Method notImpMethod = class_getInstanceMethod(fromClass, notImpSel_);
// 如果delegateClass没有实现 origSel_ 方法
// 则给delegateClass的 origSel_ 添加 orginReplaceMethod 的实现
BOOL didAddNotImpMethod =
class_addMethod([self class],
origSel_,
method_getImplementation(notImpMethod),
method_getTypeEncoding(notImpMethod));
if (didAddNotImpMethod) {
NSLog(@"%@ did add not imp method %@" , NSStringFromClass([self class]) , NSStringFromSelector(notImpSel_));
}
}
}
Но на практике для UITableView и UICollectionView, если их объекты delegate не реализовывали метод, ручное добавление tableView:didSelectRowAtIndexPath: с implementation всё равно не приводило к его вызову.
Сначала посмотрим на обычный стек вызовов tableView:didSelectRowAtIndexPath::

Здесь UITableView последовательно вызывает четыре метода, чтобы отреагировать на нажатие cell:
- 1. [UITableView _userSelectRowAtPendingSelectionIndexPath:]
- 2. [UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:]
- 3. [UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:]
- 4. [TableViewController tableView:didSelectRowAtIndexPath:]
Из порядка вызовов видно, что UITableView вызывает у delegate class метод tableView:didSelectRowAtIndexPath: только после собственного метода _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:.
Поставьте точку останова на _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: и посмотрите ассемблерный код:

Если delegate class реализует tableView:didSelectRowAtIndexPath:, всё работает и вызов tableView:didSelectRowAtIndexPath: выполняется.
Но если delegate class не реализует tableView:didSelectRowAtIndexPath:, даже ручное добавление tableView:didSelectRowAtIndexPath: с implementation не приводит к попытке вызвать tableView:didSelectRowAtIndexPath:.
Проследив ассемблерный код, можно увидеть прямой jump к другой инструкции.
Здесь WKTrackingData использует другой подход: напрямую подменяет [UITableView _userSelectRowAtPendingSelectionIndexPath:] (аналогично для UICollectionView). Это позволяет UITableView и UICollectionView отслеживать взаимодействия, даже если объект delegate не реализует соответствующий метод события cell.
4. Итоги
Несколько значений event_path для одного события
Представьте несколько выпущенных версий с небольшими различиями в экранах. Тогда у одного события может быть несколько значений event_path.
На практике такое сопоставление требует помощи коллег, работающих над сервером. Его можно сделать настраиваемым в серверной части.
Оптимизация аналитики событий
В разделе расширений для бизнес-логики описано добавление бизнес-параметров. WKTrackingData реализует это через категории.
На практике отслеживание всех событий приводило к большому расходу трафика впустую. Многие события были не нужны [(ŏ_ŏ)].
Следующая оптимизация — отправлять с сервера файл конфигурации со списком всех событий, которые нужно передавать. Отправляется только event_path, соответствующий конфигурации.
На этом этапе бизнес-параметры можно получать и без WKTrackingData: напрямую через kvc.
Например:
key_path = "viewController.banner.url"
Если вы дочитали до этого места, откройте WKTrackingData и поставьте звезду. Если есть вопросы, создайте issue на GitHub или оставьте комментарий ниже!