MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C 字符串处理方法与优化

2021-07-215.3k 阅读

Objective-C 字符串基础

NSString 与 NSAttributedString

在 Objective-C 中,NSString 是用来表示不可变字符串的类。它提供了丰富的方法来处理文本数据。例如,创建一个 NSString 对象可以使用以下方式:

NSString *str = @"Hello, World!";

这是一种最常见的字面量创建方式。另外,也可以通过其他方式初始化,比如从文件读取、从网络获取等。

NSAttributedString 则是 NSString 的子类,它除了包含字符串本身的内容,还包含了字符串的属性信息,例如字体、颜色、下划线等。创建一个简单的 NSAttributedString 示例如下:

NSDictionary *attributes = @{NSFontAttributeName : [UIFont systemFontOfSize:18],
                             NSForegroundColorAttributeName : [UIColor redColor]};
NSAttributedString *attributedStr = [[NSAttributedString alloc] initWithString:@"Styled Text" attributes:attributes];

这里我们为字符串 Styled Text 设置了字体大小为 18 以及颜色为红色。

字符串的存储与编码

NSString 对象在内存中是以 UTF - 16 编码存储的。这意味着每个字符至少占用 2 个字节(对于基本多文种平面内的字符),对于一些非 BMP 字符,可能需要 4 个字节。例如,一个普通的英文字符串 @"abc" 在内存中占用 6 个字节(每个字符 2 个字节)。

在处理字符串编码转换时,NSString 提供了一些方法。比如将 NSString 转换为 NSData 时,可以指定编码格式:

NSString *str = @"你好";
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];

这里将包含中文字符的 NSString 转换为了 UTF - 8 编码的 NSData。如果要从 NSData 再转换回 NSString,可以这样做:

NSString *newStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

常用字符串处理方法

字符串拼接

在 Objective - C 中,拼接字符串有多种方式。对于 NSString(不可变字符串),如果要拼接多个字符串,一种常见的方法是使用 stringByAppendingString: 方法。例如:

NSString *str1 = @"Hello";
NSString *str2 = @", World";
NSString *result = [str1 stringByAppendingString:str2];
NSLog(@"%@", result); // 输出 "Hello, World"

但是,如果要进行大量的字符串拼接操作,这种方式效率较低,因为每次调用 stringByAppendingString: 都会创建一个新的字符串对象。

此时,NSMutableString 类就派上用场了。NSMutableStringNSString 的可变子类,它提供了 appendString: 方法来高效地拼接字符串。示例如下:

NSMutableString *mutableStr = [NSMutableString stringWithString:@"Start"];
[mutableStr appendString:@" - Middle"];
[mutableStr appendString:@" - End"];
NSLog(@"%@", mutableStr); // 输出 "Start - Middle - End"

在这个例子中,我们使用 NSMutableString 不断追加字符串,避免了每次拼接都创建新的不可变字符串对象,从而提高了效率。

字符串查找与替换

  1. 字符串查找 NSString 提供了多种查找方法。例如,rangeOfString: 方法用于查找一个子字符串在当前字符串中的位置。示例如下:
NSString *str = @"This is a test string";
NSRange range = [str rangeOfString:@"test"];
if (range.location != NSNotFound) {
    NSLog(@"Substring 'test' found at location %lu", (unsigned long)range.location);
} else {
    NSLog(@"Substring not found");
}

这里通过 rangeOfString: 查找子字符串 test,如果找到,会返回其在字符串中的位置,否则返回 NSNotFound

  1. 字符串替换 对于 NSMutableString,可以使用 replaceCharactersInRange:withString: 方法进行字符串替换。例如:
NSMutableString *mutableStr = [NSMutableString stringWithString:@"This is an old string"];
NSRange replaceRange = [mutableStr rangeOfString:@"old"];
if (replaceRange.location != NSNotFound) {
    [mutableStr replaceCharactersInRange:replaceRange withString:@"new"];
}
NSLog(@"%@", mutableStr); // 输出 "This is a new string"

在这个例子中,我们先查找子字符串 old,然后将其替换为 new

字符串分割与合并

  1. 字符串分割 NSStringcomponentsSeparatedByString: 方法可以根据指定的分隔符将字符串分割成数组。例如:
NSString *str = @"apple,banana,orange";
NSArray *components = [str componentsSeparatedByString:@","];
for (NSString *component in components) {
    NSLog(@"%@", component);
}
// 输出:
// apple
// banana
// orange

这里以逗号为分隔符,将字符串分割成了一个包含水果名称的数组。

  1. 字符串合并 与分割相反,componentsJoinedByString: 方法可以将数组中的字符串元素合并成一个字符串,使用指定的连接符。例如:
NSArray *words = @[@"Hello", @"World"];
NSString *result = [words componentsJoinedByString:@" "];
NSLog(@"%@", result); // 输出 "Hello World"

这里将数组中的两个字符串用空格连接起来,形成了一个新的字符串。

字符串处理的性能优化

避免不必要的字符串转换

在开发过程中,要尽量避免不必要的字符串转换操作。例如,将数字转换为字符串以及再转换回数字的频繁操作可能会带来性能损耗。如果只是在计算中使用数字,就没有必要将其转换为字符串。

假设我们有一个需求,要对一系列数字进行累加操作。一种不好的做法是将数字转换为字符串再处理:

NSMutableString *sumStr = [NSMutableString string];
for (int i = 1; i <= 1000; i++) {
    NSString *numStr = [NSString stringWithFormat:@"%d", i];
    [sumStr appendString:numStr];
}
// 这里还需要将 sumStr 再转换回数字进行累加计算,过程复杂且性能低

而正确的做法应该是直接进行数字计算:

int sum = 0;
for (int i = 1; i <= 1000; i++) {
    sum += i;
}

这样直接在数字层面进行操作,避免了不必要的字符串转换,大大提高了性能。

使用合适的数据结构

在处理大量字符串数据时,选择合适的数据结构非常重要。例如,如果需要对字符串进行频繁的插入和删除操作,NSMutableString 可能是一个不错的选择。但如果只是需要对字符串进行查找,NSDictionary 或者 NSSet 可能更合适。

假设我们有一个需求,要快速判断一组字符串中是否包含某个特定字符串。如果使用数组来存储这些字符串,每次查找都需要遍历数组,时间复杂度为 O(n)。而如果使用 NSSet,查找操作的时间复杂度接近 O(1)。示例如下:

NSSet *stringSet = [NSSet setWithObjects:@"apple", @"banana", @"cherry", nil];
BOOL contains = [stringSet containsObject:@"banana"];
if (contains) {
    NSLog(@"Set contains 'banana'");
} else {
    NSLog(@"Set does not contain 'banana'");
}

这里使用 NSSet 来存储字符串,在判断是否包含某个字符串时,效率比使用数组要高得多。

优化字符串拼接操作

如前文提到,在进行大量字符串拼接时,NSMutableStringNSString 的拼接方法更高效。另外,在使用 NSMutableString 时,尽量减少中间变量的创建。例如,不要每次拼接一个字符就创建一个新的临时字符串。

假设我们要拼接一个很长的字符串,一种不好的做法是:

NSMutableString *mutableStr = [NSMutableString string];
for (int i = 0; i < 10000; i++) {
    NSString *tempStr = [NSString stringWithFormat:@"%d", i];
    [mutableStr appendString:tempStr];
}

这里每次循环都创建了一个临时的 NSString 对象 tempStr,增加了内存开销和性能损耗。可以优化为:

NSMutableString *mutableStr = [NSMutableString string];
for (int i = 0; i < 10000; i++) {
    [mutableStr appendFormat:@"%d", i];
}

通过 appendFormat: 方法,直接在 NSMutableString 上进行格式化拼接,避免了临时字符串的创建,提高了性能。

缓存字符串处理结果

如果某些字符串处理操作的结果是固定不变的,或者在程序运行过程中会被多次使用,那么可以考虑缓存这些结果。例如,在一个频繁调用的方法中,每次都对同一个字符串进行相同的格式化处理,就可以将格式化后的结果缓存起来。

假设我们有一个方法,需要频繁获取当前日期的格式化字符串:

- (NSString *)getFormattedDate {
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyy - MM - dd"];
    NSDate *now = [NSDate date];
    return [formatter stringFromDate:now];
}

这个方法每次调用都会创建一个新的 NSDateFormatter 对象并进行日期格式化。如果这个方法被频繁调用,会带来一定的性能损耗。可以将 NSDateFormatter 对象作为类的属性进行缓存:

@interface MyClass : NSObject
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
@end

@implementation MyClass
- (NSDateFormatter *)dateFormatter {
    if (!_dateFormatter) {
        _dateFormatter = [[NSDateFormatter alloc] init];
        [_dateFormatter setDateFormat:@"yyyy - MM - dd"];
    }
    return _dateFormatter;
}

- (NSString *)getFormattedDate {
    NSDate *now = [NSDate date];
    return [self.dateFormatter stringFromDate:now];
}
@end

这样,NSDateFormatter 对象只会在第一次调用 dateFormatter 方法时创建,后续调用直接使用缓存的对象,提高了性能。

国际化与本地化中的字符串处理

字符串本地化

在开发国际化应用时,字符串本地化是非常重要的一部分。Objective - C 提供了 NSLocalizedString 宏来实现字符串的本地化。首先,需要在项目中创建本地化文件(.strings 文件)。例如,在 Base.lproj 目录下创建 Localizable.strings 文件,内容如下:

"Hello" = "你好";

然后在代码中使用 NSLocalizedString 宏来获取本地化字符串:

NSString *localizedStr = NSLocalizedString(@"Hello", nil);
NSLog(@"%@", localizedStr); // 在中文环境下输出 "你好"

这样,当应用在不同语言环境下运行时,会根据系统语言设置从对应的本地化文件中获取相应的字符串。

处理不同语言的字符串格式

不同语言在日期、数字等格式上可能有很大差异。在进行字符串格式化时,需要考虑到这些差异。例如,日期格式在不同语言中可能不同。对于日期格式化,应该使用 NSDateFormatter 的本地化设置。

NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateStyle:NSDateFormatterLongStyle];
[formatter setLocale:[NSLocale currentLocale]];
NSDate *now = [NSDate date];
NSString *formattedDate = [formatter stringFromDate:now];
NSLog(@"%@", formattedDate);

这里通过设置 NSDateFormatterlocale 为当前系统语言的本地化设置,使得日期格式符合当前语言环境的习惯。

对于数字格式化,也有类似的处理方式。NSNumberFormatter 可以根据本地化设置来格式化数字。例如:

NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
[numberFormatter setLocale:[NSLocale currentLocale]];
NSNumber *number = @1234567.89;
NSString *formattedNumber = [numberFormatter stringFromNumber:number];
NSLog(@"%@", formattedNumber);

这样,在不同语言环境下,数字会按照相应的格式进行显示,如在英语环境下可能显示为 1,234,567.89,在法语环境下可能显示为 1 234 567,89

字符串处理中的内存管理

自动释放池与字符串

在处理大量字符串操作时,合理使用自动释放池可以有效地控制内存峰值。例如,在一个循环中创建大量的字符串对象,如果不及时释放这些对象,会导致内存占用不断增加。

for (int i = 0; i < 10000; i++) {
    NSString *str = [NSString stringWithFormat:@"%d", i];
    // 这里的字符串对象会被自动释放,但如果循环次数多,会在一段时间内占用大量内存
}

可以通过在循环内创建自动释放池来及时释放这些字符串对象:

for (int i = 0; i < 10000; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"%d", i];
    }
}

这样,每次循环结束后,自动释放池中的字符串对象就会被释放,从而避免了内存峰值过高的问题。

字符串对象的引用计数

在 ARC(自动引用计数)环境下,Objective - C 开发者不需要手动管理对象的引用计数。但了解引用计数的原理对于理解字符串处理中的内存管理还是有帮助的。

对于 NSString 对象,当通过字面量创建时,系统会进行优化,将其存储在常量区,引用计数为 1 且不会被释放。例如:

NSString *str1 = @"Hello";
NSString *str2 = @"Hello";

这里 str1str2 指向的是同一个字符串对象,它们共享常量区的字符串,引用计数都为 1。

而通过 alloc 或者 init 方法创建的字符串对象,其引用计数初始值为 1。当对象的引用计数变为 0 时,系统会自动释放该对象所占用的内存。例如:

NSString *str = [[NSString alloc] initWithString:@"Test"];
// 这里 str 的引用计数为 1
// 当 str 超出作用域或者被赋值为 nil 时,其引用计数会减为 0,对象被释放

在 MRC(手动引用计数)环境下,开发者需要手动调用 retain 方法增加引用计数,调用 release 方法减少引用计数,以确保对象在合适的时机被释放。但在 ARC 环境下,这些操作都由编译器自动完成,大大减轻了开发者的负担。

内存泄漏与字符串处理

在字符串处理中,也可能会出现内存泄漏的情况。例如,在使用 NSMutableString 时,如果不正确地操作对象的生命周期,可能会导致内存泄漏。

假设我们有一个类,其中有一个 NSMutableString 属性:

@interface MyClass : NSObject
@property (nonatomic, strong) NSMutableString *mutableStr;
@end

@implementation MyClass
- (void)dealloc {
    // 在 ARC 环境下,不需要手动释放对象,但在 MRC 下需要
    // [_mutableStr release];
}
@end

如果在类的其他方法中对 mutableStr 进行了错误的赋值操作,比如:

- (void)someMethod {
    NSMutableString *tempStr = [[NSMutableString alloc] initWithString:@"Initial"];
    self.mutableStr = tempStr;
    // 如果这里忘记对 tempStr 进行释放(在 MRC 下),会导致内存泄漏
    // 在 ARC 下,虽然编译器会自动管理,但类似的逻辑错误也可能导致问题
}

在 MRC 下,如果忘记释放 tempStr,会导致这块内存无法被回收,从而造成内存泄漏。在 ARC 下,虽然编译器会自动管理内存,但如果在逻辑上对对象的引用关系处理不当,也可能导致对象无法被正确释放。

为了避免内存泄漏,在开发过程中要确保对象的生命周期被正确管理,尤其是在涉及到复杂的对象引用关系和字符串处理操作时。

通过深入理解 Objective - C 字符串处理的各种方法以及优化技巧,开发者可以编写出高效、稳定且内存管理良好的代码,提升应用的性能和用户体验。在实际开发中,要根据具体的需求和场景,选择最合适的字符串处理方式和优化策略。