Особливості створення NSString

NSLog(123456789) != 123456789Стаття розрахована на новачків в Objective-C і розповідає про один спосіб вистрілити собі в ногу. Ми спробуємо створити два різних об'єкта NSString з однаковим текстом, досліджуємо реакцію на це різних компіляторів, а також дізнаємося, при яких умовах NSLog(@"%@", @«123456789») виведе зовсім не «123456789».

Об'єкти NSString і покажчики

Як ви думаєте, що виведе наступний код?
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSString *a = @"123456789";
NSString *b = a;
NSLog(@"%p %p", a, b);
}
return 0;
}

Природно, визначники будуть рівні («об'єкти присвоюються за посиланням»), так що NSLog() надрукує два однакових адреси пам'яті. Ніякої магії:

2015-01-30 14:39:27.662 1-nsstring[13574] 0x602ea0 0x602ea0

Тут і далі адреси об'єктів наводяться як приклад; при спробі відтворення фактичні значення, зрозуміло, будуть іншими.

Давайте спробуємо добитися того, щоб у нас було два різних NSString з однаковим текстом. У разі інших стандартних класів, наприклад, NSArray, ми могли б написати так:
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSArray *a = @[@"123456789"];
NSArray *b = @[@"123456789"];
NSLog(@"%p %p", a, b);
}
return 0;
}

Оскільки ми ініціалізувати NSArray окремо, то вони були поміщені в різні ділянки пам'яті і в консолі з'являться два різні адреси:

2015-01-30 14:40:45.799 2-nsarray[13634] 0xa9e1b8 0xaa34e8

Однак застосування такого ж підходу до NSString не призведе до бажаного ефекту:
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSString *a = @"123456789";
NSString *b = @"123456789";
NSLog(@"%p %p", a, b);
}
return 0;
}

2015-01-30 14:41:41.898 3-nsstring[13678] 0x602ea0 0x602ea0

Як бачимо, незважаючи на роздільну ініціалізацію, обидва покажчика раніше посилаються на одну і ту ж область пам'яті.

Використання stringWithString

Трохи покопавшись в NSString, ми виявляємо метод stringWithString, який «повертає string created by copying the characters from another given string». Так це ж те, що нам треба! Спробуємо наступний код:
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSString *a = @"123456789";
NSString *b = [NSString stringWithString:@"123456789"];
NSString *з = [NSString stringWithString:b];
NSLog(@"%p %p %p", a, b, з);
}
return 0;
}

Виявляється, що висновок цієї програми залежить від використовуваної версії компілятора. Так clang під Ubuntu на LLVM 3.4 дійсно створить три різних об'єкта, розташованих у різних комірках пам'яті. Але компіляція зазначеного коду в Xcode за допомогою clang під Mac на LLVM 3.5 згенерує один об'єкт і три пойнтера на нього:

2015-01-30 17:59:02.206 4-nsstring[670:21855] 0x100001048 0x100001048 0x100001048

Сеанс магії з викриттям

Вищевказані дивацтва пояснюються спробами компілятора оптимізувати рядкові ресурси. Зустрічаючи у вихідному коді рядкові об'єкти з однаковим змістом, він для економії витрат на зберігання і порівняння створює їх тільки один раз. Ця оптимізація виконується також і на етапі лінкування: навіть якщо рядки з однаковим текстом знаходяться в різних модулях, швидше за все вони будуть створені тільки один раз.

Оскільки тип NSString є незмінним (для змінюваних рядків використовується NSMutableString), то така оптимізація є безпечною. До тих пір, поки ми маніпулюємо з рядками тільки методами класу NSString.

Компілятор, втім, не всемогутній. Один з найпростіших способів заплутати його і дійсно створити два різних NSString c однаковим текстом — такий:
#import "Foundation/Foundation.h"
int main(){
@autoreleasepool {
NSString *a = @"123456789";
NSString *b = [NSString stringWithFormat:@"%@", a];
NSLog(@"%p %p", a, b);
}
return 0;
}

GCC

Аналогічну оптимізацію рядкових констант виконує і gcc при компіляції коду на Сі. Наприклад,
#include < stdio.h>
void main(){
char *a = "123456789";
char *b = "123456789";
printf("%p %p\n", a, b);
}

виведе 0x4005f4 0x4005f4.

Однак є суттєва відмінність c clang: gcc розміщує такі рядкові константи в read-only сегменті — спроби змінити їх рантайме (наприклад, a[0]='0') призведуть до segmentation fault. Щоб розмістити рядки в heap, де вони можуть бути змінені, потрібно замінити char *a на char a[], однак у такому випадку gcc не буде застосовувати оптимізацію. Наступний код створить вже дві різні рядки:
#include < stdio.h>
void main(){
char a[] = "123456789";
char b[] = "123456789";
printf("%p %p\n", a, b);
}

0x7fff17ed0020 0x7fff17ed0030

Стрілянина в ногу

Отже, ми знаємо, що зустрічаючи у вихідному коді однакові рядкові об'єкти, компілятор оптимізує їх і створює NSString тільки один раз. При цьому він створює її в heap, де вона може бути змінена за допомогою ручних маніпуляцій з покажчиком. (У plain C, як обговорювалося вище, таке неможливо.)

Відгадайте, що друкує наступний код?
#import <Foundation/Foundation.h>
void bad(){
NSString* a = @"123456789";
char* aa = (__bridge void *)(a);
aa[8] = 92;
}

int main(){
@autoreleasepool {
bad();
NSLog(@"%@", @"123456789");
}
return 0;
}

В залежності від компілятора результат може бути різним: мій Xcode під Маком друкує набір кракозябр «㈱㐳㘵㠷9䀥», а clang в Убунту виводить фрагмент із службової інформації «red:pars». У будь-якому випадку, це ніяк не очікуване «123456789». Експерименти з іншими значеннями aa[8], а також aa[16], пропоную читачеві виконати самостійно.

Найгірше те, що функція bad() з останнього прикладу може перебувати за хедером, наприклад, підключається бібліотеці іншого автора, який по своїм потребам змінював свій особистий (як йому здавалося) NSString. Розумний компілятор все одно знайде збігаються рядкові константи і замкне їх на один пойнтер, після чого псування змінної всередині bad() спричинить перетворення рядка у контексті main() в ієрогліфи.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.