Back home

فضح NSDictionary

بحث حول التنفيذ الأساسي لـ NSDictionary

فضح NSDictionary

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

لا يقدم NSDictionary أي وعد بتنفيذه الداخلي. لن يكون من المنطقي أن يقوم القاموس بتخزين بياناته بطريقة عشوائية تمامًا. ومع ذلك، فإن هذا الافتراض لا يجيب على السؤال الرئيسي: هل يستخدم NSDictionary جدول التجزئة؟ وهذا ما قررت التحقيق فيه. لا يقدم NSDictionary أي وعود بشأن تنفيذه الداخلي. ليس من المنطقي أن يقوم القاموس بتخزين البيانات بطريقة عشوائية تمامًا. ومع ذلك، فإن هذا الافتراض لا يجيب على السؤال الرئيسي: هل يستخدم NSDictionary جدول التجزئة؟ وهذا ما قررت التحقيق فيه.

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

يحتوي منشور المدونة هذا على مستودع مصاحب يحتوي على أجزاء من التعليمات البرمجية تمت مناقشتها. على الرغم من أن التحقيق بأكمله يعتمد على iOS 7.1 SDK الذي يستهدف جهازًا يعمل بنظام 64 بت، إلا أن الأجهزة التي تعمل بنظام التشغيل iOS 7.0 أو 32 بت لا تؤثر على النتائج. يحتوي منشور المدونة هذا على مستودع مصاحب يحتوي على مقتطفات التعليمات البرمجية التي تمت مناقشتها. في حين أن التحقيق بأكمله كان يعتمد على iOS 7.1 SDK للأجهزة ذات 64 بت، لم يؤثر أي من الأجهزة التي تعمل بنظام iOS 7.0 أو 32 بت على النتائج.

فئة الفصل

الكثير من الفصول التأسيسية عبارة عن مجموعات فصول و NSDictionary ليس استثناءً. لفترة طويلة، استخدم NSDictionary CFDictionary كتطبيق افتراضي له، ولكن بدءًا من iOS 6.0 تغيرت الأمور: العديد من الفئات الأساسية عبارة عن مجموعات فئوية، وNSDictionary ليس استثناءً. لبعض الوقت، استخدم NSDictionary CFDictionary كتطبيق افتراضي له، ولكن بدءًا من iOS 6.0، تغيرت الأمور:

(lldb) po [[NSDictionary new] class]
__NSDictionaryI

على غرار __NSArrayM، يقع __NSDictionaryI ضمن إطار عمل CoreFoundation، على الرغم من تقديمه علنًا كجزء من Foundation. يؤدي تشغيل المكتبة من خلال تفريغ الفصل إلى إنشاء تخطيط ivar التالي: مثل __NSArrayM، يوجد __NSDictionaryI ضمن إطار عمل CoreFoundation، على الرغم من أنه يتم عرضه علنًا كجزء من Foundation. تشغيل المكتبة من خلال تفريغ الفصل الدراسي ينتج عنه تخطيط ivar التالي:

@interface __NSDictionaryI : NSDictionary
{
    NSUIngeter _used:58;
    NSUIngeter _szidx:6;
}

إنها قصيرة بشكل مدهش. لا يبدو أن هناك أي مؤشر لتخزين المفاتيح أو الكائنات. وكما سنرى قريبًا، يحتفظ __NSDictionary حرفيًا بمساحة تخزينه لنفسه. إنها قصيرة بشكل مدهش. لا يبدو أن هناك أي مؤشرات للمفاتيح أو تخزين الكائنات. وكما سنرى قريبًا، فإن __NSDictionary يحتفظ فعليًا بمساحة تخزينه.

التخزين التخزين

إنشاء مثيل إنشاء مثيل

لفهم أين يحتفظ __NSDictionaryI بمحتوياته، فلنقم بجولة سريعة خلال عملية إنشاء المثيل. هناك طريقة فئة واحدة فقط مسؤولة عن إنشاء مثيلات جديدة لـ __NSDictionaryI. وفقًا لـ class-dump، تحتوي الطريقة على التوقيع التالي: لفهم مكان حفظ محتويات __NSDictionaryI، دعنا نلقي نظرة سريعة على عملية إنشاء المثيل. توجد طريقة فئة واحدة فقط مسؤولة عن إنشاء مثيلات جديدة لـ __NSDictionaryI. وفقًا لـ class-dump، تتميز هذه الطريقة بالخصائص التالية:

+ (id)__new:(const id *)arg1:(const id *)arg2:(unsigned long long)arg3:(_Bool)arg4:(_Bool)arg5;

يستغرق الأمر خمس وسيطات، يتم تسمية الوسيطة الأولى منها فقط. على محمل الجد، إذا كنت ستستخدمه في بيان @selector فسيكون له شكل @selector(__new:::::). مجموعة من الكائنات وعدد المفاتيح (الكائنات) على التوالي. لاحظ أنه يتم تبديل صفائف المفاتيح والكائنات مقارنةً بواجهة برمجة التطبيقات (API) التي تواجه الجمهور والتي تأخذ شكلاً من أشكال: يستغرق الأمر خمس معلمات، يتم تسمية أولها فقط. على محمل الجد، إذا كنت تستخدمه في عبارة @selector، فسيكون له النموذج @selector(__new:::::). يمكن استنتاج المعلمات الثلاثة الأولى بسهولة عن طريق تعيين نقطة توقف على الطريقة والتطفل على محتويات السجلات x2 وx3 وx4 التي تحتوي على مصفوفة المفاتيح ومصفوفة الكائنات وعدد المفاتيح (الكائن) على التوالي. لاحظ أنه يتم تبديل المفاتيح ومصفوفات الكائنات مقارنةً بواجهة برمجة التطبيقات العامة التي تأخذ النموذج:

+ (instancetype)dictionaryWithObjects:(const id [])objects forKeys:(const id <NSCopying> [])keys count:(NSUInteger)cnt;

لا يهم ما إذا تم تعريف الوسيطة على أنها const id * أو const id [] نظرًا لأن المصفوفات تتحلل إلى مؤشرات عند تمريرها كوسائط دالة. لا يهم ما إذا تم تعريف المعلمة على أنها const id * أو const id[]، حيث أن المصفوفة سوف تتحلل إلى مؤشر عند تمريرها كمعلمة دالة.بعد تغطية ثلاث وسيطات، يتبقى لدينا المعلمتان المنطقيتان غير المحددتين. لقد أجريت بعض أعمال الحفر التجميعي وتوصلت إلى النتائج التالية: تحدد الوسيطة الرابعة ما إذا كان ينبغي نسخ المفاتيح، وتقرر الوسيطة الأخيرة ما إذا كان ينبغي الاحتفاظ بالوسيطات. يمكننا الآن إعادة كتابة الطريقة باستخدام المعلمات المسماة: مع وجود ثلاث معلمات، يتبقى لدينا معلمتان منطقيتان غير محددتين. لقد قمت ببعض البحث حول التجميع وهذا ما وجدته: تتحكم المعلمة الرابعة في ما إذا كان يجب نسخ المفتاح، وتحدد المعلمة الأخيرة ما إذا كان يجب عدم الاحتفاظ بالمعلمة. يمكننا الآن تجاوز هذه الطريقة باستخدام المعلمات المسماة:

+ (id)__new:(const id *)keys :(const id *)objects :(unsigned long long)count :(_Bool)copyKeys :(_Bool)dontRetain;

لسوء الحظ، ليس لدينا وصول صريح إلى هذه الطريقة الخاصة، لذلك باستخدام وسائل التخصيص العادية، يتم دائمًا تعيين الوسيطتين الأخيرتين على YES وNO على التوالي. ومع ذلك فمن المثير للاهتمام أن __NSDictionaryI قادر على التحكم في المفاتيح والكائنات بشكل أكثر تعقيدًا. لسوء الحظ، لا يمكننا الوصول إلى هذه الطريقة الخاصة بشكل صريح، لذلك باستخدام طريقة التخصيص العادية، يتم دائمًا تعيين المعلمتين الأخيرتين على نعم و لا على التوالي. ومع ذلك، من المثير للاهتمام ملاحظة أن __NSDictionaryI قادر على توفير عناصر تحكم أكثر تعقيدًا في المفاتيح والكائنات.

####متغيرات المثيلات مفهرسة بواسطة ivars المفهرسة

يكشف القشط خلال تفكيك + __new::::: أنه لا يمكن العثور على كل من malloc وcalloc في أي مكان. بدلاً من ذلك، يستدعي الأسلوب __CFAllocateObject2 ويمرر الفئة __NSDictionaryI كوسيطة أولى وحجم التخزين المطلوب كوسيطة ثانية. يوضح النزول إلى بحر ARM64 أن أول شيء يفعله __CFAllocateObject2 هو الاتصال بـ class_createInstance بنفس الوسائط بالضبط. تصفح تحليل + __new:::: يكشف عدم وجود malloc أو calloc. بدلاً من ذلك، يستدعي الأسلوب __CFAllocateObject2، ويمرر فئة __NSDictionaryI كمعلمة أولى وحجم التخزين المطلوب كمعلمة ثانية. يوضح الدخول إلى محيط ARM64 أن أول شيء يفعله __CFAllocateObject2 هو استدعاء class_createInstance بنفس المعلمات بالضبط.

لحسن الحظ، في هذه المرحلة لدينا إمكانية الوصول إلى الكود المصدري لوقت تشغيل Objective-C مما يجعل المزيد من التحقيق أسهل بكثير. لحسن الحظ، لدينا الآن إمكانية الوصول إلى الكود المصدري لوقت تشغيل Objective-C، مما يجعل إجراء المزيد من البحث أسهل.

تستدعي الدالة class_createInstance(Class cls, size_t extraBytes) فقط _class_createInstanceFromZone ​​وتمرير nil كمنطقة، ولكن هذه هي الخطوة الأخيرة لتخصيص الكائن. على الرغم من أن الوظيفة نفسها تحتوي على العديد من عمليات التحقق الإضافية لظروف مختلفة ومختلفة، إلا أنه يمكن تغطية جوهرها بثلاثة أسطر فقط: تقوم الدالة class_createInstance(Class cls, size_t extraBytes) باستدعاء _class_createInstanceFromZone فقط وتمرير nil كمنطقة، ولكن هذه هي الخطوة الأخيرة لتخصيص الكائن. على الرغم من أن الوظيفة نفسها تحتوي على العديد من عمليات التحقق الإضافية لمواقف مختلفة، إلا أنه يمكن وصف جوهرها في ثلاثة أسطر:

_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
    ...
    size_t size = cls->alignedInstanceSize() + extraBytes;
    ...
    id obj = (id)calloc(1, size);
    ...
    return obj;
}

لا يمكن أن تكون الوسيطة extraBytes أكثر وصفًا. إنه حرفيًا عدد البايتات الإضافية التي تضخم حجم المثيل الافتراضي. كمكافأة إضافية، لاحظ أن استدعاء calloc هو الذي يضمن تصفية كافة الـ ivars عند تخصيص الكائن. لا يمكن أن تكون وسيطة تيرابايت أفضل. إنها في الواقع البايتات الإضافية التي تزيد من حجم المثيل الافتراضي. لاحظ أيضًا أن استدعاء calloc يضمن تصفية جميع ivars عند تخصيص الكائن.

قسم ivars المفهرس ليس أكثر من مساحة إضافية تقع في نهاية ivars العادية: جزء الفهرس ivars ليس أكثر من مسافة إضافية في نهاية ivars العادية:

تخصيص الكائنات

لا يبدو تخصيص المساحة من تلقاء نفسه أمرًا مثيرًا للغاية، لذا ينشر وقت التشغيل ملحقًا: لا يبدو تخصيص المساحة بشكل فردي أمرًا مثيرًا، لذلك ينشر وقت التشغيل ملحقًا:

void *object_getIndexedIvars(id obj)

لا يوجد أي سحر على الإطلاق في هذه الوظيفة، فهي تقوم فقط بإرجاع مؤشر إلى بداية قسم ivars المفهرس: لا يوجد شيء سحري في هذه الوظيفة، فهي تقوم فقط بإرجاع مؤشر إلى بداية قسم الفهرس ivars:

قسم ivars المفهرسة

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

ثانيا، أنها توفر وصولا أسرع إلى التخزين. كل ذلك يعود إلى كونه صديقًا لذاكرة التخزين المؤقت. بشكل عام، قد يكون الانتقال إلى مواقع الذاكرة العشوائية (عن طريق إلغاء الإشارة إلى المؤشر) مكلفًا. نظرًا لأنه تم الوصول إلى الكائن للتو (قام شخص ما باستدعاء طريقة عليه)، فمن المحتمل جدًا أن تكون ivars المفهرسة قد وصلت إلى ذاكرة التخزين المؤقت. من خلال إبقاء كل ما هو مطلوب قريبًا جدًا، يمكن للكائن توفير أفضل أداء ممكن. ثانيًا، أنها توفر وصولاً أسرع للتخزين. كل ذلك يعود إلى سهولة التخزين المؤقت. بشكل عام، قد يكون الانتقال إلى موقع ذاكرة عشوائي (عن طريق إلغاء الإشارة إلى المؤشر) مكلفًا. نظرًا لأنه تم الوصول إلى الكائن للتو (قام شخص ما باستدعاء طريقة عليه)، فمن المرجح أن يكون فهرسه ivar موجودًا بالفعل في ذاكرة التخزين المؤقت. من خلال إبقاء كل ما تحتاجه قريبًا جدًا، يمكن للكائنات توفير أفضل أداء ممكن.

أخيرًا، يمكن استخدام ivars المفهرسة كإجراء دفاعي خام لجعل الأجزاء الداخلية للكائن غير مرئية للأدوات المساعدة مثل تفريغ الفئة. تعد هذه حماية أساسية جدًا نظرًا لأن المهاجم المخصص يمكنه ببساطة البحث عن مكالمات object_getIndexedIvars في عملية التفكيك أو التحقق بشكل عشوائي من المثيل بعد قسم ivars العادي الخاص به لمعرفة ما يحدث. أخيرًا، يمكن استخدام مؤشر ivars كإجراء دفاعي أولي بحيث لا تتمكن الأدوات المساعدة مثل تفريغ الفئة من رؤية البنية الداخلية للكائن. هذه حماية أساسية للغاية، حيث يمكن للمهاجم المخصص ببساطة البحث عن استدعاء object_getIndexedIvars في عملية التفكيك، أو فحص المثيل بشكل عشوائي عبر جزء ivars العادي الخاص به لمعرفة ما يحدث.في حين أن الـ ivars المفهرسة القوية تأتي مع تحذيرين. أولاً، لا يمكن استخدام class_createInstance ضمن ARC، لذلك سيتعين عليك تجميع بعض أجزاء فصلك باستخدام علامة -fno-objc-arc لجعله يلمع. ثانيًا، لا يحتفظ وقت التشغيل بمعلومات حجم ivar المفهرسة في أي مكان. على الرغم من أن dealloc سيقوم بتنظيف كل شيء (كما يطلق عليه free داخليًا)، يجب عليك الاحتفاظ بحجم التخزين في مكان ما، على افتراض أنك تستخدم عددًا متغيرًا من البايتات الإضافية. على الرغم من أن مؤشر ivar قوي، إلا أن هناك نقطتين يجب ملاحظتهما. أولاً، لا يعمل class_createInstance ضمن ARC، لذلك يتعين عليك تجميع بعض أجزاء الفصل باستخدام العلامة -fno-objc-arc لجعله يلمع. ثانيًا، لا يتم حفظ معلومات حجم الفهرس ivar في أي مكان أثناء وقت التشغيل. على الرغم من أن برنامج Dealloc ينظف كل شيء (لأنه يستدعي مجانًا داخليًا)، إلا أنه يجب عليك الحفاظ على حجم التخزين، على افتراض أنك تستخدم مقدارًا متغيرًا من البايتات الإضافية.

البحث عن المفتاح وجلب الكائن البحث عن المفاتيح واسترداد الكائنات

تحليل التجميع تجميع التحليل

على الرغم من أنه يمكننا في هذه المرحلة البحث عن مثيلات __NSDictionaryI لمعرفة كيفية عملها، إلا أن الحقيقة المطلقة تكمن داخل التجميع. بدلاً من المرور عبر جدار ARM64 بأكمله، سنناقش رمز Objective-C المكافئ بدلاً من ذلك. بينما يمكننا في هذه المرحلة البحث في مثيلات __NSDictionaryI لمعرفة كيفية عملها، فإن الحقيقة المطلقة تكمن في التجميع. سنناقش كود Objective-C المكافئ، بدلاً من ARM64 بأكمله.

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

كود C كود C

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

تمت كتابة الكود التالي من منظور __NSDictionaryI class: تمت كتابة التعليمة البرمجية التالية من منظور فئة __NSDictionaryI:

- (id)objectForKey:(id)aKey
{
    NSUInteger sizeIndex = _szidx;
    NSUInteger size = __NSDictionarySizes[sizeIndex];
    
    id *storage = (id *)object_getIndexedIvars(dict);
    
    NSUInteger fetchIndex = [aKey hash] % size;
    
    for (int i = 0; i < size; i++) {
        id fetchedKey = storage[2 * fetchIndex];

        if (fetchedKey == nil) {
            return nil;
        }
        
        if (fetchedKey == aKey || [fetchedKey isEqual:aKey]) {
            return storage[2 * fetchIndex + 1];
        }

        fetchIndex++;
        
        if (fetchIndex == size) {
            fetchIndex = 0;
        }
    }
    
    return nil;
}

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

يتم تخزين المفاتيح والكائنات بالتناوب

تحديث: قدم Joan Lluch شرحًا مقنعًا جدًا لهذا التخطيط. يمكن أن يستخدم الكود الأصلي مجموعة من البنيات البسيطة جدًا: تحديث: قدم Joan Lluch شرحًا مقنعًا جدًا لهذا التصميم. يمكن أن يستخدم الكود الأصلي مجموعة بسيطة جدًا من الهياكل:

struct KeyObjectPair {
    id key;
    id object;
};

طريقة objectForKey: واضحة جدًا وأشجعك بشدة على اتباعها في رأسك. ومع ذلك، تجدر الإشارة إلى بعض الأشياء. أولاً، يتم استخدام _szidx ivar كمؤشر في مصفوفة __NSDictionarySizes، وبالتالي فهو على الأرجح يرمز إلى “مؤشر الحجم”. طريقة objectForKey: بسيطة جدًا وأوصي بشدة بمراجعتها في رأسك. ومع ذلك، هناك بعض النقاط الجديرة بالإشارة. أولاً، يتم استخدام _szidx ivar كمؤشر في المصفوفة __nsdictionarysize، لذا فهو على الأرجح يمثل “فهرس الحجم”.

ثانيًا، الطريقة الوحيدة التي يتم استدعاؤها على المفتاح الذي تم تمريره هي hash. يتم استخدام التذكير بتقسيم قيمة تجزئة المفتاح على حجم القاموس لحساب الإزاحة في قسم الفهرس ivars. ثانيًا، الطريقة الوحيدة التي يتم استدعاؤها على المفتاح الذي تم تمريره هي التجزئة. يتم استخدام تجزئة مفتاح التذكير مقسومًا على حجم القاموس لحساب الإزاحة في جزء ivars من الفهرس.

إذا كان المفتاح الموجود في الإزاحة هو nil، فإننا ببساطة نعيد nil، وبذلك تكون المهمة قد انتهت: إذا كان المفتاح صفرًا عند الإزاحة، فإننا نعيد الصفر وتتم المهمة:

عندما تكون فتحة المفتاح فارغة، يتم إرجاع nil

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

تم العثور على المفتاح بعد اصطدام واحديُعرف هذا النوع من البحث باسم التحقيق الخطي. لاحظ كيف يلتف __NSDictionaryI حول fetchIndex عند الوصول إلى نهاية التخزين. حلقة for موجودة للحد من عدد عمليات التحقق - إذا كان التخزين ممتلئًا وكانت حالة الحلقة مفقودة فسنستمر في البحث إلى الأبد. ويسمى هذا البحث التحقيق الخطي. لاحظ كيف يقوم __NSDictionaryI بتغليف fetchIndex عندما يصل إلى جانب التخزين. يتم استخدام حلقة for لتحديد عدد عمليات التحقق - إذا كان المتجر ممتلئًا وكان شرط الحلقة مفقودًا، فسنواصل البحث.

####__NSDictionarySizes & __NSDictionaryCapacities

نحن نعلم بالفعل أن __NSDictionarySizes هو نوع من المصفوفات التي تخزن أحجامًا مختلفة محتملة من __NSDictionaryI. يمكننا أن نستنتج أنها مصفوفة من NSUInteger، وبالفعل، إذا طلبنا من Hopper التعامل مع القيم كأعداد صحيحة غير موقعة ذات 64 بت، يصبح الأمر منطقيًا فجأة: نحن نعلم بالفعل أن __nsdictionarysize هو نوع من المصفوفات التي تخزن __NSDictionaryI بأحجام مختلفة. يمكننا أن نستنتج أنها مصفوفة من NSUIntegers، وبالفعل إذا طلبنا من Hopper التعامل مع هذه القيم كأعداد صحيحة غير موقعة 64 بت، يصبح الأمر منطقيًا فجأة:

___NSDictionarySizes:
0x00000000001577a8         dq         0x0000000000000000
0x00000000001577b0         dq         0x0000000000000003
0x00000000001577b8         dq         0x0000000000000007
0x00000000001577c0         dq         0x000000000000000d
0x00000000001577c8         dq         0x0000000000000017
0x00000000001577d0         dq         0x0000000000000029
0x00000000001577d8         dq         0x0000000000000047
0x00000000001577e0         dq         0x000000000000007f
...

في شكل عشري مألوف أكثر، يتم تقديمه كقائمة جميلة مكونة من 64 عددًا أوليًا بدءًا بالتسلسل التالي: 0، 3، 7، 13، 23، 41، 71، 127. لاحظ أن هذه ليست أعدادًا أولية متتالية مما يطرح السؤال: ما هو متوسط النسبة بين الرقمين المتجاورين؟ إنه في الواقع حوالي 1.637 - وهو تطابق قريب جدًا من 1.625 والذي كان عامل النمو لـ NSMutableArray. للحصول على تفاصيل حول سبب استخدام الأعداد الأولية لحجم التخزين، تعد إجابة Stack Overflow بداية جيدة. في شكل عشري مألوف أكثر، يمثل قائمة لطيفة مكونة من 64 رقمًا أوليًا، بدءًا من التسلسل التالي: 0، 3، 7، 13، 23، 41، 71، 127. لاحظ أن هذه ليست أرقامًا أولية متتالية. وهذا يطرح السؤال ما هو متوسط ​​النسبة بين رقمين متجاورين؟ إنه في الواقع حوالي 1.637، وهو قريب جدًا من عامل نمو NSMutableArray البالغ 1.625. لمزيد من التفاصيل حول سبب استخدام الأعداد الأولية لأحجام التخزين، تعد إجابة Stack Overflow هذه بداية جيدة.

نحن نعلم بالفعل مقدار مساحة التخزين التي يمكن أن يتمتع بها __NSDictionaryI، ولكن كيف يعرف حجم الفهرس الذي يجب اختياره عند التهيئة؟ تكمن الإجابة في طريقة فئة + __new::::: المذكورة سابقًا. يؤدي تحويل بعض أجزاء التجميع مرة أخرى إلى لغة C إلى ظهور الكود التالي: نحن نعلم بالفعل مقدار مساحة التخزين التي يمكن أن يمتلكها __NSDictionaryI، ولكن كيف يعرف حجم الفهرس الذي يجب اختياره في وقت التهيئة؟ تكمن الإجابة في طريقة الفصل + __new:::: المذكورة سابقًا. يؤدي تحويل بعض أجزاء التجميع مرة أخرى إلى لغة C إلى ظهور الكود التالي:

int szidx;
for (szidx = 0; szidx < 64; szidx++) {
    if (__NSDictionaryCapacities[szidx] >= count) {
        break;
    }
}

if (szidx == 64) {
    goto fail;
}

تبدو الطريقة خطيًا من خلال مصفوفة __NSDictionaryCapacities حتى يتناسب العدد مع الحجم. نظرة سريعة على هوبر توضح محتويات المصفوفة: تبحث هذه الطريقة خطيًا في مصفوفة __nsdictionarycapacity حتى يتطابق العدد مع هذا الحجم. في لمحة سريعة في هوبر، محتويات المصفوفة هي كما يلي:

___NSDictionaryCapacities:
0x00000000001579b0         dq         0x0000000000000000
0x00000000001579b8         dq         0x0000000000000003
0x00000000001579c0         dq         0x0000000000000006
0x00000000001579c8         dq         0x000000000000000b
0x00000000001579d0         dq         0x0000000000000013
0x00000000001579d8         dq         0x0000000000000020
0x00000000001579e0         dq         0x0000000000000034
0x00000000001579e8         dq         0x0000000000000055
...

التحويل إلى الأساس 10 يوفر 0، 3، 6، 11، 19، 32، 52، 85 وما إلى ذلك. لاحظ أن هذه الأعداد أصغر من الأعداد الأولية المذكورة سابقًا. إذا قمت بتركيب 32 زوجًا من أزواج القيمة الرئيسية في __NSDictionaryI، فسوف يتم تخصيص مساحة لـ 41 زوجًا، مما يوفر بشكل متحفظ عددًا لا بأس به من الفواصل الفارغة. يساعد هذا في تقليل عدد تصادمات التجزئة، مما يجعل وقت الجلب قريبًا من الثبات قدر الإمكان. بصرف النظر عن الحالة التافهة المكونة من 3 عناصر، لن تمتلئ مساحة تخزين __NSDictionaryI أبدًا، حيث تملأ في المتوسط ​​62% من مساحتها على الأكثر. التحويل إلى الأساس 10 يوفر 0، 3، 6، 11، 19، 32، 52، 85، وهكذا. لاحظ أن هذه الأرقام أصغر من الأعداد الأولية المذكورة سابقًا. إذا قمت بوضع 32 زوجًا من قيمة المفتاح في __NSDictionaryI، فسوف يخصص مساحة لـ 41 زوجًا من قيمة المفتاح، مما يوفر بشكل متحفظ الكثير من الفتحات الفارغة. يساعد هذا في تقليل عدد تصادمات التجزئة ويحافظ على وقت الجلب ثابتًا قدر الإمكان. باستثناء الحالة البسيطة المكونة من 3 عناصر، فإن مساحة تخزين __NSDictionaryI لا تمتلئ أبدًا، حيث تملأ ما يصل إلى 62% من المساحة في المتوسط.

كمعلومات تافهة، آخر قيمة غير فارغة لـ __NSDictionaryCapacities هي 0x11089481C742 وهي 18728548943682 في الأساس 10. سيتعين عليك أن تحاول جاهدًا عدم التوافق مع الحد الأقصى لعدد الأزواج، على الأقل في بنية 64 بت. كمعلومات تافهة، آخر قيمة غير فارغة لـ __nsdictionarycapacity هي 0x11089481C742، والتي في الأساس 10 هي 18728548943682. على الأقل في بنيات 64 بت، عليك أن تحاول جاهدًا عدم الالتزام بالحد الأقصى للعدد.

الرموز غير المصدرة علامة عدم التصدير

إذا كنت ستستخدم __NSDictionarySizes في التعليمات البرمجية الخاصة بك عن طريق الإعلان عنها كمصفوفة extern، فسوف تدرك بسرعة أن الأمر ليس بهذه السهولة. لن يتم تجميع الكود بسبب خطأ في الرابط - الرمز __NSDictionarySizes غير محدد. فحص مكتبة CoreFoundation باستخدام الأداة المساعدة nm: إذا كنت تستخدم __nsdictionarysize في التعليمات البرمجية الخاصة بك عن طريق الإعلان عن __nsdictionarysize كمصفوفة خارجية، فسوف تدرك بسرعة أن هذا ليس بالأمر السهل. فشل تجميع الكود بسبب خطأ في الرابط - رمز __nsdictionarysize غير محدد. استخدم أداة nm للتحقق من مكتبة CoreFoundation:

nm CoreFoundation | grep ___NSDictionarySizes

…يُظهر بوضوح وجود الرموز (لـ ARMv7 وARMv7s وARM64 على التوالي): …يظهر بوضوح وجود الرموز (ARMv7 وARMv7s وARM64 على التوالي):

00139c80 s ___NSDictionarySizes
0013ac80 s ___NSDictionarySizes
0000000000156f38 s ___NSDictionarySizes

لسوء الحظ، ينص دليل نانومتر بوضوح على ما يلي: لسوء الحظ، ينص دليل نانومتر بوضوح على ما يلي:

إذا كان الرمز محليًا (غير خارجي)، فسيتم تمثيل نوع الرمز بالحرف الصغير المقابل.ببساطة، لا يتم تصدير رموز __NSDictionarySizes - فهي مخصصة للاستخدام الداخلي للمكتبة. لقد أجريت بعض الأبحاث لمعرفة ما إذا كان من الممكن الارتباط برموز غير مُصدَّرة، ولكن يبدو أن الأمر ليس كذلك (من فضلك أخبرني إذا كان الأمر كذلك!). لا يمكننا الوصول إليهم. وهذا يعني أننا لا نستطيع الوصول إليهم بسهولة. لا يتم تصدير رموز __nsdictionarysize - فهي للاستخدام الداخلي للمكتبة. لقد أجريت بعض الأبحاث وتساءلت عما إذا كان من الممكن الارتباط بالرموز غير المصدرة، ولكن يبدو أن ذلك غير ممكن (من فضلك أخبرني إذا كان ذلك ممكنًا!) ولا يمكننا الوصول إليها. أي أننا لا نستطيع الوصول إليهم بسهولة.

####التسلل سرا

إليك ملاحظة مثيرة للاهتمام: في كل من iOS 7.0 و7.1، تم وضع الثابت kCFAbsoluteTimeIntervalSince1904 مباشرةً قبل __NSDictionarySizes: ملاحظة مثيرة للاهتمام هنا: في iOS 7.0 و7.1، يتم إدراج الثابت kCFAbsoluteTimeIntervalSince1904 مباشرةً قبل __nsdictionarysize:

_kCFAbsoluteTimeIntervalSince1904:
0x00000000001577a0         dq         0x41e6ceaf20000000
___NSDictionarySizes:
0x00000000001577a8         dq         0x0000000000000000

أفضل شيء في kCFAbsoluteTimeIntervalSince1904 هو أنه يتم تصديره! سنضيف 8 بايت (حجم double) إلى عنوان هذا الثابت ونعيد تفسير النتيجة كمؤشر إلى NSUInteger: أفضل شيء في kcfabsolutetimeintervalvalis منذ عام 1904 هو أنه يتم تصديره! سنضيف 8 بايت (ضعف الحجم) إلى عنوان هذا الثابت ونعيد تفسير النتيجة كمؤشر إلى NSUInteger:

NSUInteger *Explored__NSDictionarySizes = (NSUInteger *)((char *)&kCFAbsoluteTimeIntervalSince1904 + 8);

ثم يمكننا الوصول إلى قيمها عن طريق الفهرسة الملائمة: يمكننا بعد ذلك الوصول إلى قيمته عبر فهرس مناسب:

(lldb) p Explored__NSDictionarySizes[0]
(NSUInteger) $0 = 0
(lldb) p Explored__NSDictionarySizes[1]
(NSUInteger) $1 = 3
(lldb) p Explored__NSDictionarySizes[2]
(NSUInteger) $2 = 7

هذا الاختراق هش للغاية ومن المرجح أن ينكسر في المستقبل، ولكن هذا هو المشروع الاختباري لذا فهو جيد تمامًا. هذا الاختراق هش للغاية ومن المرجح أن ينكسر في المستقبل، ولكن هذا مشروع اختباري لذا فهو مثالي.

__خصائص NSDictionaryI __خصائص NSDictionaryI

الآن بعد أن اكتشفنا البنية الداخلية لـ __NSDictionaryI، يمكننا استخدام هذه المعلومات لمعرفة سبب عمل الأشياء بالطريقة التي تعمل بها وما هي العواقب غير المتوقعة التي يقدمها التنفيذ الحالي لـ __NSDictionaryI. الآن بعد أن اكتشفنا البنية الداخلية لـ __NSDictionaryI، يمكننا استخدام هذه المعلومات لمعرفة سبب عمل الأشياء بالطريقة التي تعمل بها، وما هي العواقب غير المتوقعة لتطبيق __NSDictionaryI الحالي.

رمز الطباعة رمز الطباعة

لجعل تحقيقنا أسهل قليلاً، سنقوم بإنشاء طريقة فئة NSDictionary المساعدة التي ستقوم بطباعة محتويات المثيل لتسهيل بحثنا، سنقوم بإنشاء طريقة فئة مساعدة NSDictionary والتي ستقوم بطباعة محتويات المثيل

- (NSString *)explored_description
{
    assert([NSStringFromClass([self class]) isEqualToString:@"__NSDictionaryI"]);
    
    BCExploredDictionary *dict = (BCExploredDictionary *)self;

    NSUInteger count = dict->_used;
    NSUInteger sizeIndex = dict->_szidx;
    NSUInteger size = Explored__NSDictionarySizes[sizeIndex];
    
    __unsafe_unretained id *storage = (__unsafe_unretained id *)object_getIndexedIvars(dict);
    
    NSMutableString *description = [NSMutableString stringWithString:@"\n"];
    
    [description appendFormat:@"Count: %lu\n", (unsigned long)count];
    [description appendFormat:@"Size index: %lu\n", (unsigned long)sizeIndex];
    [description appendFormat:@"Size: %lu\n", (unsigned long)size];

    for (int i = 0; i < size; i++) {
        [description appendFormat:@"[%d] %@ - %@\n", i, [storage[2*i] description], [storage[2*i + 1] description]];
    }
    
    return description;
}

ترتيب المفاتيح/الكائنات في التعداد هو نفس ترتيب المفاتيح/الكائنات في التخزين

ترتيب المفاتيح/الكائنات في التعداد هو نفس ترتيب المفاتيح/الكائنات الموجودة في وحدة التخزين

لنقم بإنشاء قاموس بسيط يحتوي على أربع قيم: لنقم بإنشاء قاموس بسيط بأربع قيم:

NSDictionary *dict = @{@1 : @"Value 1",
                       @2 : @"Value 2",
                       @3 : @"Value 3",
                       @4 : @"Value 4"};

NSLog(@"%@", [dict explored_description]);

ناتج الوصف المستكشف هو: ناتج الوصف المستكشف هو:

Count: 4
Size index: 2
Size: 7
[0] (null) - (null)
[1] 3 - Value 3
[2] (null) - (null)
[3] 2 - Value 2
[4] (null) - (null)
[5] 1 - Value 1
[6] 4 - Value 4

مع أخذ ذلك في الاعتبار، دعونا نقوم بتعداد سريع عبر القاموس: مع أخذ ذلك في الاعتبار، دعونا ننشئ قاموسًا سريعًا للتعداد:

[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
    NSLog(@"%@ - %@", key, obj);
}];

و الإخراج: والإخراج:

3 - Value 3
2 - Value 2
1 - Value 1
4 - Value 4

يبدو أن التعداد يسير ببساطة عبر وحدة التخزين، ويتجاهل مفاتيح nil ويستدعي الكتلة فقط للفتحات غير الفارغة. وهذا هو الحال أيضًا بالنسبة للتعداد السريع وطرق keyEnumerator وallKeys وallValues. فمن المنطقي تماما. لم يتم ترتيب NSDictionary، لذلك لا يهم حقًا التسلسل الذي يتم توفير المفاتيح والقيم فيه. يعد استخدام التخطيط الداخلي هو الخيار الأسهل وربما الأسرع. يبدو أن التعداد يتكرر فقط على مساحة التخزين، ويتجاهل المفاتيح الصفرية، ويستدعي فقط الكتل للفتحات غير الفارغة. وهذا هو الحال أيضًا بالنسبة للتعداد السريع والعدادين الرئيسيين وأساليب allKeys وallValues. وهذا منطقي تمامًا. لم يتم ترتيب NSDictionary، لذا لا يهم ترتيب المفاتيح والقيم. يعد استخدام التخطيط الداخلي هو الخيار الأسهل والأسرع.

إذا أخطأت، __NSDictionaryقد أقوم بإرجاع شيء مقابل مفتاح لا شيء! إذا أخطأت، __nsdictionaryقد أرجع شيئًا مقابل مفتاح لا شيء!

دعونا نفكر في مثال. تخيل أننا نبني لعبة إستراتيجية ثلاثية الأبعاد بسيطة تدور أحداثها في الفضاء. ينقسم الكون بأكمله إلى قطاعات تشبه المكعب يمكن للفصائل الخيالية أن تتقاتل عليها. يمكن الإشارة إلى القطاع من خلال فهارسه i وj وk. من شأنه أن يضيع الذاكرة في تخزين مؤشرات nil. بدلاً من ذلك، سنستخدم مساحة تخزين متفرقة في شكل NSDictionary مع فئة مفاتيح مخصصة تجعل من السهل جدًا الاستعلام عما إذا كان هناك شيء ما في موقع معين. دعونا نفكر في مثال. لنفترض أننا نبني لعبة إستراتيجية ثلاثية الأبعاد بسيطة. ينقسم الكون بأكمله إلى مناطق تشبه المكعب يمكن للفصائل الخيالية أن تتقاتل عليها. يمكن الإشارة إلى القطاع من خلال مؤشراته i وj وk. لا ينبغي لنا استخدام مصفوفة ثلاثية الأبعاد لتخزين معلومات القطاع - مساحة اللعبة ضخمة وفارغة في الغالب، لذلك سنضيع الذاكرة في تخزين مؤشرات صفرية. بدلاً من ذلك، سنستخدم مساحة تخزين متفرقة في شكل NSDictionary، مع فئة مفاتيح مخصصة ستجعل من السهل جدًا الاستعلام عما إذا كان هناك شيء ما في موقع معين.

إليك واجهة المفتاح، فئة BC3DIndex: هذه هي واجهة المفتاح، فئة BC3DIndex:

@interface BC3DIndex : NSObject <NSCopying>

@property (nonatomic, readonly) NSUInteger i, j, k; // you actually can do that

- (instancetype)initWithI:(NSUInteger)i j:(NSUInteger)j k:(NSUInteger)k;

@end

وتنفيذها تافهة على حد سواء: وتنفيذها تافهة على حد سواء:

@implementation BC3DIndex

- (instancetype)initWithI:(NSUInteger)i j:(NSUInteger)j k:(NSUInteger)k
{
    self = [super init];
    if (self) {
        _i = i;
        _j = j;
        _k = k;
    }
    return self;
}

- (BOOL)isEqual:(BC3DIndex *)other
{
    return other.i == _i && other.j == _j && other.k == _k;
}

- (NSUInteger)hash
{
    return _i ^ _j ^ _k;
}

- (id)copyWithZone:(NSZone *)zone
{
    return self; // we're immutable so it's OK
}

@end
```لاحظ كيف أصبحنا مواطنين صالحين للفئات الفرعية: قمنا بتطبيق كل من طريقتي `isEqual`: و`hash` وتأكدنا من أنه إذا كان هناك فهرسان ثلاثي الأبعاد متساويان، فإن قيم التجزئة الخاصة بهما متساوية أيضًا. تم استيفاء متطلبات المساواة في الكائنات.
لاحظ كيف أننا مواطنون صالحون للفئات الفرعية: نحن نطبق طرق isEqual: وhash ونتأكد من أنه إذا كان مؤشران ثلاثي الأبعاد متساويين، فإن تجزئاتهما متساوية أيضًا. تم استيفاء متطلبات المساواة في الكائنات.

إليك معلومات بسيطة: ما الذي سيطبعه الكود التالي؟
إليك سؤال صغير: ما الذي سيطبعه الكود التالي؟

NSDictionary *indexes = @{[[BC3DIndex alloc] initWithI:2 j:8 k:5] : @“A black hole!”, [[BC3DIndex alloc] initWithI:0 j:0 k:0] : @“Asteroids!”, [[BC3DIndex alloc] initWithI:4 j:3 k:4] : @“A planet!”};

NSLog(@“%@”, [indexes objectForKey:nil]);


ينبغي أن يكون `(null)` أليس كذلك؟ كلا:
ينبغي أن يكون (خالية)، أليس كذلك؟ لا:

Asteroids!


لمزيد من التحقيق في هذا دعونا نحصل على وصف القاموس:
لمزيد من التحقيق في هذا، دعونا نحصل على وصف القاموس:

Count: 3 Size index: 1 Size: 3 [0] <BC3DIndex: 0x17803d340> - A black hole! [1] <BC3DIndex: 0x17803d360> - Asteroids! [2] <BC3DIndex: 0x17803d380> - A planet!


اتضح أن `__NSDictionaryI` لا يتحقق مما إذا كان `key` الذي تم تمريره إلى `objectForKey:` هو `nil` (وأنا أزعم أن هذا قرار تصميم جيد). يؤدي استدعاء أسلوب `hash` على `nil` إلى إرجاع `0`، مما يجعل الفئة تقارن المفتاح في الفهرس `0` مع `nil`. هذا مهم: المفتاح المخزن هو الذي ينفذ طريقة `isEqual:`، وليس المفتاح الذي تم تمريره.
والنتيجة هي أن __nsdictionari لا يتحقق مما إذا كان المفتاح الذي تم تمريره إلى objectForKey: لا شيء (وهو ما أعتقد أنه قرار تصميمي جيد). سيؤدي استدعاء طريقة التجزئة على nil إلى إرجاع 0، مما سيؤدي إلى قيام الفصل بمقارنة المفتاح الموجود في الفهرس 0 بالصفر. هذا مهم: المفتاح المخزن هو الذي ينفذ طريقة isEqual:، وليس المفتاح الذي تم تمريره.

فشلت المقارنة الأولى منذ ظهور مؤشر `i` لـ “الثقب الأسود!” هو `2` بينما بالنسبة إلى `nil` فهو صفر. المفاتيح غير متساوية مما يجعل القاموس يواصل البحث، ويضغط على مفتاح مخزن آخر: مفتاح "الكويكبات!". يحتوي هذا المفتاح على جميع خصائص `i` و`j` و`k` الثلاثة المساوية لـ `0` وهو أيضًا ما سيرجعه `nil` عند سؤاله عن خصائصه (عن طريق التحقق من `nil` داخل `objc_msgSend`).
فشلت المقارنة الأولى لأنني قمت بفهرسة "ثقب أسود!" وهو 2 والصفر يساوي 0. المفتاحان غير متساويين، مما يجعل القاموس يواصل البحث ويضغط على مفتاح مخزن آخر: "الكويكب!" يحتوي هذا المفتاح على سمات i وj وk الثلاث التي تساوي 0، وهي أيضًا القيمة nil التي يتم إرجاعها عند طلب سماته (عبر التحقق من nil في objc_msgSend).

هذا هو جوهر المشكلة. قد يؤدي تطبيق `isEqual:` لـ `BC3DIndex`، في بعض الحالات، إلى إرجاع `YES` لمقارنة `nil`. كما ترون، هذا سلوك خطير للغاية ويمكن أن يفسد الأمور بسهولة. تأكد دائمًا من أن الكائن الخاص بك لا يساوي `nil`.
هذا هو جوهر المسألة. isEqual: في ظل ظروف معينة، قد تقوم تطبيقات BC3DIndex بإرجاع YES لمقارنات لا شيء. كما ترون، هذا سلوك خطير للغاية ويمكن أن يفسد الأمور بسهولة. تأكد دائمًا من أن الكائن لا يساوي الصفر.

#### فئة المفاتيح المساعدة فئة المفاتيح المساعدة

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

ها هي الواجهة:
هذه هي الواجهة:

@interface BCNastyKey : NSObject <NSCopying>

@property (nonatomic, readonly) NSUInteger hashValue;

  • (instancetype)keyWithHashValue:(NSUInteger)hashValue;

@end


والتنفيذ:

@implementation BCNastyKey

  • (instancetype)keyWithHashValue:(NSUInteger)hashValue { return [[BCNastyKey alloc] initWithHashValue:hashValue]; }
  • (instancetype)initWithHashValue:(NSUInteger)hashValue { self = [super init]; if (self) { _hashValue = hashValue; } return self; }

  • (id)copyWithZone:(NSZone *)zone { return self; }

  • (NSUInteger)hash { NSLog(@“Key %@ is asked for its hash”, [self description]);

    return _hashValue; }

  • (BOOL)isEqual:(BCNastyKey *)object { NSLog(@“Key %@ equality test with %@: %@”, [self description], [object description], object == self ? @“YES” : @“NO”);

    return object == self; }

  • (NSString *)description { return [NSString stringWithFormat:@“(&:%p #:%lu)”, self, (unsigned long)_hashValue]; }

@end


هذا المفتاح فظيع: نحن متساوون فقط مع أنفسنا، ولكننا نعيد تجزئة عشوائية. لاحظ أن هذا لا يخرق عقد المساواة.
هذا المفتاح سيء: نحن نساوي الذات فقط، ولكننا نعيد تجزئة عشوائية. لاحظ أن هذا لا ينتهك عقد المساواة.

#### ليس من الضروري استدعاء isEqual لمطابقة المفتاح

لنقم بإنشاء مفتاح وقاموس:
لنقم بإنشاء مفتاح وقاموس:

BCNastyKey *key = [BCNastyKey keyWithHashValue:3]; NSDictionary *dict = @{key : @“Hello there!”};


النداء التالي :
أرقام الهواتف التالية:

[dict objectForKey:key];


يطبع هذا على وحدة التحكم:
الطباعة إلى وحدة التحكم:

Key (&:0x17800e240 #:3) is asked for its hash


كما ترون، لم يتم استدعاء الأسلوب `isEqual:`. هذا رائع جداً! نظرًا لأن الغالبية العظمى من المفاتيح الموجودة هي `NSString`، فإنها تشترك في نفس العنوان في التطبيق بأكمله. حتى لو كان المفتاح عبارة عن سلسلة حرفية طويلة جدًا، فلن يقوم `__NSDictionaryI` بتشغيل طريقة `isEqual:` التي قد تستغرق وقتًا طويلاً إلا إذا لزم الأمر. وبما أن بنيات 64 بت قدمت مؤشرات ذات علامات، فإن بعض مثيلات `NSNumber`، و`NSDate`، وعلى ما يبدو، `NSIndexPath` تستفيد من هذا التحسين أيضًا.
كما ترون، لم يتم استدعاء الأسلوب isEqual: بعد. هذا رائع! نظرًا لأن الغالبية العظمى من المفاتيح عبارة عن أحرف NSString، فإنها تشترك في نفس العنوان في جميع أنحاء التطبيق. حتى لو كان المفتاح عبارة عن سلسلة حرفية طويلة، فلن يقوم __NSDictionaryI بتشغيل الأسلوب isEqual: الذي قد يستغرق وقتًا طويلاً ما لم يكن ذلك ضروريًا للغاية. نظرًا لأن بنيات 64 بت قدمت مؤشرات ذات علامات، فمن الواضح أن بعض مثيلات NSNumber وNSDate وNSIndexPath استفادت أيضًا من هذا التحسين.

#### أسوأ أداء هو خطي أسوأ أداء هو خطي

لنقم بإنشاء حالة اختبار بسيطة جدًا:
لنقم بإنشاء حالة اختبار بسيطة جدًا:

BCNastyKey *targetKey = [BCNastyKey keyWithHashValue:36];

NSDictionary *b = @{[BCNastyKey keyWithHashValue:1] : @1, [BCNastyKey keyWithHashValue:8] : @2, [BCNastyKey keyWithHashValue:15] : @3, [BCNastyKey keyWithHashValue:22] : @4, [BCNastyKey keyWithHashValue:29] : @5, targetKey : @6 };


خط قاتل واحد:
ورقة رابحة بسيطة:

NSLog(@“Result: %@”, [[b objectForKey:targetKey] description]);


يكشف الكارثة:
كشفت الكارثة:

Key (&:0x170017640 #:36) is asked for its hash Key (&:0x170017670 #:1) equality test with (&:0x170017640 #:36): NO Key (&:0x170017660 #:8) equality test with (&:0x170017640 #:36): NO Key (&:0x170017680 #:15) equality test with (&:0x170017640 #:36): NO Key (&:0x1700176e0 #:22) equality test with (&:0x170017640 #:36): NO Key (&:0x170017760 #:29) equality test with (&:0x170017640 #:36): NO Result: 6


هذه حالة مرضية للغاية - كل مفتاح في القاموس تم اختبار المساواة فيه. على الرغم من أن كل تجزئة كانت مختلفة، إلا أنها لا تزال تتصادم مع كل مفتاح آخر، لأن تجزئات المفاتيح كانت متطابقة مع الوحدة 7، والتي تبين أنها تمثل حجم تخزين القاموس.
هذه حالة مرضية للغاية - يتم اختبار كل مفتاح في القاموس للتأكد من مساواة بن. على الرغم من أن كل تجزئة مختلفة، إلا أنها لا تزال تتصادم مع المفاتيح الأخرى لأن تجزئة المفتاح هي modulo 7، وهو حجم تخزين القاموس.كما ذكرنا سابقًا، لاحظ أن اختبار `isEqual:` الأخير مفقود. قام `__NSDictionaryI` ببساطة بمقارنة المؤشرات واكتشف أنه يجب أن يكون نفس المفتاح.
كما ذكرنا من قبل، لاحظ أن الأخير هو: الاختبار مفقود. يقوم __nsdictionari ببساطة بمقارنة المؤشرات ويكتشف أنها يجب أن تكون نفس المفتاح.

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

### الكلمات النهائية الكلمات النهائية

أنا منبهر بمدى بساطة `__NSDictionaryI`. وغني عن القول أن الفصل يخدم غرضه بالتأكيد وليس هناك حاجة لجعل الأمور معقدة بشكل مفرط. بالنسبة لي، أجمل جانب في التنفيذ هو تخطيط الكائن الرئيسي. هذه فكرة رائعة.
لقد أذهلتني مدى بساطة هذا القاموس. وغني عن القول أن هذه الفئة تخدم غرضها بالتأكيد وليس هناك حاجة إلى المبالغة في تعقيد الأمور. بالنسبة لي، أجمل جانب في هذا التنفيذ هو تخطيط الكائن الرئيسي. هذه فكرة جيدة.

إذا كنت ستأخذ نصيحة واحدة من هذه المقالة، فسأحرص على الحذر من أساليب `hash` و`isEqual:`. من المؤكد أنه نادرًا ما يكتب المرء فئات مفاتيح مخصصة لاستخدامها في القاموس، ولكن تنطبق هذه القواعد على `NSSet` أيضًا.
إذا أخذت نصيحة واحدة من هذه المقالة، فإنني أوصي بالانتباه إلى طرق hash وisEqual:. من المؤكد أن عددًا قليلًا من الأشخاص يكتبون فئات مفاتيح مخصصة لاستخدامها في القواميس، لكن هذه القواعد تنطبق على مجموعات NSSS أيضًا.

أدرك أنه في وقت ما سوف يتغير `NSDictionary` وستصبح النتائج التي توصلت إليها قديمة. إن استيعاب تفاصيل التنفيذ الحالية قد يصبح عبئًا في المستقبل، عندما لا تعود الافتراضات المحفوظة قابلة للتطبيق. ومع ذلك، هنا والآن، من الممتع جدًا رؤية كيفية سير الأمور وآمل أن تشاركني حماستي.
أعلم أنه في مرحلة ما سيتغير NSDictionary وستصبح النتائج التي توصلت إليها قديمة. يمكن أن يصبح استيعاب تفاصيل التنفيذ الحالية عبئًا مستقبليًا عندما لا تعود الافتراضات التي تم تذكرها قابلة للتطبيق. ومع ذلك، هنا والآن، من الممتع حقًا رؤية كيفية سير الأمور، وآمل أن تتمكن من مشاركة حماسي.



النص الأصلي: https://ciechanow.ski/exposing-nsdictionary/
FAQ

What to read next

Related

Continue reading