Theo dõi sự kiện phía máy khách nhìn chung được chia thành ba loại:
- Theo dõi bằng mã
- Theo dõi trực quan
- Theo dõi tự động
Bài viết này chủ yếu giới thiệu ý tưởng và quá trình triển khai theo dõi tự động.
1. Theo dõi bằng mã
Theo dõi bằng mã có nghĩa là gọi một giao diện tại từng điểm cần theo dõi và trực tiếp tải dữ liệu sự kiện lên. Phần lớn dịch vụ phân tích bên thứ ba đều cung cấp api theo dõi bằng mã, rất tiện lợi.
Nhưng điều này tạo ra một vấn đề: mã theo dõi nằm rải rác trong logic nghiệp vụ và gắn chặt với nó. Khi màn hình hoặc luồng nghiệp vụ thay đổi, ta không biết nên xử lý sự kiện cũ thế nào hay có cần sự kiện mới không. Khi số lượng sự kiện tăng lên, việc bảo trì mã theo dõi rải rác trở thành cơn ác mộng. Macro và lớp factory có thể đơn giản hóa mã, nhưng không thay đổi được bản chất.
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 埋点
APE(kEvent4001);
}
2. Theo dõi trực quan
Theo dõi trực quan cấu hình các điểm thu thập bằng công cụ trực quan. Máy khách tự động phân tích cấu hình và báo cáo sự kiện, tạo ra cái gọi là “theo dõi không dấu vết”. Một giải pháp nguồn mở tiêu biểu là Mixpanel.
3. Theo dõi tự động
Theo dõi tự động, còn gọi là theo dõi toàn bộ sự kiện, không thật sự có nghĩa là không cần gắn công cụ. Nó thu thập và báo cáo mọi sự kiện, để máy chủ lọc ra dữ liệu hữu ích.
Để tối ưu thêm, máy chủ có thể gửi tệp cấu hình để máy khách tự lọc sự kiện.
Vì KPI, tôi đã làm lại một công cụ theo ý tưởng này và xây dựng WKTrackingData.
Ở đây tôi ghi lại một số vấn đề và ý tưởng trong quá trình triển khai WKTrackingData.
Ý tưởng triển khai rất đơn giản. Mã được tổ chức như sau:

Trong đó, WKTrackingDataManager xử lý toàn bộ việc quản lý dữ liệu và cấu hình theo dõi sự kiện.

WKTrackingDataViewPathHelper tạo các giá trị event_path.
AOP là mô-đun chịu trách nhiệm theo dõi mọi sự kiện.
Để xem cách dùng chi tiết, hãy truy cập GitHub.
Làm sao nhận dạng duy nhất một sự kiện?
Có hai câu hỏi: biểu diễn một sự kiện thế nào, và làm sao bảo đảm tính duy nhất?
Biểu diễn một sự kiện
Sự kiện được biểu diễn bằng event_path. Với responder kích hoạt sự kiện, ý tưởng cốt lõi là đi theo chuỗi responder của nó và xây dựng path của chuỗi đó.
Đường dẫn responder path thu được trông như sau:
"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
Nhận dạng duy nhất một sự kiện
Trong một số tình huống nghiệp vụ, cùng một button hoặc điều khiển khác đại diện cho các sự kiện nghiệp vụ khác nhau khi thuộc tính của nó thay đổi.
Ví dụ, một button trên màn hình chính có thể hiển thị Chạm để đăng nhập khi chưa đăng nhập, rồi hiển thị Xác minh danh tính sau khi đăng nhập nếu vẫn chưa hoàn tất xác minh.
Với cùng button đó, cây view không thay đổi nên path responder được tạo vẫn giống nhau.
Những trường hợp tương tự gồm trạng thái bật/tắt của UISwitch, thuộc tính indexSelect của UISegmentedControl, UIStepper, và tương tác với một cell trong UITableView hoặc UICollectionView.
Để xử lý, WKTrackingData tạo event_path bằng cách chọn lọc và nối thêm thuộc tính của chính điều khiển. event_path thu được trông như sau:
"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate#currentTitle=Button#state=1#enabled=1#selected=0";
Mở rộng theo nghiệp vụ
Hãy xét một tình huống khác: một băng chuyền banner trên màn hình chính.
Mỗi vị trí banner có hình ảnh và url đích do máy chủ cung cấp, và vị trí của chúng có thể cấu hình.
Ở đây, banner tại mỗi index không có đích cố định. Không thể xác định sự kiện ở vị trí 0 chỉ bằng event_path.
Để nhận dạng duy nhất sự kiện, ta cần nối thêm tham số nghiệp vụ cụ thể như url.
WKTrackingData cũng hỗ trợ mở rộng tham số nghiệp vụ, cho phép bên gọi nối thêm tham số tùy chỉnh:

Có thể loại trừ những điều khiển không nên theo dõi bằng wk_ignoreTracking:
self.slider.wk_ignoreTracking = YES;
Các vấn đề khác
Để theo dõi các sự kiện khác nhau, WKTrackingData dùng cách tiếp cận hướng khía cạnh và sử dụng runtime để hoán đổi phương thức trực tiếp.
Tuy nhiên, để theo dõi UIAlertView, UIActionSheet, UITableView và UICollectionView, ta cần hoán đổi các phương thức delegate của chúng.
Nếu
delegate classđã triển khai phương thức, chỉ cần hoán đổi.Nếu
delegate classchưa triển khai mà ta vẫn muốn theo dõi sự kiện, cần thêm thủ công phần triển khai củadelegate methodtương ứng.
- (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_));
}
}
}
Tuy vậy, trong thực tế, khi đối tượng delegate của UITableView và UICollectionView chưa triển khai phương thức, việc thêm thủ công tableView:didSelectRowAtIndexPath: bằng một implementation vẫn không khiến nó được gọi.
Trước tiên, hãy xem call stack thông thường của tableView:didSelectRowAtIndexPath::

Ở đây, UITableView lần lượt gọi bốn phương thức sau để phản hồi một lần chạm cell:
- 1. [UITableView _userSelectRowAtPendingSelectionIndexPath:]
- 2. [UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:]
- 3. [UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:]
- 4. [TableViewController tableView:didSelectRowAtIndexPath:]
Thứ tự gọi cho thấy UITableView chỉ gọi phương thức tableView:didSelectRowAtIndexPath: của delegate class sau khi gọi phương thức của chính nó là _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:.
Đặt breakpoint tại _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: rồi kiểm tra mã assembly:

Nếu delegate class triển khai tableView:didSelectRowAtIndexPath:, mọi thứ hoạt động và lời gọi tableView:didSelectRowAtIndexPath: hoàn tất.
Nhưng nếu delegate class không triển khai tableView:didSelectRowAtIndexPath:, ngay cả khi thêm thủ công tableView:didSelectRowAtIndexPath: bằng một implementation, hệ thống vẫn không cố phản hồi tableView:didSelectRowAtIndexPath:.
Lần theo mã assembly cho thấy luồng thực thi jump thẳng đến một chỉ thị khác.
WKTrackingData dùng cách khác ở đây: trực tiếp hoán đổi [UITableView _userSelectRowAtPendingSelectionIndexPath:] (với cách tương tự cho UICollectionView). Nhờ đó UITableView và UICollectionView có thể theo dõi tương tác ngay cả khi đối tượng delegate không triển khai phương thức sự kiện cell tương ứng.
4. Lời kết
Nhiều giá trị event_path cho cùng một sự kiện
Hãy xét nhiều phiên bản đã phát hành với khác biệt nhỏ trên màn hình. Khi đó cùng một sự kiện có thể có nhiều giá trị event_path.
Trong thực tế, ánh xạ này cần sự phối hợp từ đồng nghiệp phía máy chủ. Nó có thể được cấu hình trong backend.
Tối ưu phân tích sự kiện
Phần mở rộng theo nghiệp vụ đã mô tả việc thêm tham số nghiệp vụ. WKTrackingData triển khai điều này bằng category.
Trong thực tế, theo dõi toàn bộ sự kiện đã lãng phí rất nhiều băng thông. Nhiều sự kiện trong số đó không cần thiết [(ŏ_ŏ)].
Một cách tối ưu thêm là để máy chủ gửi tệp cấu hình liệt kê mọi sự kiện cần báo cáo. Chỉ event_path khớp cấu hình mới được tải lên.
Ở bước này, cũng có thể lấy tham số nghiệp vụ mà không dùng cách triển khai của WKTrackingData: thay vào đó dùng trực tiếp kvc.
Ví dụ:
key_path = "viewController.banner.url"
Nếu đã đọc đến đây, hãy mở WKTrackingData và tặng dự án một ngôi sao. Nếu có câu hỏi, hãy mở issue trên GitHub hoặc để lại bình luận bên dưới!