本节书摘来自华章出版社《编写高质量代码:改善Objective-C程序的61个建议》一 书中的第2章,作者:刘一道,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
建议11:谨记兼容32位和64位环境下代码编写事项
在iOS 7版本出现之前,应用程序主要都是基于32位的iOS运行环境设计的,很少会考虑到要兼容64位的iOS运行环境。现在64位的iOS运行环境已经出现了。这个时候,在编写应用程序的时候,就不得不考虑了如何确保自己写的应用程序,既能在iOS的32位环境下运行又能在64位的环境下运行。
下面就编写兼容iOS 32位和64位运行环境的应用程序容易犯的错误,进行逐一介绍,希望能对各位有所帮助。
- 不要将长整型数据赋予整型
在许多导致编程错误产生因素之中,最为典型的因素莫过于在应用程序的整个代码中,不能使用一贯的数据类型,导致编译应用程序代码时候,产生大量的警告提醒信息。
故此,当调用函数时,要确保接收到的结果与该函数返回的变量的类型相匹配。与接收变量相比,如果返回类型是一个较大的整数,那么该值将会被截断。
下面的代码示例就表现出了此种错误,分配给一个变量时却截断一个返回值。在此代码示例中PerformCalculation 函数将返回一个长整型。在 32 位运行时中,int 和 long 都是 32 位,即使代码不正确,但也能确保分配为int类型能有效工作。在 64 位运行时中,结果的高32位被分配时,将被丢掉。要保证不出现数据丢失,就应将结果赋给一个长整数 ,确保代码在32位和64位的运行时环境中都能有效运行。
long PerformCalculation(void);
//不正确
int x = PerformCalculation();
//正确
long y = PerformCalculation();
当作为一个参数传递值时,也会出现与上边一样的问题。例如,在下面的示例代码中64位运行时执行的输入参数时被截断。
int PerformAnotherCalculation(int input);
long i = LONG_MAX;
int x = PerformCalculation(i);
在下面的代码示例中,在64位运行时中返回值也被截断,因为该函数的返回类型超出返回值的范围。
int ReturnMax()
{
return LONG_MAX;
}
在上面的这些示例中,都是假定int 和 long 是完全相同的代码,即相同的数据类型,但是ANSI C 标准不能确保假设的这些情况都成立。当应用程序运行在64位环境中时,上面的代码就会出现明显错误。在默认编译环境下,编译器会自动启用32位和64位校验机制,一旦一个值被截断,在大部分情况下,编译器将会自动抛出警告。如果编译器没有启用32位和64位校验机制,这时候应该在编译器选项中明确启用它,或者同时选择转化选项,这样就能利用编译器的校验机制,发现更多更详细的潜在错误。
在Cocoa Touch的应用程序中,查找以下的整数类型并确保正确地使用它们:
long
NSInteger
CFIndex
size_t (调用的 sizeof 内在操作的结果)
在这两个运行时环境中的 fpos_t 和 off_t 的类型都是 64 位的,所以从来没有将它们分配给一个 int 类型。
- 善用NSInteger来处理32位和64位之间的转换
在编写应用程序时,可以在代码中多用NSInteger来处理数字类型的变量,因为NSInteger可以与iOS不同的运行环境兼容。
无论是在运行32位运行环境中,还是在运行在64位环境中,NSInteger的类型的应用贯穿于整个Cocoa Touch。其在 32 位运行时中是32 位整数,其在64 位运行时中是 64 位整数。所以,当从一个框架的方法接收信息时,它采用一个NSInteger的类型,因此务必使用NSInteger的类型来保存结果。
永远不要假设NSInteger的类型是一个大小相同的int类型,这里有几个关键例子来看看:
转换成NSNumber对象或转换NSNumber对象为其他。
编码和解码数据里,使用的是 NSCoder 类。尤其是,编码 NSInteger在64 位的设备上,但将它解码在 32 位的设备上,如果值超过一个 32 位整数的范围,解码方法将会引发异常。
使用 NSInteger 作为框架中定义的常量。特别值得注意的是NSNotFound 常数。它的价值是大于一个int类型的最大范围,所以在的应用程序中截断其价值往往会导致错误的出现。
在64位的代码中,CGFloat的大小改变了,GFloat类型变为一个64位单精度数。作为NSInteger的类型,不能想当然地把CGFloat认为是一个单精度或双精度数类型。因此,要使用一致的CGFloat。下面的示例就是使用Core Foundation来创建 CFNumber的。
// 不正确
CGFloat value = 200.0;
CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &value);
// 正确
CGFloat value = 200.0;
CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &value);
- 创建数据结构要注意固定大小和对齐
当数据在32位和64位版本的应用程序之间共享时,就有必要确保创建兼容32位和64位的数据结构是完全相同的。当数据存储在一个文件或在一个传输网络的设备中时,其运行环境可能是相对立的。例如,用户把数据备份存储在32位的设备中,却在64位的设备中进行数据恢复,环境的差异性,在一定的程度上,会影响数据的正常使用,故此,对于这样的数据互操作存在的问题,必须要找到解决的方法。
(1)使用明确的整数数据类型
不管底层的硬件结构,C99标准提供了内置的数据类型都是特定大小的。当数据必须是一个固定大小时,或者当知道一个特定的变量有一个有限的可能值范围时,这个时候就应该考虑使用这些数据类型。通过选择适当的数据类型,会得到一个固定宽度的类型,可以存储在内存中,也避免分配一个变量时浪费内存,其范围远大于需要。
表 2-1列出每个 C99 类型和范围的允许值。
(2)对齐64位整数类型时要小心
在64位运行时,所有的64位整数类型的变化从4字节到8字节对齐。即使明确指定每个整数类型,这两种结构仍然可能是不相同的两个运行时。在代码清单2-1中,对齐改变,即使该字段声明明确的整数类型。
代码清单2-1 对齐的64位整数结构
struct bar {
int32_t foo0;
int32_t foo1;
int32_t foo2;
int64_t bar;
};
当使用32位的编译器编译此代码时,字段栏(field bar)是从结构起始开始的12字节;当使用 64 位编译器编译该代码时,字段栏(field bar)是从结构起始的16字节开始的。在foo2中填充4字节,就可以确保foo2的栏(bar)具有8字节,从而实现边界对齐。
如果定义新的数据结构,首先组织的元素具有最大对齐值,最后的是最小的元素。这个结构组织消除正是大多数的填充字节需要的。如果正在使用现有的结构,其中包括未对齐的 64 位整数,可以使用 pragma 来强制其正确对齐。代码清单2-2给出了相同的数据结构,但这里的结构是被迫使用32位对齐规则的。
代码清单2-2 使用的pragma控制对齐
#pragma pack(4)
struct bar {
int32_t foo0;
int32_t foo1;
int32_t foo2;
int64_t bar;
};
#pragma options align=reset
只有在必要时才使用此选项,主要因为访问未对齐的数据,易造成性能上损失。故此要想确保自己的32位版本的应用程序中数据结构,能具有向后兼容性,就有很必要使用此选项。
- 选择一种紧凑的数据表示形式
在编写应用程序代码时,选择一种恰当的数据结构形式,就可以比较好地表示数据。例如,使用下面的数据结构存储日历日期:
struct date
{
NSInteger second;
NSInteger minute;
NSInteger hour;
NSInteger day;
NSInteger month;
NSInteger year;
};
这种结构的长度为24字节;在64位运行时,它需要48字节,只为一个日期!一个更紧凑的表示方式是以秒数来存储某一特定时间。必要时,将此紧凑的表示形式转换为日历的日期和时间。
struct date
{
uint32_t seconds;
};
对于对齐的数据结构,编译器有时会向其中添加填充物,例如:
struct bad
{
char a; // offset 0
int32_t b; // offset 4
char c; // offset 8
int64_t d; // offset 16
};
这种结构包括 14 字节的数据,但由于填充,它占用的 24 字节的空间。更好的设计方式是对字段进行从大到小的排序来对齐。
struct good
{
int64_t d; // offset 0
int32_t b; // offset 8
char a; // offset 12;
char c; // offset 13;
};
要点
(1)不要将长整型数据赋予整型。
(2)利用用NSInteger来处理32位和64位之间的转换。
(3)创建数据结构要注意固定大小和对齐。