Mengekspos NSDictionary
Penelitian tentang implementasi yang mendasari NSDictionary
Ekspos NSDictionary
Tabel hash sungguh luar biasa. Sampai hari ini saya merasa menarik bahwa seseorang dapat mengambil objek yang sesuai dengan kunci sembarang dalam waktu yang konstan. Meskipun iOS 6.0 memperkenalkan tabel hash eksplisit, NSDictionary-lah yang hampir secara eksklusif digunakan untuk penyimpanan asosiatif. Tabel hash sangat bagus. Sampai hari ini, saya masih merasa sangat menarik untuk mendapatkan objek yang sesuai dengan kunci apa pun dalam jangka waktu tertentu. Meskipun iOS 6.0 memperkenalkan tabel hash eksplisit, NSDictionary digunakan hampir secara eksklusif untuk penyimpanan asosiatif.
NSDictionary tidak menjanjikan implementasi internalnya. Tidak masuk akal jika kamus menyimpan datanya secara acak. Namun, asumsi ini tidak menjawab pertanyaan kunci: apakah NSDictionary menggunakan tabel hash? Inilah yang saya putuskan untuk selidiki. NSDictionary tidak menjanjikan implementasi internalnya. Tidak masuk akal jika kamus menyimpan data dengan cara yang sepenuhnya acak. Namun, asumsi ini tidak menjawab pertanyaan kunci: apakah NSDictionary menggunakan tabel hash? Itulah yang saya putuskan untuk diselidiki.
Mengapa tidak menggunakan NSMutableDictionary yang berfitur lengkap? Dapat dimengerti bahwa kamus yang bisa berubah jauh lebih kompleks dan jumlah pembongkaran yang harus saya lalui sangat mengerikan. NSDictionary reguler masih memberikan tantangan penguraian ARM64 yang tidak sepele. Meskipun tidak dapat diubah, kelas ini memiliki beberapa detail implementasi yang sangat menarik yang akan membuat perjalanan berikutnya menyenangkan. Mengapa tidak berurusan dengan NSMutableDictionary berfitur lengkap saja? Maklum saja, kamus yang bisa berubah jauh lebih kompleks, dan jumlah pemfaktoran yang perlu saya lakukan akan sangat besar. NSDictionary biasa masih menghadirkan tantangan decoding ARM64 yang signifikan. Meskipun kelas ini tidak dapat diubah, kelas ini memiliki beberapa detail implementasi yang sangat menarik yang seharusnya membuat proses di bawah ini menyenangkan.
Entri blog ini memiliki repo pendamping yang berisi potongan kode yang dibahas. Meskipun seluruh penyelidikan didasarkan pada SDK iOS 7.1 yang menargetkan perangkat 64-bit, baik perangkat iOS 7.0 maupun 32-bit tidak memengaruhi temuan tersebut. Posting blog ini disertai repo yang berisi cuplikan kode yang dibahas. Meskipun seluruh penyelidikan didasarkan pada SDK iOS 7.1 untuk perangkat 64-bit, baik perangkat iOS 7.0 maupun 32-bit tidak memengaruhi temuan tersebut.
Kelas kelas
Banyak kelas Foundation yang merupakan cluster kelas dan NSDictionary tidak terkecuali. Untuk waktu yang cukup lama NSDictionary menggunakan CFDictionary sebagai implementasi defaultnya, namun mulai dengan iOS 6.0 banyak hal telah berubah: Banyak kelas dasar merupakan cluster kelas, dan NSDictionary tidak terkecuali. Untuk beberapa waktu, NSDictionary menggunakan CFDictionary sebagai implementasi defaultnya, namun, mulai dari iOS 6.0, banyak hal telah berubah:
(lldb) po [[NSDictionary new] class]
__NSDictionaryI
Mirip dengan __NSArrayM, __NSDictionaryI berada dalam kerangka CoreFoundation, meskipun disajikan secara publik sebagai bagian dari Foundation. Menjalankan perpustakaan melalui class-dump menghasilkan tata letak ivar berikut: Seperti __NSArrayM, __NSDictionaryI berada dalam kerangka CoreFoundation, meskipun diekspos secara publik sebagai bagian dari Foundation. Menjalankan perpustakaan melalui dump kelas menghasilkan tata letak ivar berikut:
@interface __NSDictionaryI : NSDictionary
{
NSUIngeter _used:58;
NSUIngeter _szidx:6;
}
Ini sangat singkat. Sepertinya tidak ada penunjuk ke kunci atau objek penyimpanan. Seperti yang akan segera kita lihat, __NSDictionary benar-benar menyimpan penyimpanannya sendiri. Ini sangat singkat. Sepertinya tidak ada petunjuk apa pun ke kunci atau penyimpanan objek. Seperti yang akan segera kita lihat, __NSDictionary benar-benar menyimpan penyimpanannya.
Penyimpanan Penyimpanan
Pembuatan Instance Pembuatan instance
Untuk memahami di mana __NSDictionaryI menyimpan kontennya, mari ikuti tur singkat melalui proses pembuatan instance. Hanya ada satu metode kelas yang bertanggung jawab untuk memunculkan instance baru __NSDictionaryI. Menurut class-dump, metode ini memiliki tanda tangan berikut: Untuk memahami di mana konten __NSDictionaryI disimpan, mari kita lihat sekilas proses pembuatan instance. Hanya ada satu metode kelas yang bertanggung jawab untuk menghasilkan instance baru dari __NSDictionaryI. Menurut class-dump, metode ini memiliki ciri-ciri sebagai berikut:
+ (id)__new:(const id *)arg1:(const id *)arg2:(unsigned long long)arg3:(_Bool)arg4:(_Bool)arg5;
Dibutuhkan lima argumen, dan hanya argumen pertama yang disebutkan. Serius, jika Anda menggunakannya dalam pernyataan @selector, itu akan berbentuk @selector(__new:::::). array objek dan jumlah kunci (objek) masing-masing. Perhatikan, bahwa array kunci dan objek ditukar dibandingkan dengan API yang dilihat publik yang berbentuk:
Dibutuhkan lima parameter, hanya parameter pertama yang diberi nama. Serius, jika Anda menggunakannya dalam pernyataan @selector, itu akan berbentuk @selector(__new:::::). Tiga parameter pertama dapat dengan mudah disimpulkan dengan menetapkan breakpoint pada metode dan mengintip isi register x2, x3, dan x4 yang masing-masing berisi larik kunci, larik objek, dan jumlah kunci (objek). Perhatikan bahwa kunci dan array objek ditukar dibandingkan dengan API yang dapat dilihat publik yang berbentuk:
+ (instancetype)dictionaryWithObjects:(const id [])objects forKeys:(const id <NSCopying> [])keys count:(NSUInteger)cnt;
Tidak masalah apakah argumen didefinisikan sebagai const id * atau const id [] karena array meluruh menjadi pointer ketika diteruskan sebagai argumen fungsi.
Tidak masalah apakah parameternya didefinisikan sebagai const id * atau const id[], karena array akan meluruh menjadi pointer ketika diteruskan sebagai parameter fungsi.Dengan tiga argumen yang tercakup, kita memiliki dua parameter boolean yang tidak teridentifikasi. Saya telah melakukan beberapa penggalian perakitan dengan hasil sebagai berikut: argumen keempat mengatur apakah kunci harus disalin, dan argumen terakhir memutuskan apakah argumen tidak boleh dipertahankan. Sekarang kita dapat menulis ulang metode dengan parameter bernama:
Dengan tiga parameter, kita memiliki dua parameter boolean yang tidak teridentifikasi. Saya melakukan penggalian di sekitar Majelis dan inilah yang saya temukan: parameter keempat mengontrol apakah kunci harus disalin, dan parameter terakhir menentukan apakah parameter tersebut tidak boleh dipertahankan. Sekarang kita dapat mengganti metode ini dengan parameter bernama:
+ (id)__new:(const id *)keys :(const id *)objects :(unsigned long long)count :(_Bool)copyKeys :(_Bool)dontRetain;
Sayangnya, kami tidak memiliki akses eksplisit ke metode privat ini, jadi dengan menggunakan cara alokasi reguler, dua argumen terakhir selalu disetel ke YES dan NO. Menariknya, __NSDictionaryI mampu mengendalikan tombol dan objek yang lebih canggih.
Sayangnya, kami tidak dapat mengakses metode privat ini secara eksplisit, jadi dengan menggunakan metode alokasi reguler, dua parameter terakhir selalu disetel ke YES dan NO. Meskipun demikian, menarik untuk dicatat bahwa __NSDictionaryI mampu menyediakan kontrol kunci dan objek yang lebih kompleks.
####Variabel instan diindeks oleh ivar yang diindeks
Melihat sekilas pembongkaran + __new::::: mengungkapkan bahwa malloc dan calloc tidak ditemukan. Sebaliknya, metode ini memanggil __CFAllocateObject2 dengan meneruskan kelas __NSDictionaryI sebagai argumen pertama dan meminta ukuran penyimpanan sebagai argumen kedua. Melangkah ke lautan ARM64 menunjukkan bahwa hal pertama yang dilakukan __CFAllocateObject2 adalah memanggil class_createInstance dengan argumen yang sama persis.
Menelusuri dekomposisi + __new:::: mengungkapkan bahwa baik malloc maupun calloc tidak ada. Sebaliknya, metode ini memanggil __CFAllocationObject2, meneruskan kelas __NSDictionaryI sebagai parameter pertama dan ukuran penyimpanan yang diminta sebagai parameter kedua. Melangkah ke lautan ARM64 menunjukkan bahwa hal pertama yang dilakukan __CFAllocationObject2 adalah memanggil class_createInstance dengan parameter yang sama persis.
Untungnya, saat ini kami memiliki akses ke kode sumber runtime Objective-C yang membuat penyelidikan lebih lanjut menjadi lebih mudah. Untungnya, kami sekarang memiliki akses ke kode sumber runtime Objective-C, yang membuat penelitian lebih lanjut menjadi lebih mudah.
Fungsi class_createInstance(Class cls, size_t extraBytes) hanya memanggil _class_createInstanceFromZone dengan meneruskan nil sebagai zona, namun ini adalah langkah terakhir alokasi objek. Meskipun fungsinya sendiri memiliki banyak pemeriksaan tambahan untuk berbagai keadaan, intinya dapat dibahas hanya dengan tiga baris:
Fungsi class_createInstance(Class cls, size_t extraBytes) hanya memanggil _class_createInstanceFromZone dan meneruskan nil sebagai zona, tetapi ini adalah langkah terakhir alokasi objek. Meskipun fungsinya sendiri memiliki banyak pemeriksaan tambahan untuk berbagai situasi, intinya dapat dijelaskan dalam tiga baris:
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
...
size_t size = cls->alignedInstanceSize() + extraBytes;
...
id obj = (id)calloc(1, size);
...
return obj;
}
Argumen extraBytes sangat deskriptif. Ini sebenarnya adalah jumlah byte tambahan yang memperbesar ukuran instans default. Sebagai bonus tambahan, perhatikan bahwa panggilan calloc-lah yang memastikan semua ivar di-nolkan saat objek dialokasikan.
Argumen terabyte sangat baik. Sebenarnya byte tambahan itulah yang membuat ukuran instance default membengkak. Juga, perhatikan bahwa panggilan calloc memastikan bahwa semua ivar di-nolkan ketika objek dialokasikan.
Bagian ivars yang diindeks tidak lebih dari ruang tambahan yang berada di akhir ivars biasa: Bagian indeks ivars tidak lebih dari spasi tambahan di akhir ivars biasa:
Mengalokasikan objek
Mengalokasikan ruang sendiri kedengarannya tidak terlalu menarik sehingga runtime menerbitkan pengakses: Mengalokasikan ruang satu per satu kedengarannya tidak menarik, sehingga runtime menerbitkan pengakses:
void *object_getIndexedIvars(id obj)
Tidak ada keajaiban apa pun dalam fungsi ini, ia hanya mengembalikan pointer ke awal bagian ivars yang diindeks: Tidak ada yang ajaib dalam fungsi ini, ia hanya mengembalikan pointer ke awal bagian indeks ivars:
Bagian ivars yang diindeks
Ada beberapa hal keren tentang ivar yang diindeks. Pertama-tama, setiap instance dapat memiliki jumlah byte tambahan berbeda yang didedikasikan untuknya. Inilah fitur yang digunakan __NSDictionaryI.
Ada beberapa hal keren tentang pengindeksan ivars. Pertama, setiap instance dapat memiliki jumlah byte tambahan yang berbeda. Inilah yang digunakan __NSDictionaryI.
Kedua, mereka menyediakan akses lebih cepat ke penyimpanan. Semuanya bermuara pada ramah cache. Secara umum, melompat ke lokasi memori acak (dengan melakukan dereferensi pointer) bisa memakan biaya yang mahal. Karena objek baru saja diakses (seseorang telah memanggil metode di dalamnya), kemungkinan besar ivar yang diindeks telah masuk ke cache. Dengan menjaga segala sesuatu yang diperlukan tetap dekat, objek dapat memberikan performa sebaik mungkin. Kedua, mereka menyediakan akses penyimpanan yang lebih cepat. Semuanya bermuara pada keramahan cache. Secara umum, melompat ke lokasi memori acak (dengan melakukan dereferensi pointer) bisa memakan biaya yang mahal. Karena objek baru saja diakses (seseorang memanggil metode di dalamnya), indeks ivar kemungkinan besar sudah ada di cache. Dengan menjaga semua yang dibutuhkan tetap dekat, objek dapat memberikan performa terbaik.
Terakhir, ivar yang diindeks dapat digunakan sebagai ukuran pertahanan kasar untuk membuat internal objek tidak terlihat oleh utilitas seperti class-dump. Ini adalah perlindungan yang sangat mendasar karena penyerang khusus dapat dengan mudah mencari panggilan object_getIndexedIvars dalam pembongkaran atau secara acak menyelidiki instance melewati bagian ivars regulernya untuk melihat apa yang terjadi.
Terakhir, indeks ivars dapat digunakan sebagai ukuran pertahanan kasar sehingga utilitas seperti class dump tidak dapat melihat struktur internal suatu objek. Ini adalah perlindungan yang sangat mendasar, karena penyerang khusus dapat dengan mudah mencari panggilan object_getIndexedIvars dalam pembongkaran, atau menyelidiki instance secara acak melalui bagian ivars regulernya untuk melihat apa yang terjadi.Meskipun kuat, ivar yang diindeks memiliki dua peringatan. Pertama-tama, class_createInstance tidak dapat digunakan di bawah ARC, jadi Anda harus mengkompilasi beberapa bagian kelas Anda dengan flag -fno-objc-arc untuk membuatnya bersinar. Kedua, runtime tidak menyimpan informasi ukuran ivar yang diindeks di mana pun. Meskipun dealloc akan membersihkan semuanya (seperti yang disebut free secara internal), Anda harus menyimpan ukuran penyimpanan di suatu tempat, dengan asumsi Anda menggunakan jumlah variabel byte tambahan.
Meskipun indeks ivar sangat kuat, ada dua hal yang perlu diperhatikan. Pertama, class_createInstance tidak berfungsi di bawah ARC, jadi Anda harus mengkompilasi beberapa bagian kelas dengan flag -fno-objc-arc agar bersinar. Kedua, informasi ukuran indeks ivar tidak disimpan di mana pun saat runtime. Meskipun dealloc membersihkan semuanya (karena memanggil gratis secara internal), Anda harus mempertahankan ukuran penyimpanan, dengan asumsi Anda menggunakan jumlah variabel byte tambahan.
Mencari Kunci dan Mengambil Objek Mencari kunci dan mengambil objek
Menganalisis Majelis Analisis Majelis
Meskipun pada titik ini kita dapat melihat contoh __NSDictionaryI untuk mengetahui cara kerjanya, kebenaran utamanya terletak pada perakitannya. Daripada membahas keseluruhan ARM64, kita akan membahas kode Objective-C yang setara.
Meskipun pada titik ini kita dapat melihat-lihat instance __NSDictionaryI untuk melihat cara kerjanya, kebenaran utamanya terletak pada perakitannya. Kami akan membahas kode Objective-C yang setara, bukan keseluruhan ARM64.
Kelas itu sendiri mengimplementasikan sangat sedikit metode, tapi menurut saya yang paling penting adalah objectForKey: – inilah yang akan kita bahas lebih detail. Karena saya tetap membuat analisis perakitan, Anda dapat membacanya di halaman terpisah. Ini padat, tetapi langkah menyeluruhnya akan meyakinkan Anda bahwa kode berikut ini kurang lebih benar.
Kelas itu sendiri mengimplementasikan sangat sedikit metode, tapi menurut saya yang paling penting adalah objectForKey: - ini adalah sesuatu yang akan kita bahas lebih detail. Karena saya sudah melakukan analisis kompilasinya, Anda bisa membacanya di halaman tersendiri. Memang padat, tetapi langkah lengkapnya akan membuat Anda percaya bahwa kode di bawah ini kurang lebih benar.
Kode C Kode C
Sayangnya, saya tidak memiliki akses ke basis kode Apple, sehingga kode rekayasa balik di bawah ini tidak sama dengan implementasi aslinya. Di sisi lain, tampaknya ini berfungsi dengan baik dan saya belum menemukan kasus edge yang berperilaku berbeda dibandingkan dengan metode asli. Sayangnya, saya tidak memiliki akses ke basis kode Apple, sehingga kode rekayasa balik di bawah ini tidak sama dengan implementasi aslinya. Di sisi lain, tampaknya ini berfungsi dengan baik, dan saya belum menemukan kasus sudut yang berperilaku berbeda dari metode sebenarnya.
Kode berikut ditulis dari perspektif __NSDictionaryI class:
Kode berikut ditulis dari perspektif kelas __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;
}
Saat Anda melihat lebih dekat pada kode C, Anda mungkin melihat sesuatu yang aneh tentang pengambilan kunci. Itu selalu diambil dari offset genap, sedangkan objek yang dikembalikan berada di indeks berikutnya. Ini adalah kelebihan dari penyimpanan internal __NSDictionaryI: ia menyimpan kunci dan objek secara bergantian:
Saat Anda mencermati kode C, Anda mungkin melihat beberapa keanehan dengan perolehan kunci. Itu selalu membutuhkan offset genap dan objek yang dikembalikan berada di indeks berikutnya. Ini adalah kesalahan fatal dalam penyimpanan internal __NSDictionaryI: ia menyimpan kunci dan objek secara bergantian:
Kunci dan benda disimpan secara bergantian
Update: Joan Lluch memberikan penjelasan yang sangat meyakinkan untuk layout ini. Kode asli dapat menggunakan array struct yang sangat sederhana: Update: Joan Lluch memberikan penjelasan yang sangat meyakinkan tentang layout ini. Kode asli dapat menggunakan susunan struktur yang sangat sederhana:
struct KeyObjectPair {
id key;
id object;
};
Metode objectForKey: sangat mudah dan saya sangat menganjurkan Anda untuk memikirkannya. Meskipun demikian, ada baiknya menunjukkan beberapa hal. Pertama-tama, ivar _szidx digunakan sebagai indeks ke dalam array __NSDictionarySizes, sehingga kemungkinan besar merupakan singkatan dari “indeks ukuran”.
Metode objectForKey: sangat sederhana dan saya sangat menyarankan Anda memikirkannya. Meskipun demikian, ada beberapa hal yang perlu diperhatikan. Pertama, ivar _szidx digunakan sebagai indeks ke dalam array __nsdictionarysize, sehingga kemungkinan besar mewakili “indeks ukuran”.
Kedua, satu-satunya metode yang dipanggil pada kunci yang diteruskan adalah hash. Pengingat untuk membagi nilai hash kunci dengan ukuran kamus digunakan untuk menghitung offset ke dalam bagian indeks ivars.
Kedua, satu-satunya metode yang dipanggil pada kunci yang diteruskan adalah hash. Hash kunci pengingat dibagi dengan ukuran kamus digunakan untuk menghitung offset ke dalam bagian ivars indeks.
Jika kunci di offset adalah nil, kita cukup mengembalikan nil, pekerjaan selesai:
Jika kuncinya adalah nil pada offset, kita cukup mengembalikan nil dan pekerjaan selesai:
Ketika slot kunci kosong, nihil dikembalikan
Namun jika kunci pada offset bukan nil, maka kedua kasus tersebut dapat terjadi. Jika kuncinya sama, maka kita mengembalikan objek yang berdekatan. Jika keduanya tidak sama maka terjadi tabrakan hash dan kita harus terus mencari lebih jauh. __NSDictionaryI terus mencari sampai kecocokan atau nil ditemukan:
Namun, kedua kasus tersebut mungkin terjadi jika kunci pada offset tidak nihil. Jika nilai kuncinya sama, objek yang berdekatan akan dikembalikan. Jika keduanya tidak sama, terjadi tabrakan hash dan kita harus terus mencari. __nsdictionari terus mencari hingga menemukan kecocokan atau nihil:
Kunci ditemukan setelah satu tabrakanPencarian seperti ini disebut dengan linear probing. Perhatikan bagaimana __NSDictionaryI membungkus fetchIndex ketika ujung penyimpanan tercapai. Perulangan for ada untuk membatasi jumlah pemeriksaan – jika penyimpanan penuh dan kondisi perulangan tidak ada, kami akan terus mencarinya selamanya.
Pencarian ini disebut probing linier. Perhatikan bagaimana __NSDictionaryI membungkus FetchIndex ketika mencapai sisi penyimpanan. Perulangan for digunakan untuk membatasi jumlah pemeriksaan - jika penyimpanan penuh dan kondisi perulangan tidak ada, kita akan terus mencari.
####__NSDictionaryUkuran & __NSDictionaryKapasitas
Kita sudah tahu __NSDictionarySizes adalah semacam array yang menyimpan berbagai kemungkinan ukuran __NSDictionaryI. Kita dapat beralasan bahwa ini adalah array NSUInteger dan tentu saja, jika kita meminta Hopper untuk memperlakukan nilainya sebagai bilangan bulat 64-bit yang tidak ditandatangani, hal ini menjadi sangat masuk akal:
Kita sudah tahu bahwa __nsdictionarysize adalah semacam array yang menyimpan __NSDictionaryI dengan ukuran berbeda. Kita dapat menyimpulkan bahwa ini adalah array NSUIntegers, dan memang jika kita meminta Hopper untuk menangani nilai-nilai ini sebagai bilangan bulat 64-bit yang tidak ditandatangani, tiba-tiba menjadi masuk akal:
___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
...
Dalam bentuk desimal yang lebih familiar, bilangan ini disajikan sebagai daftar 64 bilangan prima yang dimulai dengan barisan berikut: 0, 3, 7, 13, 23, 41, 71, 127. Perhatikan, bahwa bilangan-bilangan tersebut bukanlah bilangan prima berurutan yang menimbulkan pertanyaan: berapakah perbandingan rata-rata dari dua bilangan bertetangga? Ini sebenarnya sekitar 1.637 – sangat mirip dengan 1.625 yang merupakan faktor pertumbuhan untuk NSMutableArray. Untuk perincian mengapa bilangan prima digunakan untuk ukuran penyimpanan, jawaban Stack Overflow ini adalah awal yang baik.
Dalam bentuk desimal yang lebih familiar, ini mewakili daftar 64 bilangan prima, dimulai dengan barisan berikut: 0, 3, 7, 13, 23, 41, 71, 127. Perhatikan bahwa ini bukan bilangan prima berurutan. Hal ini menimbulkan pertanyaan berapa perbandingan rata-rata dua bilangan yang berdekatan? Sebenarnya sekitar 1,637, yang sangat dekat dengan faktor pertumbuhan NSMutableArray sebesar 1,625. Untuk detail lebih lanjut tentang mengapa bilangan prima digunakan untuk ukuran penyimpanan, jawaban Stack Overflow ini adalah awal yang baik.
Kita sudah mengetahui berapa banyak penyimpanan yang dapat dimiliki __NSDictionaryI, namun bagaimana __NSDictionaryI mengetahui indeks ukuran mana yang harus dipilih pada inisialisasi? Jawabannya terletak pada metode kelas + __new::::: yang disebutkan sebelumnya. Mengonversi beberapa bagian rakitan kembali ke C akan menampilkan kode berikut:
Kita sudah mengetahui berapa banyak penyimpanan yang dapat dimiliki __NSDictionaryI, tetapi bagaimana cara mengetahui indeks ukuran mana yang harus dipilih pada waktu inisialisasi? Jawabannya terletak pada metode kelas + __new:::: yang disebutkan sebelumnya. Mengonversi beberapa bagian rakitan kembali ke C akan menampilkan kode berikut:
int szidx;
for (szidx = 0; szidx < 64; szidx++) {
if (__NSDictionaryCapacities[szidx] >= count) {
break;
}
}
if (szidx == 64) {
goto fail;
}
Metode ini mencari secara linear melalui array __NSDictionaryCapacities hingga hitungan sesuai dengan ukurannya. Sekilas di Hopper menunjukkan isi array:
Metode ini mencari secara linear melalui array __nsdictionarycapacity hingga hitungan cocok dengan ukuran tersebut. Sekilas di Hopper, isi arraynya adalah sebagai berikut:
___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
...
Mengonversi ke basis-10 menghasilkan 0, 3, 6, 11, 19, 32, 52, 85 dan seterusnya. Perhatikan bahwa bilangan tersebut lebih kecil dari bilangan prima yang disebutkan sebelumnya. Jika Anda memasukkan 32 pasangan nilai kunci ke dalam __NSDictionaryI, ini akan mengalokasikan ruang untuk 41 pasangan, secara konservatif menghemat beberapa slot kosong. Hal ini membantu mengurangi jumlah tabrakan hash, menjaga waktu pengambilan sedekat mungkin dengan konstan. Terlepas dari kasus sepele 3 elemen, penyimpanan __NSDictionaryI tidak akan pernah penuh, rata-rata mengisi paling banyak 62% ruangnya.
Mengonversi ke basis-10 menghasilkan 0, 3, 6, 11, 19, 32, 52, 85, dan seterusnya. Perhatikan bahwa bilangan-bilangan ini lebih kecil dari bilangan prima yang disebutkan sebelumnya. Jika Anda memasukkan 32 pasangan nilai kunci ke dalam __NSDictionaryI, ini akan mengalokasikan ruang untuk 41 pasangan nilai kunci, sehingga secara konservatif menghemat cukup banyak slot kosong. Hal ini membantu mengurangi jumlah tabrakan hash dan menjaga waktu pengambilan sedekat mungkin dengan konstan. Kecuali untuk kasus sederhana yang terdiri dari 3 elemen, penyimpanan __NSDictionaryI tidak pernah penuh, rata-rata mengisi hingga 62% ruang.
Sebagai trivia, nilai tak kosong terakhir dari __NSDictionaryCapacities adalah 0x11089481C742 yaitu 18728548943682 di basis-10. Anda harus berusaha sangat keras untuk tidak mengikuti batas jumlah pasangan, setidaknya pada arsitektur 64-bit.
Sebagai trivia, nilai non-null terakhir dari __nsdictionarycapacity adalah 0x11089481C742, yang pada basis-10 adalah 18728548943682. Setidaknya pada arsitektur 64-bit, Anda harus berusaha keras untuk tidak mematuhi batas hitungan.
Simbol Non-Ekspor Bendera non-ekspor
Jika Anda menggunakan __NSDictionarySizes dalam kode Anda dengan mendeklarasikannya sebagai array extern, Anda akan segera menyadari bahwa itu tidak semudah itu. Kode tidak dapat dikompilasi karena kesalahan tautan – simbol __NSDictionarySizes tidak terdefinisi. Memeriksa perpustakaan CoreFoundation dengan utilitas nm:
Jika Anda menggunakan __nsdictionarysize dalam kode Anda dengan mendeklarasikan __nsdictionarysize sebagai array eksternal, Anda akan segera menyadari bahwa ini tidak mudah. Kode gagal dikompilasi karena kesalahan tautan - simbol __nsdictionarysize tidak ditentukan. Gunakan alat nm untuk memeriksa perpustakaan CoreFoundation:
nm CoreFoundation | grep ___NSDictionarySizes
…dengan jelas menunjukkan simbol-simbol yang ada di sana (masing-masing untuk ARMv7, ARMv7s dan ARM64): …dengan jelas menunjukkan keberadaan simbol (masing-masing ARMv7, ARMv7s, dan ARM64):
00139c80 s ___NSDictionarySizes
0013ac80 s ___NSDictionarySizes
0000000000156f38 s ___NSDictionarySizes
Sayangnya manual nm dengan jelas menyatakan: Sayangnya, manual nm dengan jelas menyatakan:
Jika simbol bersifat lokal (non-eksternal), jenis simbol diwakili oleh huruf kecil yang sesuai.Simbol untuk
__NSDictionarySizestidak diekspor – simbol tersebut dimaksudkan untuk penggunaan internal perpustakaan. Saya telah melakukan riset untuk mencari tahu apakah mungkin untuk menghubungkan dengan simbol yang tidak diekspor, namun ternyata tidak (tolong beri tahu saya jika ya!). Kami tidak dapat mengaksesnya. Artinya, kita tidak dapat mengaksesnya dengan mudah. Simbol untuk__nsdictionarysizetidak diekspor - simbol tersebut ditujukan untuk penggunaan internal perpustakaan. Saya melakukan riset dan bertanya-tanya apakah mungkin untuk menautkan ke simbol yang tidak diekspor, namun ternyata hal itu tidak mungkin (tolong beri tahu saya jika mungkin!) dan kami tidak dapat mengaksesnya. Artinya, kita tidak bisa mengaksesnya dengan mudah.
####Menyelinap ke dalam secara diam-diam
Berikut pengamatan yang menarik: di iOS 7.0 dan 7.1, konstanta kCFAbsoluteTimeIntervalSince1904 diletakkan tepat sebelum __NSDictionarySizes:
Pengamatan menarik di sini: di iOS 7.0 dan 7.1, konstanta kCFAbsoluteTimeInterval Since1904 dicantumkan langsung sebelum __nsdictionarysize:
_kCFAbsoluteTimeIntervalSince1904:
0x00000000001577a0 dq 0x41e6ceaf20000000
___NSDictionarySizes:
0x00000000001577a8 dq 0x0000000000000000
Hal terbaik tentang kCFAbsoluteTimeIntervalSince1904 adalah ia diekspor! Kami akan menambahkan 8 byte (ukuran double) ke alamat konstanta ini dan menafsirkan ulang hasilnya sebagai penunjuk ke NSUInteger:
Hal terbaik tentang kcfabsolutetimeintervallis sejak 1904 adalah ia diekspor! Kami akan menambahkan 8 byte (dua kali lipat ukurannya) ke alamat konstanta ini dan menafsirkan ulang hasilnya sebagai penunjuk ke NSUInteger:
NSUInteger *Explored__NSDictionarySizes = (NSUInteger *)((char *)&kCFAbsoluteTimeIntervalSince1904 + 8);
Kemudian kita dapat mengakses nilainya dengan pengindeksan yang mudah: Kami kemudian dapat mengakses nilainya melalui indeks yang mudah digunakan:
(lldb) p Explored__NSDictionarySizes[0]
(NSUInteger) $0 = 0
(lldb) p Explored__NSDictionarySizes[1]
(NSUInteger) $1 = 3
(lldb) p Explored__NSDictionarySizes[2]
(NSUInteger) $2 = 7
Peretasan ini sangat rapuh dan kemungkinan besar akan rusak di masa depan, tetapi ini adalah proyek uji coba jadi tidak masalah. Peretasan ini sangat rapuh dan kemungkinan besar akan rusak di masa mendatang, tetapi ini adalah proyek uji coba jadi sempurna.
__NSDictionaryI Karakteristik __NSDictionaryI Karakteristik
Sekarang kita telah menemukan struktur internal __NSDictionaryI, kita dapat menggunakan informasi ini untuk mencari tahu mengapa segala sesuatunya berjalan sebagaimana mestinya dan konsekuensi tak terduga apa yang ditimbulkan oleh penerapan __NSDictionaryI saat ini.
Sekarang kita telah menemukan struktur internal __NSDictionaryI, kita dapat menggunakan informasi ini untuk mencari tahu mengapa segala sesuatunya berjalan sebagaimana mestinya, dan apa konsekuensi tak terduga dari penerapan __NSDictionaryI saat ini.
Kode Cetakan Kode cetakan
Untuk membuat penyelidikan kami sedikit lebih mudah, kami akan membuat metode kategori pembantu NSDictionary yang akan mencetak konten instance
Untuk mempermudah penelitian kami, kami akan membuat metode kategori NSDictionary pembantu yang akan mencetak konten instance
- (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;
}
Urutan kunci/benda pada pencacahan sama dengan urutan kunci/benda pada penyimpanan
Urutan kunci/benda di enum sama dengan urutan kunci/benda di penyimpanan
Mari buat kamus sederhana yang berisi empat nilai: Mari buat kamus sederhana dengan empat nilai:
NSDictionary *dict = @{@1 : @"Value 1",
@2 : @"Value 2",
@3 : @"Value 3",
@4 : @"Value 4"};
NSLog(@"%@", [dict explored_description]);
Output dari deskripsi yang dieksplorasi adalah: Output dari deskripsi yang dieksplorasi adalah:
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
Dengan mengingat hal itu, mari kita lakukan enumerasi singkat melalui kamus: Dengan mengingat hal ini, mari membuat kamus enumerasi singkat:
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSLog(@"%@ - %@", key, obj);
}];
Dan hasilnya: dan keluaran:
3 - Value 3
2 - Value 2
1 - Value 1
4 - Value 4
Pencacahan tampaknya hanya berjalan melalui penyimpanan, mengabaikan kunci nil dan memanggil blok hanya untuk slot yang tidak kosong. Hal ini juga berlaku untuk metode enumerasi cepat, keyEnumerator, allKeys dan allValues. Itu sangat masuk akal. NSDictionary tidak diurutkan, jadi tidak masalah urutan kunci dan nilai apa yang diberikan. Menggunakan tata letak internal adalah pilihan termudah dan mungkin tercepat.
Enum tampaknya hanya mengulangi penyimpanan, mengabaikan kunci nil, dan hanya memanggil blok untuk slot non-null. Hal ini juga berlaku untuk metode enumerasi cepat, enumerator kunci, allKeys, dan allValues. Ini sangat masuk akal. NSDictionary tidak diurutkan, jadi urutan kunci dan nilai tidak menjadi masalah. Menggunakan tata letak internal adalah pilihan termudah dan tercepat.
Jika Anda membuat kesalahan, __NSDictionarySaya mungkin mengembalikan sesuatu untuk kunci nil! Jika Anda membuat kesalahan, __nsdictionarySaya mungkin mengembalikan sesuatu untuk kunci nihil!
Mari kita pertimbangkan sebuah contoh. Bayangkan kita sedang membuat game strategi 3D sederhana yang berlatar luar angkasa. Seluruh alam semesta terpecah menjadi sektor-sektor seperti kubus yang dapat diperebutkan oleh faksi imajiner. Suatu sektor dapat direferensikan dengan indeks i, j dan k. akan membuang-buang memori untuk menyimpan pointer nil. Sebagai gantinya, kita akan menggunakan penyimpanan kecil dalam bentuk NSDictionary dengan kelas kunci khusus yang akan membuatnya sangat mudah untuk menanyakan apakah ada sesuatu di lokasi tertentu.
Mari kita pertimbangkan sebuah contoh. Katakanlah kita sedang membuat game strategi 3D sederhana. Seluruh alam semesta terpecah menjadi area seperti kubus yang dapat dilawan oleh faksi imajiner. Suatu sektor dapat direferensikan dengan indeks i, j dan knya. Kita tidak boleh menggunakan array 3D untuk menyimpan informasi sektor - ruang permainan sangat besar dan sebagian besar kosong, jadi kita akan membuang-buang memori untuk menyimpan nihil pointer. Sebagai gantinya, kita akan menggunakan penyimpanan yang jarang dalam bentuk NSDictionary, dengan kelas kunci khusus yang akan membuatnya sangat mudah untuk menanyakan apakah ada sesuatu di lokasi tertentu.
Berikut antarmuka untuk kunci, kelas BC3DIndex:
Ini adalah antarmuka kunci, kelas 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
Dan implementasinya yang sama sepelenya: Dan implementasinya yang sama sepelenya:
@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
```Perhatikan bagaimana kita menjadi warga subkelas yang baik: kita menerapkan metode `isEqual`: dan `hash` dan memastikan bahwa jika dua indeks 3D sama maka nilai hashnya juga sama. Persyaratan kesetaraan objek terpenuhi.
Perhatikan bagaimana kita menjadi warga subkelas yang baik: kita menerapkan metode isEqual: dan hash dan memastikan bahwa jika dua indeks 3d sama, maka hashnya juga sama. Persyaratan kesetaraan objek terpenuhi.
Berikut trivianya: kode apa yang akan dicetak berikut ini?
Ini sedikit pertanyaan: kode apa yang akan dicetak berikut ini?
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]);
Seharusnya `(null)` kan? Tidak:
Seharusnya (null), kan? Tidak:
Asteroids!
Untuk menyelidiki hal ini lebih lanjut mari kita ambil deskripsi kamus:
Untuk menyelidiki ini lebih jauh, mari kita dapatkan deskripsi kamus:
Count: 3 Size index: 1 Size: 3 [0] <BC3DIndex: 0x17803d340> - A black hole! [1] <BC3DIndex: 0x17803d360> - Asteroids! [2] <BC3DIndex: 0x17803d380> - A planet!
Ternyata `__NSDictionaryI` tidak memeriksa apakah `key` yang diteruskan ke `objectForKey:` adalah `nil` (dan menurut saya ini adalah keputusan desain yang bagus). Memanggil metode `hash` pada `nil` akan mengembalikan `0`, yang menyebabkan kelas membandingkan kunci pada indeks `0` dengan `nil`. Ini penting: kunci tersimpanlah yang mengeksekusi metode `isEqual:`, bukan kunci yang diteruskan.
Hasilnya adalah __nsdictionari tidak memeriksa apakah kunci yang diteruskan ke objectForKey: adalah nihil (yang menurut saya merupakan keputusan desain yang bagus). Memanggil metode hash pada nil akan menghasilkan 0, yang akan menyebabkan kelas membandingkan kunci pada indeks 0 dengan nihil. Ini penting: kunci tersimpanlah yang mengeksekusi metode isEqual:, bukan kunci yang diteruskan.
Perbandingan pertama gagal, karena indeks `i` untuk “Lubang hitam!” adalah `2` sedangkan untuk `nil` nol. Kuncinya tidak sama sehingga menyebabkan kamus terus mencari, menemukan kunci lain yang tersimpan: kunci untuk “Asteroid!”. Kunci ini memiliki ketiga properti `i`, `j`, dan `k` yang sama dengan `0` yang juga akan dikembalikan `nil` ketika ditanya propertinya (melalui pemeriksaan `nil` di dalam `objc_msgSend`).
Perbandingan pertama gagal karena saya mengindeks "Lubang hitam!" yaitu 2 dan nil adalah 0. Kedua kunci tersebut tidak sama, yang menyebabkan kamus terus mencari dan menekan kunci lain yang tersimpan: "Asteroid!" Kunci ini memiliki ketiga atribut i, j, dan k yang sama dengan 0, yang juga merupakan nilai nil yang dikembalikan saat meminta atributnya (melalui pemeriksaan nihil di objc_msgSend).
Inilah inti permasalahannya. Implementasi `isEqual:` dari `BC3DIndex`, dalam kondisi tertentu, dapat mengembalikan `YES` untuk perbandingan `nil`. Seperti yang Anda lihat, ini adalah perilaku yang sangat berbahaya dan dapat dengan mudah mengacaukan segalanya. Selalu pastikan bahwa objek Anda tidak sama dengan `nil`.
Inilah inti permasalahannya. isEqual: Dalam kondisi tertentu, implementasi BC3DIndex dapat menghasilkan YES untuk perbandingan nihil. Seperti yang Anda lihat, ini adalah perilaku yang sangat berbahaya dan dapat dengan mudah mengacaukan segalanya. Selalu pastikan objeknya tidak sama dengan nihil.
#### Kelas Kunci Pembantu Kelas kunci tambahan
Untuk dua pengujian berikutnya kita akan membuat kelas kunci khusus yang akan memiliki nilai hash yang dapat dikonfigurasi dan akan mencetak sesuatu ke konsol saat menjalankan metode `hash` dan `isEqual:`.
Dalam dua pengujian berikutnya, kita akan membuat kelas kunci khusus yang akan memiliki nilai hash yang dapat dikonfigurasi dan mencetak konten ke konsol ketika metode hash dan isEqual: dijalankan.
Inilah antarmukanya:
Ini adalah antarmukanya:
@interface BCNastyKey : NSObject <NSCopying>
@property (nonatomic, readonly) NSUInteger hashValue;
- (instancetype)keyWithHashValue:(NSUInteger)hashValue;
@end
Dan implementasinya:
@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
Kunci ini sangat buruk: kita hanya setara dengan self, tetapi kita mengembalikan hash yang sewenang-wenang. Perhatikan bahwa hal ini tidak melanggar kontrak kesetaraan.
Kunci ini buruk: kita hanya menyamakan diri, tetapi kita mengembalikan hash yang sewenang-wenang. Perlu dicatat bahwa hal ini tidak melanggar kontrak kesetaraan.
#### isEqual tidak harus dipanggil untuk mencocokkan kunci
Mari buat kunci dan kamus:
Mari buat kunci dan kamus:
BCNastyKey *key = [BCNastyKey keyWithHashValue:3]; NSDictionary *dict = @{key : @“Hello there!”};
Panggilan berikut:
Nomor telepon berikut:
[dict objectForKey:key];
Cetak ini ke konsol:
Cetak ke konsol:
Key (&:0x17800e240 #:3) is asked for its hash
Seperti yang Anda lihat, metode `isEqual:` belum dipanggil. Ini keren sekali! Karena sebagian besar kunci di luar sana adalah literal `NSString`, mereka berbagi alamat yang sama di seluruh aplikasi. Bahkan jika kuncinya adalah string literal yang sangat panjang, `__NSDictionaryI` tidak akan menjalankan metode `isEqual:` yang berpotensi memakan waktu kecuali jika benar-benar diperlukan. Dan karena arsitektur 64-bit memperkenalkan pointer yang diberi tag, beberapa contoh `NSNumber`, `NSDate` dan, tampaknya, `NSIndexPath` juga mendapat manfaat dari pengoptimalan ini.
Seperti yang Anda lihat, metode isEqual: belum dipanggil. Ini keren! Karena sebagian besar kunci adalah literal NSString, mereka berbagi alamat yang sama di seluruh aplikasi. Bahkan jika kuncinya adalah string literal yang panjang, __NSDictionaryI tidak akan menjalankan metode isEqual: yang berpotensi memakan waktu kecuali benar-benar diperlukan. Sejak arsitektur 64-bit memperkenalkan pointer yang diberi tag, beberapa instance NSNumber, NSDate, dan NSIndexPath tampaknya juga mendapat manfaat dari pengoptimalan ini.
#### Kinerja kasus terburuk adalah linier. Kinerja kasus terburuk adalah linier
Mari buat kasus uji yang sangat sederhana:
Mari kita buat kasus uji yang sangat sederhana:
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 };
Satu baris pembunuh:
Kartu truf sederhana:
NSLog(@“Result: %@”, [[b objectForKey:targetKey] description]);
Mengungkapkan bencana:
Bencana terungkap:
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
Ini adalah kasus yang sangat patologis – setiap kunci dalam kamus telah diuji kesetaraannya. Meskipun setiap hash berbeda, namun tetap bertabrakan dengan setiap kunci lainnya, karena hash kunci tersebut kongruen modulo 7, yang ternyata merupakan ukuran penyimpanan kamus.
Ini adalah kasus yang sangat patologis - setiap kunci dalam kamus diuji kesetaraan Ben. Meskipun setiap hash berbeda, namun tetap bertabrakan dengan kunci lain karena hash kunci tersebut adalah modulo 7, yang merupakan ukuran penyimpanan kamus.Seperti disebutkan sebelumnya, perhatikan bahwa tes `isEqual:` terakhir tidak ada. `__NSDictionaryI` hanya membandingkan petunjuknya dan menemukan bahwa itu pasti kunci yang sama.
Seperti disebutkan sebelumnya, perhatikan bahwa tes isEqual: yang terakhir tidak ada. __nsdictionari cukup membandingkan pointer dan menemukan bahwa keduanya harus merupakan kunci yang sama.
Haruskah Anda peduli dengan pengambilan waktu linier ini? Sama sekali tidak. Saya tidak terlalu menyukai analisis probabilistik distribusi hash, tetapi Anda harus sangat beruntung karena semua hash Anda memiliki modulo yang kongruen dengan ukuran kamus. Beberapa tabrakan di sana-sini akan selalu terjadi, ini adalah sifat tabel hash, tetapi Anda mungkin tidak akan pernah mengalami masalah waktu linier. Yaitu, kecuali Anda mengacaukan fungsi `hash` Anda.
Haruskah Anda peduli dengan perolehan waktu linier ini? Sama sekali tidak. Saya bukan penggemar berat analisis probabilistik distribusi hash, tetapi Anda harus sangat beruntung karena semua hash Anda berukuran modulo kamus. Akan selalu ada beberapa tabrakan di sana-sini, itulah sifat tabel hash, tapi Anda mungkin tidak akan pernah mengalami masalah waktu linier. Kecuali Anda mengacaukan fungsi hash.
### Kata-Kata Terakhir Kata-kata Terakhir
Saya terpesona betapa sederhananya `__NSDictionaryI`. Tentu saja, kelas tersebut sudah memenuhi tujuannya dan tidak perlu membuat hal-hal menjadi terlalu rumit. Bagi saya, aspek implementasi yang paling indah adalah tata letak objek-kunci-objek-kunci. Ini adalah ide cemerlang.
Saya tertarik dengan betapa sederhananya kamus ini. Tentu saja, kelas ini pasti memenuhi tujuannya dan tidak perlu membuat hal-hal menjadi terlalu rumit. Bagi saya, aspek yang paling indah dari implementasi ini adalah tata letak objek-kunci-objek-kunci. Ini adalah ide yang bagus.
Jika Anda mengambil satu tip dari artikel ini maka saya akan memperhatikan metode `hash` dan `isEqual:` Anda. Memang benar, jarang ada yang menulis kelas kunci khusus untuk digunakan dalam kamus, namun aturan tersebut juga berlaku untuk `NSSet`.
Jika Anda mengambil salah satu tip dari artikel ini, maka saya sarankan untuk memperhatikan metode hash dan isEqual:. Memang benar, hanya sedikit orang yang menulis kelas kunci khusus untuk digunakan dalam kamus, namun aturan ini juga berlaku untuk NSSet.
Saya sadar bahwa suatu saat `NSDictionary` akan berubah dan temuan saya akan menjadi usang. Menginternalisasi rincian implementasi saat ini mungkin menjadi beban di masa depan, ketika asumsi-asumsi yang sudah diingat tidak lagi berlaku. Namun, saat ini dan saat ini sangat menyenangkan melihat cara kerjanya dan semoga Anda berbagi kegembiraan saya.
Saya tahu bahwa suatu saat NSDictionary akan berubah dan temuan saya akan menjadi usang. Menginternalisasi rincian implementasi saat ini dapat menjadi beban di masa depan ketika asumsi-asumsi yang diingat tidak lagi berlaku. Namun, saat ini, sungguh menyenangkan melihat cara kerjanya, dan saya harap Anda dapat berbagi kegembiraan saya.
Teks asli: https://ciechanow.ski/exposing-nsdictionary/
What to read next
Want more posts about iOS?
Posts in the same category are usually the best next step for reading more on this topic.
View same categoryWant to keep following #iOS?
Tags are useful for related tools, specific problems, and similar troubleshooting notes.
View same tagWant to explore another direction?
If you are not sure what to read next, return to the homepage and start from categories, topics, or latest updates.
Back home