大咖说:React Native 全埋点实现原理(内附赠书)
本文主要介紹如何實現(xiàn) React Native 的全埋點,主要是控件點擊 $AppClick 事件。該內(nèi)容,會默認你有一定的 React Native 開發(fā)經(jīng)驗,(若沒有,也可參與文末贈書)。
■?作者?■
王灼洲 合肥研發(fā)中心負責人
《Android 全埋點解決方案》和《iOS?全埋點解決方案》一書作者,有 10+ 年 Android & iOS 相關開發(fā)經(jīng)驗,是國內(nèi)第一批從事 Android 研發(fā)工作,開發(fā)和維護國內(nèi)第一個商用的開源 Android & iOS 數(shù)據(jù)埋點 SDK。
溫馨提示:文末附贈書。
一、React Native 簡介
React Native 是由 Facebook 推出的移動應用開發(fā)框架,可以用來開發(fā) iOS、Android、Web 等跨平臺應用程序,官網(wǎng)為:
https://facebook.github.io/react-native/。
React Native 和傳統(tǒng)的 Hybrid 應用最大的區(qū)別就是它拋開了 WebView 控件。React Native 產(chǎn)出的并不是“網(wǎng)頁應用”、“HTML5 應用”或者“混合應用”,而是一個真正的移動應用,從使用感受上和用 Objective-C 或 Java 編寫的應用相比幾乎是沒有區(qū)分的。React Native 所使用的基礎 UI 組件和原生應用完全一致。我們要做的就是把這些基礎組件使用 JavaScript 和 React 的方式組合起來。React Native 是一個非常優(yōu)秀的跨平臺框架。
下面我們先用 React Native 創(chuàng)建一個簡單的 Demo。
1.1
創(chuàng)建項目
使用 React Native 開發(fā)移動應用, 首先需要安裝 React Native 相關的組件。具體的安裝方法,可以參照 React Native 官方介紹。
React Native 安裝完成之后,就可以使用命令行工具創(chuàng)建新項目了。
react-native init AwesomeProject上面的命令創(chuàng)建了一個名為 AwesomeProject 的項目,然后就可以通過下面的命令進入 AwesomeProject 文件夾并運行 iOS 程序。
cd AwesomeProjectreact-native run-ios然后等待一會,iOS 模擬器將會啟動,就可以看到如下圖 1-1 所示的運行結(jié)果。
圖 1-1?模擬器
在命令行輸入下面的命令,Xcode 將會打開上面創(chuàng)建的 AwesomeProject 項目。
open ./ios/AwesomeProject.xcworkspaceXcode 中,就可以看到 AwesomeProject 項目相關的代碼。首先,需要把 SensorsSDK 項目添加進來。Xcode 中,點擊?File → Add Files to "AwesomeProject" ...,會彈出如下圖 1-2 所示的對話框,選擇 SensorsSDK.xcodeproj 文件,并勾選相應的 Target,最后點擊 Add 按鈕。
圖 1-2 添加項目
然后,還需要添加相應的依賴關系。選中 AwesomeProject 項目,在 General 標簽的 Frameworks 欄中點擊加號(+)按鈕,添加 SensorsSDK.framework。
最后,?AppDelegate.m 中引入 SensorsSDK,并在?- application:didFinishLaunchingWithOptions: 方法中調(diào)用 SensorsAnalyticsSDK 的 - startWithServerURL: 初始化方法初始化 SDK。
#import <SensorsSDK/SensorsSDK.h>@implementation AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {[SensorsAnalyticsSDK startWithServerURL:@"xxxxx"];......return YES; }@end運行 AwesomeProject 項目,我們就可以在 Xcode 控制臺中看到?$AppStart 事件信息。
按 Home 鍵或上滑 HomeBar 讓應用程序進入后臺,將會在 Xcode 控制臺中看到 $AppEnd 事件。
{"properties": {"$model": "x86_64","$manufacturer": "Apple","$lib_version": "1.0.0","$os": "iOS","$event_duration": 434917.40625,"$app_version": "1.0","$os_version": "12.3","$lib": "iOS"},"event": "$AppEnd","time": 1576141581203,"distinct_id": "D13CE550-1EE4-45B4-AB83-CDF7601C9C77" }從而也可以說明,對于 React Native 項目的?$AppStart 和 $AppEnd 事件,我們無需做任何特殊處理,即可直接支持。
其實在控制臺中也會打印頁面瀏覽($AppViewScreen)事件,但是這個事件在嚴格意義上來說不應該屬于 ReactNative 應用程序的頁面瀏覽事件,這個只是應用程序中的 UIWindow 控件的根視圖控制器的頁面瀏覽事件。而實際上在 React Native 中,是使用 react-navigation 進行頁面間的跳轉(zhuǎn)。對于 iOS 來說,跳轉(zhuǎn)的新頁面并不是一個視圖控制器,而是彈出一個視圖,因此并不能采集到正確的頁面瀏覽事件。
1.2
基礎控件
React Native 支持的控件有很多,詳細可以參照 React Native 官網(wǎng)的相關介紹和說明:
https://facebook.github.io/react-native/docs/activityindicator。
下面我們以 React Native 的 Switch 控件為例來做介紹。
可以通過修改 AwesomeProject 項目中的 App.js 文件,在頁面中添加一個 Switch 組件。
import React, { Component } from 'react'; import {SafeAreaView,StyleSheet,ScrollView,View,Text,Switch,StatusBar, } from 'react-native';import {Header,LearnMoreLinks,Colors,DebugInstructions,ReloadInstructions, } from 'react-native/Libraries/NewAppScreen';export default class App extends Component {state = {value: false,}render() {return (<><StatusBar barStyle="dark-content" /><SafeAreaView><ScrollViewcontentInsetAdjustmentBehavior="automatic"style={styles.scrollView}><Header />{global.HermesInternal == null ? null : (<View style={styles.engine}><Text style={styles.footer}>Engine: Hermes</Text></View>)}<View style={styles.body}><View style={styles.pContainer}><Text style={styles.pTitle}>Components</Text><View style={styles.pContainer}><Switch style={{ marginLeft: 20 }} value={this.state.value} thumbColor='black' onValueChange={(value) => {this.setState({value: value})}} /></View></View></View></ScrollView></SafeAreaView></>);} };const styles = StyleSheet.create({body: {backgroundColor: Colors.white,},pContainer: {marginTop: 32,paddingHorizontal: 24,}, });使用 Xcode 運行應用程序,可以得到如下圖 1-3 顯示效果。
圖 1-3 運行效果
React Native 的 Switch 控件和 iOS 原生中的 UISwitch 控件是類似的。打開或者關閉 Switch,在 Xcode 的控制臺中,均可以看到正常觸發(fā)了 $AppClick 事件 。
{"properties": {"$model": "x86_64","$manufacturer": "Apple","$element_type": "RCTSwitch","$lib_version": "1.0.0","$os": "iOS","$element_content": "checked","$app_version": "1.0","$screen_name": "UIViewController","$os_version": "12.3","$lib": "iOS"},"event": "$AppClick","time": 1576142976118,"distinct_id": "E934E526-6517-4CA1-A61E-0DCE2172D56A" }從而也可以看出,即使我們在 SensorsSDK 中沒有做任何修改,也可以正常采集 React Native 中 Switch 控件的點擊事件。從 $element_type 屬性可以看到,在 React Native 中,Switch 控件所對應的類是 RCTSwitch 。
下面,我們繼續(xù)查看 RCTSwitch 相關的源碼。
RCTSwitch.h 定義如下:
/*** Copyright (c) Facebook, Inc. and its affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.*/#import <UIKit/UIKit.h>#import <React/RCTComponent.h>@interface RCTSwitch : UISwitch@property (nonatomic, assign) BOOL wasOn; @property (nonatomic, copy) RCTBubblingEventBlock onChange;@endRCTSwitch.m 實現(xiàn)如下:
從代碼中可以看出,RCTSwitch 其實是繼承自 UISwitch 的子類控件。在前面的章節(jié),我們已經(jīng)在 SensorsSDK 中實現(xiàn)了 iOS 原生 UISwitch 控件的 $AppClick 事件全埋點,所以自然也就支持了 React Native 的 RCTSwitch 控件的?$AppClick 事件全埋點。
在 React Native 中,類似于 Switch 控件的還有 Slider、SegmentedControlIOS 等控件。因此,對于這些控件來說,已可以支持采集它們的 $AppClick 事件。但是,對于 React Native 中的 Button 控件來說,情況就不太一樣了。
我們可以先試驗一下,修改 App.js 文件, 在頁面中 UISwitch 控件的下方添加一個Button 控件。
import React, { Component } from 'react'; import {SafeAreaView,StyleSheet,ScrollView,View,Text,Switch,Button,Alert,StatusBar, } from 'react-native';import {Header,Colors, } from 'react-native/Libraries/NewAppScreen';export default class App extends Component {state = {value: false,}render() {return (<><StatusBar barStyle="dark-content" /><SafeAreaView><ScrollViewcontentInsetAdjustmentBehavior="automatic"style={styles.scrollView}><Header />{global.HermesInternal == null ? null : (<View style={styles.engine}><Text style={styles.footer}>Engine: Hermes</Text></View>)}<View style={styles.body}><View style={styles.pContainer}><Text style={styles.pTitle}>Components</Text><View style={styles.pContainer}><Switch style={{ marginLeft: 20 }} value={this.state.value} thumbColor='black' onValueChange={(value) => {this.setState({value: value})}} /></View><View style={styles.pContainer}><Button title="Press me" onPress={() => Alert.alert('Simple Button pressed')} /></View></View></View></ScrollView></SafeAreaView></>);} };const styles = StyleSheet.create({scrollView: {backgroundColor: Colors.lighter,},engine: {position: 'absolute',right: 0,},body: {backgroundColor: Colors.white,},pContainer: {marginTop: 32,paddingHorizontal: 24,},pTitle: {fontSize: 24,fontWeight: '600',color: Colors.black,},highlight: {fontWeight: '700',},footer: {color: Colors.dark,fontSize: 12,fontWeight: '600',padding: 4,paddingRight: 12,textAlign: 'right',}, });保存,可以看到在 Switch 控件的下方就出現(xiàn)了我們剛添加的按鈕。點擊按鈕,彈出了一個提示窗口,和 iOS 系統(tǒng)里的 UIAlert 的顯示效果相同,如下圖 1-4 所示。
圖 1-4?提示窗口
但是此時,我們在 Xcode 的控制臺中并沒有看到觸發(fā)了 $AppClick 事件。
那么我們?nèi)绾螌崿F(xiàn) React Native 中 Button 控件的 $AppClick 事件全埋點呢?
二、React Native 全埋點
在實現(xiàn) Button 控件的 $AppClick 事件全埋點之前,我們先簡單的介紹一下 React Native 的事件響應機制。
2.1
事件響應
在 React Native 中,觸摸事件響應會涉及到 JavaScript 端和 Native 端,這里的 Native 端指的是 iOS 端,本章的內(nèi)容暫不涉及 Android 部分。
我們使用 Xcode 打開 AwesomeProject 項目,查看 Pod 工程中 React Native 的源碼,通過類名我們很容易找到兩個與觸摸事件相關的類:
RCTTouchEvent
RCTTouchHandler
RCTTouchEvent 類實現(xiàn)了 RCTEvent 協(xié)議。從觸摸開始、移動到觸摸結(jié)束或取消,都會創(chuàng)建一個 RCTTouchEvent 類的對象,用來描述觸摸的各個不同階段。在 Native 端,將觸摸狀態(tài)發(fā)送到 JavaScript 端的過程中,傳遞的也是 RCTTouchEvent 類的對象。其實, RCTTouchEvent 類的對象就是在 RCTTouchHandler 類中創(chuàng)建的。
RCTTouchHandler 類繼承自?
UIGestureRecognizer 類,也就是說
RCTTouchHandler 類其實就是一個手勢識別器,它重寫了觸摸響應傳遞的以下幾個方法。
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent *)event;事實上,以上幾個方法都會調(diào)用?- _updateAndDispatchTouches:eventName: 方法。在該方法中,使用 RCTTouchEvent 類的對象來描述當前的觸摸狀態(tài)。由于 RCTTouchHandler 類也是一個手勢識別器,因此需要將其添加到一個視圖中才能響應觸摸事件。
我們先來看看,?AwesomeProject 項目 AppDelegate.m 文件中- application:didFinishLaunchingWithOptions: 方法的實現(xiàn)。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [SensorsAnalyticsSDK startWithServerURL:@"xxxx"];[[SensorsAnalyticsSDK sharedInstance] enableTrackReactNativeEvent];RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridgemoduleName:@"AwesomeProject"initialProperties:nil];rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];UIViewController *rootViewController = [UIViewController new];rootViewController.view = rootView;self.window.rootViewController = rootViewController;[self.window makeKeyAndVisible];return YES; }可以看到,首先創(chuàng)建了一個?RCTRootView 對象作為一個視圖控制器的視圖,然后將該視圖控制器設置為 window 對象的根視圖控制器。在 RCTRootView 類中,有一個很重要的視圖對象,即 RCTRootContentView 類型的?_contentView。
這個視圖對象是在 JavaScript 包加載完成之后創(chuàng)建的。在 React Native 中,所有 JavaScript 端生成的頁面其實都是添加在這個視圖對象中。在?_contentView 創(chuàng)建的時候,同時也會創(chuàng)建 RCTTouchHandler 類的對象并調(diào)用 - attachToView: 方法,將手勢識別器添加到 _contentView 中。這也就意味著,在?_contentView 中發(fā)生的所有觸摸事件都會交由 RCTTouchHandler 類的對象進行處理。
那么,是否交換了 RCTTouchHandler 類的?- _updateAndDispatchTouches:eventName: 方法就可以采集到控件的 $AppClick 事件了呢?
雖然我們通過這種方法,能接收到所有的觸摸事件,但是在這個方法中,我們無法知道在 JavaScript 端到底是哪個控件響應了觸摸事件。因此,此種實現(xiàn)方案并不可取,不能滿足我們實際的全埋點采集需求。
我們繼續(xù)往下分析。
在 RCTTouchHandler 類的對象進行處理完成之后,會通過一系列方法將觸摸事件發(fā)送到 JavaScript 端。在 JavaScript 端也實現(xiàn)了類似于 Native 端的觸摸事件處理機制——手勢響應系統(tǒng)。每個觸摸事件都可以通過手勢響應系統(tǒng)找到能夠響應的組件,并執(zhí)行響應事件。當觸摸事件找到響應者時,會觸發(fā) ReactNativeGlobalResponderHandler.js 的 onChange 方法,相關代碼片段如下。
// Module provided by RN: var ReactNativeGlobalResponderHandler = {onChange: function(from, to, blockNativeResponder) {if (to !== null) {var tag = to.stateNode._nativeTag;ReactNativePrivateInterface.UIManager.setJSResponder(tag,blockNativeResponder);} else {ReactNativePrivateInterface.UIManager.clearJSResponder();}} };從上面的代碼可以看出,當響應控件觸摸事件的時候,JavaScript 端會調(diào)用?UIManager 中的?- setJSResponder: 方法,然后調(diào)用?
Native 端的 RCTUIManager 類中的?
- setJSResponder:blockNativeResponder: 方法。該方法的實現(xiàn)代碼較少,參考如下。
/*** JS sets what *it* considers to be the responder. Later, scroll views can use* this in order to determine if scrolling is appropriate.*/ RCT_EXPORT_METHOD(setJSResponder:(nonnull NSNumber *)reactTagblockNativeResponder:(__unused BOOL)blockNativeResponder) {[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {_jsResponder = viewRegistry[reactTag];if (!_jsResponder) {RCTLogWarn(@"Invalid view set to be the JS responder - tag %@", reactTag);}}]; }這個方法有兩個參數(shù),通過第一個參數(shù) reactTag 我們可以獲取到響應者。
介紹到這里,我們已經(jīng)有實現(xiàn) React Native 中 Button 控件 $AppClick 事件的全埋點方案了,那就是交換 RCTUIManager 類中的 - setJSResponder:blockNativeResponder: 方法。
2.2
$AppClick 事件
下面,我們詳細介紹實現(xiàn)步驟。
第一步:在項目?SensorsSDK 中給?SensorsAnalyticsSDK?
新增一個類別?ReactNative,并新增一個?
- enableTrackReactNativeEvent 方法,用來開啟 React Native 中 $AppClick 事件的全埋點功能。
在 SensorsAnalyticsSDK.h 文件中,類別 ReactNative 聲明如下:
@interface SensorsAnalyticsSDK (ReactNative)- (void)enableTrackReactNativeEvent;@end在 SensorsAnalyticsSDK.m 文件中,類別 ReactNative 實現(xiàn)如下:
#import <objc/runtime.h>@implementation SensorsAnalyticsSDK (ReactNative)/** * 交換兩個方法的實現(xiàn) * * @param className 需要交換的類名稱 * @param methodName1 被交換的方法名,即原始的方法 * @param methodName2 交換后的方法名,即新的實現(xiàn)方法 * @param method2IMP 交換后的方法實現(xiàn) */ static inline void sensorsdata_method_exchange(const char *className, const char *methodName1, const char *methodName2, IMP method2IMP) {// 通過類名獲取類Class cls = objc_getClass(className);// 獲取原始方法的名SEL selector1 = sel_getUid(methodName1);// 通過方法名獲取方法指針Method method1 = class_getInstanceMethod(cls, selector1);// 獲得指定方法的描述struct objc_method_description *desc = method_getDescription(method1);if (desc->types) {// 把交換后的實現(xiàn)方法注冊到 runtime 中SEL selector2 = sel_registerName(methodName2);// 通過運行時,把方法動態(tài)添加到類中if (class_addMethod(cls, selector2, method2IMP, desc->types)) {// 獲取實例方法Method method2 = class_getInstanceMethod(cls, selector2);// 交換方法method_exchangeImplementations(method1, method2);}} }- (void)enableTrackReactNativeEvent {sensorsdata_method_exchange("RCTUIManager", "setJSResponder:blockNativeResponder:", "sensorsdata_setJSResponder:blockNativeResponder:", (IMP)sensorsdata_setJSResponder); }static void sensorsdata_setJSResponder(id obj, SEL cmd, NSNumber *reactTag, BOOL blockNativeResponder) {}@end第二步:實現(xiàn)交換后的?
sensorsdata_setJSResponder 函數(shù)。
在該函數(shù)中,需要做三件事情:
1. 調(diào)用原始的方法,保證 React Native 可以繼續(xù)完成觸摸事件的響應
2. 獲取觸發(fā)事件響應的視圖控件
3. 觸發(fā) $AppClick 事件
完整的代碼實現(xiàn)如下:
@implementation SensorsAnalyticsSDK (ReactNative)......static void sensorsdata_setJSResponder(id obj, SEL cmd, NSNumber *reactTag, BOOL blockNativeResponder) {// 先執(zhí)行原來的方法SEL oriSel = sel_getUid("sensorsdata_setJSResponder:blockNativeResponder:");// 獲取原始方法的實現(xiàn)函數(shù)指針void (*imp)(id, SEL, id, BOOL) = (void (*)(id, SEL, id, BOOL))[obj methodForSelector:oriSel];// 完成第一步調(diào)用原始方法,讓 React Native 完成事件響應imp(obj, cmd, reactTag, blockNativeResponder);dispatch_async(dispatch_get_main_queue(), ^{// 獲取 viewForReactTag: 的方法名,目的是獲取觸發(fā)當前觸摸事件的控件SEL viewForReactTagSelector = NSSelectorFromString(@"viewForReactTag:");// 完成第二步,獲取響應觸摸事件的視圖UIView *view = ((UIView * (*)(id, SEL, NSNumber *))[obj methodForSelector:viewForReactTagSelector])(obj, viewForReactTagSelector, reactTag);// 觸發(fā) $AppClick 事件[[SensorsAnalyticsSDK sharedInstance] trackAppClickWithView:view properties:nil];}); }@end第三步:在 AppDelegate.m 的?
- application:(UIApplication *)application didFinishLaunchingWithOptions: 中,初始化 SDK 之后,調(diào)用 SensorsAnalyticsSDK 的 - enableTrackReactNativeEvent 方法開啟 React Native 的?$AppClick 事件全埋點。
@implementation AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {[SensorsAnalyticsSDK startWithServerURL:@"xxxx"];[[SensorsAnalyticsSDK sharedInstance] enableTrackReactNativeEvent];......return YES; }......@end第四步:測試驗證。
運行 AwesomeProject 項目,點擊 Press me 按鈕,在 Xcode 控制臺中可以看到 $AppClick 事件。
{"properties": {"$model": "x86_64","$manufacturer": "Apple","$element_type": "RCTView","$lib_version": "1.0.0","$os": "iOS","$app_version": "1.0","$screen_name": "UIViewController","$os_version": "12.3","$lib": "iOS"},"event": "$AppClick","time": 1576146363711,"distinct_id": "E934E526-6517-4CA1-A61E-0DCE2172D56A" }但是,我們發(fā)現(xiàn)沒有?$element_content 屬性,其實按鈕上是有文本的(Press me)。
也就是說之前獲取視圖控件顯示內(nèi)容的方法并沒有覆蓋到 React Native 的控件,因此需要修改之前實現(xiàn)的?UIView+SensorsData.m 中獲取控件顯示內(nèi)容的擴展方法?- sensorsdata_elementContent。
通過 RCTView 的源碼可知,獲取控件上的內(nèi)容可以通過 accessibilityLabel 屬性進行獲取,因此在 - sensorsdata_elementContent 方法中,當獲取到的內(nèi)容為空時,返回 accessibilityLabel 屬性即可。
@implementation UIView (SensorsData)- (NSString *)sensorsdata_elementContent {// 如果是隱藏控件,則不獲取控件內(nèi)容if (self.isHidden || self.alpha == 0) {return nil;}// 初始化數(shù)組,用于保存子控件的內(nèi)容NSMutableArray *contents = [NSMutableArray array];for (UIView *view in self.subviews) {// 獲取子控件的內(nèi)容// 如果子類有內(nèi)容,例如:UILabel 的 text,獲取到的就是 text 屬性;// 如果沒有就遞歸調(diào)用此方法,獲取其子控件的內(nèi)容。NSString *content = view.sensorsdata_elementContent;if (content.length > 0) {// 當該子控件中有內(nèi)容時,保存在數(shù)組中[contents addObject:content];}}// 當未獲取到子控件內(nèi)容時返回空。如果獲取到多個子控件內(nèi)容時,使用 - 拼接return contents.count == 0 ? self.accessibilityLabel : [contents componentsJoinedByString:@"-"]; }@end再次運行 AwesomeProject 項目,點擊 Press me 按鈕,就能看到 $AppClick 事件已有 $element_content 屬性了。
{"properties": {"$model": "x86_64","$manufacturer": "Apple","$element_type": "RCTView","$lib_version": "1.0.0","$os": "iOS","$element_content": "Press me","$app_version": "1.0","$screen_name": "UIViewController","$os_version": "12.3","$lib": "iOS"},"event": "$AppClick","time": 1576146741159,"distinct_id": "E934E526-6517-4CA1-A61E-0DCE2172D56A" }不過問題并沒有就此結(jié)束!當點擊 Switch 控件的時候,發(fā)現(xiàn)會觸發(fā)兩次?$AppClick 事件。之前有提到一些特殊的控件其實已經(jīng)支持了采集?$AppClick 事件,但是當點擊這些控件的時候,React Native 同樣也會走觸摸事件的響應流程,因此造成了觸發(fā)兩次?$AppClick 事件。對于這種情況,我們需要在采集 React Native 的?$AppClick 事件時,把這些特殊的控件給剔除。修改 sensorsdata_setJSResponder 函數(shù)的實現(xiàn),在觸發(fā)?$AppClick 事件之前判斷如果是 UIControl 類的控件直接返回。
@implementation SensorsAnalyticsSDK (ReactNative)......static void sensorsdata_setJSResponder(id obj, SEL cmd, NSNumber *reactTag, BOOL blockNativeResponder) {// 先執(zhí)行原來的方法SEL oriSel = sel_getUid("sensorsdata_setJSResponder:blockNativeResponder:");// 獲取原始方法的實現(xiàn)函數(shù)指針void (*imp)(id, SEL, id, BOOL) = (void (*)(id, SEL, id, BOOL))[obj methodForSelector:oriSel];// 完成第一步調(diào)用原始方法,讓 React Native 完成事件響應imp(obj, cmd, reactTag, blockNativeResponder);dispatch_async(dispatch_get_main_queue(), ^{// 獲取 viewForReactTag: 的方法名,目的是獲取觸發(fā)當前觸摸事件的控件SEL viewForReactTagSelector = NSSelectorFromString(@"viewForReactTag:");// 完成第二步,獲取響應觸摸事件的視圖UIView *view = ((UIView * (*)(id, SEL, NSNumber *))[obj methodForSelector:viewForReactTagSelector])(obj, viewForReactTagSelector, reactTag);// 如果是 UIControl 的子類,例如:RCTSwitch、RCTSlider 等,直接返回if ([view isKindOfClass:UIControl.class]) {return;}// 觸發(fā) $AppClick 事件[[SensorsAnalyticsSDK sharedInstance] trackAppClickWithView:view properties:nil];}); }@end如果此時你以為已經(jīng)考慮到了所有情況,那你就錯了。當我們滾動頁面的時候,同樣也會觸發(fā) $AppClick 事件 !
{"properties": {"$model": "x86_64","$manufacturer": "Apple","$element_type": "RCTScrollView","$lib_version": "1.0.0","$os": "iOS","$element_content": "Welcome to React-Components-checked-Press me","$app_version": "1.0","$screen_name": "UIViewController","$os_version": "12.3","$lib": "iOS"},"event": "$AppClick","time": 1576147089922,"distinct_id": "E934E526-6517-4CA1-A61E-0DCE2172D56A" }這是因為在滾動頁面時,React Native 的 JavaScript 端同樣會回調(diào)該響應方法,因此這種情況同樣需要排除在外。
我們通過代碼實現(xiàn)發(fā)現(xiàn),在滾動頁面時,在?sensorsdata_setJSResponder 函數(shù)中獲取到的視圖其實是 RCTScrollView 類型的。因此,實現(xiàn)也比較簡單。
@implementation SensorsAnalyticsSDK (ReactNative)......static void sensorsdata_setJSResponder(id obj, SEL cmd, NSNumber *reactTag, BOOL blockNativeResponder) {// 先執(zhí)行原來的方法SEL oriSel = sel_getUid("sensorsdata_setJSResponder:blockNativeResponder:");// 獲取原始方法的實現(xiàn)函數(shù)指針void (*imp)(id, SEL, id, BOOL) = (void (*)(id, SEL, id, BOOL))[obj methodForSelector:oriSel];// 完成第一步調(diào)用原始方法,讓 React Native 完成事件響應imp(obj, cmd, reactTag, blockNativeResponder);dispatch_async(dispatch_get_main_queue(), ^{// 獲取 viewForReactTag: 的方法名,目的是獲取觸發(fā)當前觸摸事件的控件SEL viewForReactTagSelector = NSSelectorFromString(@"viewForReactTag:");// 完成第二步,獲取響應觸摸事件的視圖UIView *view = ((UIView * (*)(id, SEL, NSNumber *))[obj methodForSelector:viewForReactTagSelector])(obj, viewForReactTagSelector, reactTag);// 如果是 UIControl 的子類,例如:RCTSwitch、RCTSlider 等,直接返回// 如果是 RCTScrollView,說明是在滑動的響應,并不是控件的點擊if ([view isKindOfClass:UIControl.class] || [view isKindOfClass:NSClassFromString(@"RCTScrollView")]) {return;}// 觸發(fā) $AppClick 事件[[SensorsAnalyticsSDK sharedInstance] trackAppClickWithView:view properties:nil];}); }@end到此,我們已實現(xiàn)了 React Native 的 $AppClick 事件的全埋點。
搶先送
若你對全埋點感興趣,請圍繞“數(shù)據(jù)采集”或“全埋點”相關話題,在留言區(qū)寫下你的疑惑、觀點等,留言點贊排名靠前及優(yōu)秀留言者即可獲得免費贈書,《Android 全埋點解決方案》或《iOS 全埋點解決方案》二選一哦~
???
【更多內(nèi)容】
神策學堂“訓練營+特訓營”,種子學員招募中,來一起出圈呀!
直播報名 | 券商如何精細化運營?
戳此,下載《iOS 全埋點技術白皮書》
總結(jié)
以上是生活随笔為你收集整理的大咖说:React Native 全埋点实现原理(内附赠书)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 神策学堂“训练营+特训营”,种子学员招募
- 下一篇: 案例 | 易快报:解放“客户成功”的背后