ينقسم تتبّع الأحداث على جانب العميل عمومًا إلى ثلاث فئات:

  • التتبّع بالشيفرة
  • التتبّع المرئي
  • التتبّع التلقائي

يقدّم هذا المقال أساسًا الأفكار وعملية التنفيذ وراء التتبّع التلقائي.

1. التتبّع بالشيفرة

التتبّع بالشيفرة يعني استدعاء واجهة في كل نقطة تريد تتبّعها، ورفع بيانات الحدث مباشرة. وتوفّر معظم خدمات التحليلات الخارجية واجهة api للتتبّع بالشيفرة، وهو أمر مريح.

لكن ذلك يخلق مشكلة: تتوزع شيفرة التتبّع في أنحاء منطق العمل وترتبط به بشدة. وعند تغيير الشاشات أو مسارات العمل، لا يتضح ما ينبغي فعله بالأحداث القديمة أو إن كنا نحتاج إلى أحداث جديدة. ومع زيادة عدد الأحداث، تصبح صيانة شيفرة التتبّع المتناثرة كابوسًا. يمكن للماكرو وأصناف المصانع تبسيط الشيفرة، لكنها لا تغيّر الأمر جذريًا.

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    // 埋点
    APE(kEvent4001);
}

2. التتبّع المرئي

يضبط التتبّع المرئي نقاط الجمع عبر أداة مرئية. يحلّل العميل الإعدادات تلقائيًا ويرسل الأحداث، محققًا ما يُسمّى «التتبّع بلا أثر». ومن الحلول مفتوحة المصدر الممثلة لهذا النهج Mixpanel.

3. التتبّع التلقائي

التتبّع التلقائي، ويُسمّى أيضًا تتبّع جميع الأحداث، لا يعني حقًا عدم وجود شيفرة للرصد. بل يجمع كل الأحداث ويرسلها، ويترك للخادم تصفية البيانات المفيدة.

ولمزيد من التحسين، يستطيع الخادم إرسال ملف إعدادات كي يصفّي العميل الأحداث بنفسه.

من أجل مؤشرات الأداء KPI، أعدت اختراع العجلة حول هذه الفكرة وبنيت WKTrackingData.

أسجّل هنا بعض المشكلات والأفكار التي ظهرت أثناء تنفيذ WKTrackingData.

فكرة التنفيذ بسيطة. الشيفرة منظّمة كما يلي:

بنية WKTrackingData الأصلية: المدير ومسارات الأحداث ووحدات AOP

هنا يتولى 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 أو اترك تعليقًا أدناه!