iOS 类簇

遇到类簇

以前并不了解类簇的概念,直到在查找定位线上生产 Bug 时,看到了如下日志:

reason: *** -[NSPlaceholderString initWithString:]: nil argument
reason: *** -[NSPlaceholderString initWithString:]: nil argument
callStackSymbols: (
0 CoreFoundation 0x00000001829f6dc8 <redacted> + 148
1 libobjc.A.dylib 0x000000018205bf80 objc_exception_throw + 56
2 CoreFoundation 0x00000001829f6cf8 <redacted> + 0
3 Foundation 0x00000001832febbc <redacted> + 112
4 ************** 以下堆栈信息省略 ***************

线上的 Bug 不能直接断点至 Crash 的位置,NSPlaceholderString 是什么,是 NSString 吗?没直接使用过,而且直接使用也会报错。于是 Google 调研,这才了解到类簇的概念。

类簇是Foundation框架中广泛使用的设计模式。类簇将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,以这种方法来组织类可以简化一个面向对象框架的公开架构,而又不减少功能的丰富性。

所以创建 NSString 对象时,得到的可能是 NSLiteralString、NSCFString、NSSimpleCString,NSPlaceholderString等。对程序员来说,我们创建不同的 NSString 对象调用同一个接口 A,接口 A 的底层实现可能是不同的,但对程序员来说是相同的稳定的,使我们编写的代码可以独立于底层实现。

常见的类簇有 NSString、NSArray,NSDictionary等,我们以 NSString 为例,通过创建不同的 NSString 对象,然后使用 object_getClassName(obj) 方法打印对象,查看不同对象区别。

# NSPlaceholderString->NSString
id str0 = [NSString alloc];
# __NSCFConstantString->__NSCFString->NSMutableString->NSString
id str1 = [[NSString alloc] init];
# NSTaggedPointerString->NSString
id str3 = [NSString stringWithFormat:@"123"];
# NSPlaceholderMutableString->NSMutableString->NSString
id str4 = [NSMutableString alloc];
# __NSCFString->NSMutableString->NSString
id str5 = [NSMutableString new];

定位解决问题

经排查项目中大量使用了[[NSString alloc] initWithString:方法来初始化字符串对象,例如代码下面的代码,dict 是后台返回的数据,当 data 为 nil 时,就会发生-[NSPlaceholderString initWithString:]: nil argument的 crash。

NSDictionary *dict = nil;
NSString *str = [[NSString alloc]initWithString:[dict objectForKey:@"key"]];
NSLog(@"%@",str);

解决办法很简单,使用[NSString stringWithFormat:@"%@",[dict objectForKey:@"key"]]方法代替即可。但这样改动太大,例如项目中有八百多处,全部替换显然不理智。正确的方法是用运行时做 Crash 防护。

新建一个 NSObject 分类,增加一个运行时交互的方法,在其他基类中很方便做运行时。

///MARK: - NSObject+Swizzling.h
#import <Foundation/Foundation.h>

@interface NSObject (Swizzling)
///  NSObject 分类增加运行时交互的方法
+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector;
@end

///MARK: - NSObject+Swizzling.m
#import "NSObject+Swizzling.h"
#import <objc/runtime.h>
@implementation NSObject (Swizzling)
+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector {
Class class = [self class];

Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

// 若已经存在,则添加会失败
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

// 若原来的方法并不存在,则添加即可
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end

然后新建一个 NSString 的分类,在分类中做方法交换。

///MARK: - NSString+solveBug.h
#import <Foundation/Foundation.h>
@interface NSString (solveBug)
@end

///MARK: - NSString+solveBug.m
#import "NSString+solveBug.h"
#import "NSObject+Swizzling.h"
#import <objc/message.h>

@implementation NSString (solveBug)
//NSPlaceholderString
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

[objc_getClass("NSPlaceholderString") swizzleSelector:@selector(initWithString:)
withSwizzledSelector:@selector(safeInitWithString:)];
});
}

// 防止 [[NSString alloc] initWithString:nil] 崩溃
- (instancetype)safeInitWithString:(id)obj {
if (obj == nil) {
return @"";
}else{
return  [self safeInitWithString:obj];
}
}
@end

同理,新建一个 NSMutableString 的分类,防止 NSMutableString 使用 initWithString 的时候 Crash。

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSMutableString (solveBug)
@en
NS_ASSUME_NONNULL_END

#import "NSMutableString+solveBug.h"
#import "NSObject+Swizzling.h"
#import <objc/message.h>

@implementation NSMutableString (solveBug)

//NSPlaceholderMutableString
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

[objc_getClass("NSPlaceholderMutableString") swizzleSelector:@selector(initWithString:)
withSwizzledSelector:@selector(safeInitWithString:)];
});
}

// 防止 [[NSMutableString alloc] initWithString:nil] 崩溃
- (instancetype)safeInitWithString:(id)obj {
if (obj == nil) {
return [self safeInitWithString:@""];
}else{
return [self safeInitWithString:obj];
}
}
@end