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:

Kiến trúc WKTrackingData nguyên bản: trình quản lý, đường dẫn sự kiện và mô-đun AOP

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.

Ví dụ sử dụng nguyên bản cho 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:

Ví dụ mã nguyên bản mở rộng sự kiện bằng tham số nghiệp vụ

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 class chư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ủa delegate method tươ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_));
        }
    }
}

Phần triển khai nằm ở đây

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::

Call stack trình gỡ lỗi nguyên bản khi chọn một hàng trong table view

Ở đâ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:

Khung nhìn assembly nguyên bản cho thấy nhánh phương thức delegate

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!