对NSValue的探索

开发过程中我有把结构体或者基本数据类型加入数组字典的需求,比如CGSize,CGRect,CGPoint等数据,但是大家都知道OC的容器中只能加入对象类型的数据。
一般有两个解决办法,第一种办法是转成 NSString ,用时再从NSString转成需要的数据类型,但是这种方式有两个缺点:

  1. 不支持自定义类型的结构体
  2. 性能太差!不,非常差!!

综上,如果没有必要我还是比较倾向于用NSValue,除此以外,使用NSValue还可以满足一些奇葩的需求,比如想加入容器中,但是不增加引用计数。
说起来NSValue,大家用的可能不多,但是NSNumber肯定很常用,其实NSNumber是NSValue的子类,在这就不详细介绍NSNumber了,下面说一下NSValue.

NSValue的API:

基本方法

- initWithBytes:objCType: 以指定的类型初始化一个包装了传入数据的NSValue对象

objCType 获得当前NSValue对象包装的数据类型

基本用法:

1
2
3
4
5
6
7
8
9
// 以CGRect 举例
CGRect testRect = CGRectMake(1, 2, 3, 4);

NSValue *value = [NSValue value:&testRect withObjCType:@encode(CGRect)];
CGRect valueRect = [value CGRectValue];
NSLog(@"rectValue = %@",NSStringFromCGRect(valueRect));

----打印结果----
rectValue = {{1, 2}, {3, 4}}

现在系统支持的结构体类型有以下几种:

支持类型 便捷设置方法 取值方法
CGPoint + valueWithCGPoint: CGPointValue
CGVectorValue + valueWithCGVector: CGVectorValue
CGSize + valueWithCGSize: CGSizeValue
CGRect + valueWithCGRect: CGRectValue
CGAffineTransform + valueWithCGAffineTransform: CGAffineTransformValue
UIEdgeInsets + valueWithUIEdgeInsets: UIEdgeInsetsValue
NSDirectionalEdgeInsets + valueWithDirectionalEdgeInsets: directionalEdgeInsetsValue
UIOffset + valueWithUIOffset: UIOffsetValue

如果是自定义的结构体类型怎么办?

其实苹果的官方开发文档已经提出了解决办法:创建一个NSValue的分类

例如下面仿照苹果的开发文档写一个直线的结构体分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
---- .h文件中 -------------

#import <Foundation/Foundation.h>

// 一条线的结构体,用两个点表示
typedef struct LinePoint {
CGPoint pointA;
CGPoint pointB;
}LinePoint;

@interface NSValue (CustomValue)
// 为LinePoint结构体生成NSValue对象的便捷方法
+ (instancetype)valuewithLinePoint:(LinePoint)value;

// 取值
@property (readonly) LinePoint LinePointValue;

@end

---- .m文件中 -------------

#import "NSValue+CustomValue.h"

@implementation NSValue (CustomValue)
+ (instancetype)valuewithLinePoint:(LinePoint)value {

return [NSValue value:&value withObjCType:@encode(LinePoint)];
}

- (LinePoint)LinePointValue {

LinePoint linePoint;


if (@available(iOS 11.0, *)) {
// iOS 11以后推荐用此方法
[self getValue:&linePoint size:sizeof(LinePoint)];
} else {
// 此方法在将来可能会被废弃
[self getValue:&linePoint];
}

return linePoint;
}

@end

---- 用法 ---------

LinePoint *line = malloc(sizeof(LinePoint));

line_1->pointA = CGPointMake(0, 0);
line_1->pointB = CGPointMake(100, 100);

NSValue *value = [NSValue valuewithLinePoint:*line];

LinePoint resultLine = [value LinePointValue];

NSLog(@"resultLine --- A:%@,B:%@",NSStringFromCGPoint(resultLine.pointA),NSStringFromCGPoint(resultLine.pointB));

---- 打印结果 ---------
resultLine --- A:{0, 0},B:{100, 100}

初始化方法

+ valueWithBytes:objCType: 作用与基本方法中的初始化方法一致

+ value:withObjCType: 作用与基本方法中的初始化方法一致

扩展方法

+ valueWithNonretainedObject: 创建一个包装了传入对象的NSValue对象,并且不增加引用计数,如果你想把一个对象加入到容器(NSArray,NSDictary,NSSet等)中,并不想使其引用计数增加,可以使用此方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
self.m_array = @[].mutableCopy;

NSObject *object1 = [[NSObject alloc] init];

NSObject *object2 = [[NSObject alloc] init];

NSLog(@"加入前:obj1=%@->%lu,obj2=%@->%lu",object1,(unsigned long)[object1 retainCount],object2,(unsigned long)[object2 retainCount]);

NSValue *value = [NSValue valueWithNonretainedObject:object2];

[self.m_array addObject:object1];

[self.m_array addObject:value];

NSLog(@"加入后:obj1=%@->%lu,obj2=%@->%lu,解包后:obj2_value = %@->%lu",object1,(unsigned long)[object1 retainCount],object2,(unsigned long)[object2 retainCount],[value nonretainedObjectValue],[[value nonretainedObjectValue] retainCount]);

---- 打印结果 ---------
加入前:obj1=<NSObject: 0x1c40169f0>->1,obj2=<NSObject: 0x1c40127c0>->1

加入后:obj1=<NSObject: 0x1c40169f0>->2,obj2=<NSObject: 0x1c40127c0>->1,解包后:obj2_value = <NSObject: 0x1c40127c0>->1

可以看到在使用+ valueWithNonretainedObject:包装前object1添加到数组中后引用计数增加了1,包装后,object2引用计数没有增加。但是需要注意的一点是:如果被包装的对象被释放了,从NSValue对象中取出的值并不是nil,具体是什么我并没有深究,有了解的可以告知我一下。

+ valueWithPointer: 创建一个包装了指针的NSValue对象
这个方法是一个很强大的方法,你可以用NSValue包装一个指针,比如一个结构体的指针,一个方法的指针

结构体的指针包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 假如有一个这样的结构体,需要包装成对象,但是不想写NSValue的分类
typedef struct PointStruct{
CGFloat x;
CGFloat y;
} PointStruct;
...
// 用法
PointStruct *testPoint = malloc(sizeof(PointStruct));
testPoint->x = 2.0f;
testPoint->y = 3.0f;

NSValue *pointValue = [NSValue valueWithPointer:testPoint];

PointStruct *result = (PointStruct *)[pointValue pointerValue];

GLLog(@"result,x=%f,y=%f",result->x,result->y);
---- 打印结果 -------------
result,x=2.000000,y=3.000000

// 方法指针的包装(@selector())

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSValue *selValue = [NSValue valueWithPointer:@selector(p_selecterTest)];

[self performSelector:[selValue pointerValue]];


...
- (void)p_selecterTest {

NSLog(@"此方法被运行 --- %s",__func__);
}
...

---- 打印结果 -----------

此方法被运行 --- -[ValueTestViewController p_selecterTest]

针对NSValue和NSString的类型转换性能测试

原始数据:

  • 测试环境:Xcode 9.2 + iPhone 6sPlus(真机) + iOS 11.2.5
  • 测试方式:将不同的数据类型使用各自的方式转成对象,再从相应对象转成原始数据,各循环1000000(一百万)次,每项测试三次
  • 测试指标:运行开始的时间戳和运行结束后的时间戳
  • 其中NSInteger测试项是使用NSNumber类进行转换的,NSNumber 是NSValue的子类
比较项目 NSValue_1 NSValue_2 NSValue_3 NSString_1 NSString_2 NSString_3
CGRect 1518600937.330795
1518600937.963562
1518601232.691197
1518601233.306452
1518601284.956521
1518601285.562490
1518601339.185438
1518601348.121756
1518601371.606468
1518601380.774911
1518601403.208475
1518601412.353251
CGPoint 1518601895.491483
1518601895.959701
1518601921.690009
1518601922.152504
1518601946.091435
1518601946.550951
1518601742.319816
1518601745.790220
1518601806.652705
1518601810.159964
1518601830.311894
1518601833.820591
NSInteger 1518602407.599263
1518602407.659061
1518602429.995714
1518602430.066774
1518602457.383714
1518602457.455960
1518602514.271342
1518602515.732950
1518602538.392223
1518602539.877542
1518602561.810762
1518602563.315673

处理结果

  • 处理方式:时间戳相减,获得运行消耗的准确时间,然后取三次的平均值
  • 处理结果:每项测试运行的时间,单位是秒,保留六位小数
比较项目(秒) NSValue NSString
CGRect 0.617997 9.083179
CGPoint 0.463410 3.495453
NSInteger 0.067701 1.483946

结论:结构体和NSValue互转时性能比NSString更好