統計打點是 App 開發裏很重要的一個環節,App 的運行狀態、改版後的效果、用戶的各類行爲等都須要打點,市面上也有很多可供選擇的第三方庫。 假設產品有這麼個需求:當用戶在詳情頁點擊購買按鈕時,記錄一下事件。咱們實現起來大概會是這樣html
// DetailViewController.m - (void)onBuyButtonTapped:(UIButton *)button { // do some stuff, maybe send a request to server [XXXAnalytics event:kSomeEventYouDefined]; }
這個需求就這樣輕鬆搞定了,但細細想一想仍是有很多問題的:ios
頁面上會有其餘的 Button,可能每一個 Button 都要放上這麼一段代碼。程序員
這些統計其實跟具體的業務無關,不必跟業務代碼混雜在一塊兒,不優雅。編程
當改版或者重構時,有可能忘了把相應的打點代碼遷移過去。後端
因此須要一種更好的方式來作這件事,這就是使用 AOP(Aspect-Oriented-Programming),翻譯過來就是「面向切面編程」網絡
經過預編譯方式和運行期動態代理實如今不修改源代碼的狀況下給程序動態統一添加功能的一種技術。app
簡單來講,就是能夠動態的在函數調用的先後插一段代碼。iOS 可使用 Pete Steinberger 開發的 Aspects 這個庫,大體原理是在 runtime 層,經過 swizzle method 來實現的。函數
來看一個小 Demo性能
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) { NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated); } error:NULL];
這樣在 UIViewController 的 viewWillAppear: 被調用後,還會再調一下咱們定義的 Block,這段日誌就會被輸出。而打點正好符合這種場景:正事幹完以後,額外幹一些跟業務無關的事情。測試
上面的例子,咱們經過 AOP 來作的話,大概就是這樣
// DetailViewController.m - (void)onBuyButtonTapped:(UIButton *)button { // do some stuff, maybe send a request to server // no need to call [XXXAnalytics event:] } // AppDelegate.m - (void)setupAnalytics { [DetailViewController aspect_hookSelector:@selector(onBuyButtonTapped:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) { [XXXAnalytics event:kSomeEventYouDefined]; } error:NULL]; }
這樣統計代碼就從業務代碼中剝離出來了。可是又產生了一個新問題,多個 Button Event,豈不是要寫不少行這樣的代碼,「重複」這樣的事情,做爲一個程序員怎麼能忍,簡單,造一個方法
- (void)trackEventWithClass:(Class)klass selector:(SEL)selector event:(NSString *)event { [klass aspect_hookSelector:@selector(selector) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) { [XXXAnalytics event:event]; } error:NULL]; }
使用起來就像這樣
- (void)setupAnalytics { [self trackEventWithClass:DetailViewController selector:@seletor(onBuyButtonTapped:) event:kSomeEventYouDefined]; [self trackEventWithClass:ListViewController selector:@seletor(followButtonTapped:) event:kAnotherEventYouDefined]; // ... }
看起來又幹淨了些。這時,產品經理又提了個需求:當這個按鈕點擊時,若是已經登陸了,發送 EventA,若是沒有登陸則發送 EventB,也就是說,再也不只是 [XXXAnalytics event:] 這麼簡單了,還須要加上額外的邏輯,這也難不倒咱們,加上一個 block 便可。
- (void)trackEventWithClass:(Class)klass selector:(SEL)selector eventHandler:(void (^)(id<AspectInfo> aspectInfo))eventHandler { [klass aspect_hookSelector:@selector(selector) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) { if (eventHandler) { eventHandler(aspectInfo); } } error:NULL]; } // 使用 [self trackEventWithClass:DetailViewController selector:@seletor(onBuyButtonTapped:) eventHandler:^(id<AspectInfo> aspectInfo){ user.loggedIn ? [XXXAnalytics event:EventA] : [XXXAnalytics event:EventB]; }];
好了,如今只要不是太複雜的打點邏輯(那些須要方法上下文變量的)咱們都能應付了,接下來就該等產品來驗收了。產品搬了個凳子坐在身邊,而後點一下 Button,看一下 Console,被幾輪蹂躪後,產品也慢慢地接受了這種驗收方式。後來某一天,突然發現某一項或某幾項數據有異常,而後找到開發,瞄了一眼:哦,這個方法被重構了。或者新加的方法忘了加統計了。只能等到下個版本再加上了,若是隻是通常的統計數據倒還好,跟錢相關的就麻煩了。
那麼有沒有一種直觀的驗證方式呢?固然,程序員是萬能的呀。一個理想的情況是,產品打開 App 後,開啓某個開關就能看到全部會發送 Event 的按鈕,就像這樣
其中數字表明瞭 EventID。如何實現呢?還記得註冊事件時,咱們有傳入 class 和 selector 麼,通常咱們都會有一個 BaseViewController,那麼就能夠在 BaseViewController 的 viewDidAppear: 裏作點文章了。
// BaseViewController.m - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; // 獲取已經註冊過的 classes NSDictionary *registeredClasses = [OurAnalytics sharedInstance].registeredClasses; [registeredClasses enumerateKeysAndObjectsUsingBlock:^(NSString *className, NSArray *selectors, BOOL *stop) { if ([self isKindOfClass:NSClassFromString(className)]) { // 如何根據 selector 找到它的宿主? } }]; }
因此如今問題就剩下,如何根據 selector 找到對應的 Button,這裏要注意,有些 Button 可能要等網絡請求完成纔會出現,好比 TableViewCell 裏的 Button。
沒有想到太方便的方法,簡單粗暴點就是設置個 Timer 每隔一段時間掃一下 subviews,若是是 button 或 包含 tapGesture 的,就拿它們的 action 對比一下,若是 match 就能夠高亮那個 button / view 了。
EventID 也同樣,以前在註冊時也會傳一個 EventID 過來,這裏直接顯示出來便可。對於那些傳 eventHandler 的就不行了。
因此理論上是可行的,性能上會稍微有點損耗,尤爲是當 subViews 的結構比較複雜時,不過只是內部用來作驗證,因此這也不是什麼問題。
看起來效果已經不錯了,有沒有可能讓這套體系再靈活一些?好比能夠從後端制定打點規則?客戶端只是讀取一個配置文件,就像這樣
- (void)setupAnalytics { // analyticsRules 是從配置文件中讀取出來的 [analyticsRules enumerateObjectsUsingBlock:^(NSDictionary *rules, NSUInteger idx, BOOL *stop) { Class klass = NSClassFromString(rules[@"class"]); SEL selector = NSSelectorFromString(rules[@"selector"]); NSString *eventID = rules[@"eventID"]; [self trackEventWithClass:klass seletor:seletor event: eventID]; }]; }
那若是在後臺的時候填錯了 Class 或 Selector 怎麼辦?還好有 objc_getClassList 和 class_copyMethodList 這兩個運行時方法,有了它們就能夠在 App 啓動時掃一遍已註冊的類(過濾掉 UI / NS 開頭的),而後將它們的 seletor 也一併保存下來發送給服務端,固然這種操做只需在適當的時機作一下就能夠了,好比集成打包時。
如今,這套體系就比較完整了。固然這只是個人一些構想,並無在實踐中嘗試過,因此確定會踩到各類各樣的坑,不過至少看起來是個可行的方案。
[1] iOS 統計打點那些事
http://limboy.me/ios/2015/09/09/ios-analytics.html
[2] 基於Aspects和JSPatch的熱埋點方案
[3] 移動APP日誌上報優化實踐
[4] 天貓App A/B測試實踐
[5] 統計埋點的那些事
http://www.jianshu.com/p/973d626fa19a?from=timeline
[6] 用戶行爲的深度追蹤——事件與埋點
http://www.cnblogs.com/ventlam/p/6414584.html
[7] Android無埋點數據收集SDK關鍵技術