《编写高质量代码:改善Objective-C程序的61个建议》——建议11:谨记兼容32位和64位环境下代码编写事项

简介:

本节书摘来自华章出版社《编写高质量代码:改善Objective-C程序的61个建议》一 书中的第2章,作者:刘一道,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

建议11:谨记兼容32位和64位环境下代码编写事项

在iOS 7版本出现之前,应用程序主要都是基于32位的iOS运行环境设计的,很少会考虑到要兼容64位的iOS运行环境。现在64位的iOS运行环境已经出现了。这个时候,在编写应用程序的时候,就不得不考虑了如何确保自己写的应用程序,既能在iOS的32位环境下运行又能在64位的环境下运行。
下面就编写兼容iOS 32位和64位运行环境的应用程序容易犯的错误,进行逐一介绍,希望能对各位有所帮助。

  1. 不要将长整型数据赋予整型
    在许多导致编程错误产生因素之中,最为典型的因素莫过于在应用程序的整个代码中,不能使用一贯的数据类型,导致编译应用程序代码时候,产生大量的警告提醒信息。

故此,当调用函数时,要确保接收到的结果与该函数返回的变量的类型相匹配。与接收变量相比,如果返回类型是一个较大的整数,那么该值将会被截断。
下面的代码示例就表现出了此种错误,分配给一个变量时却截断一个返回值。在此代码示例中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 类型。

  1. 善用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);
  1. 创建数据结构要注意固定大小和对齐
    当数据在32位和64位版本的应用程序之间共享时,就有必要确保创建兼容32位和64位的数据结构是完全相同的。当数据存储在一个文件或在一个传输网络的设备中时,其运行环境可能是相对立的。例如,用户把数据备份存储在32位的设备中,却在64位的设备中进行数据恢复,环境的差异性,在一定的程度上,会影响数据的正常使用,故此,对于这样的数据互操作存在的问题,必须要找到解决的方法。

(1)使用明确的整数数据类型
不管底层的硬件结构,C99标准提供了内置的数据类型都是特定大小的。当数据必须是一个固定大小时,或者当知道一个特定的变量有一个有限的可能值范围时,这个时候就应该考虑使用这些数据类型。通过选择适当的数据类型,会得到一个固定宽度的类型,可以存储在内存中,也避免分配一个变量时浪费内存,其范围远大于需要。
表 2-1列出每个 C99 类型和范围的允许值。

39e8d3c931a74668b9b2293f43a8fb02fdf11b22

(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位版本的应用程序中数据结构,能具有向后兼容性,就有很必要使用此选项。

  1. 选择一种紧凑的数据表示形式
    在编写应用程序代码时,选择一种恰当的数据结构形式,就可以比较好地表示数据。例如,使用下面的数据结构存储日历日期:
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)创建数据结构要注意固定大小和对齐。

相关文章
|
程序员 iOS开发
《编写高质量代码:改善Objective-C程序的61个建议》——导读
我一直在思考,如何才能编写出高质量、优秀的代码,我也在不停地探寻,希望找出类似于武侠小说中所说的武功秘籍,在编写代码一途可以帮助大家走“捷径”从而达到事半功倍的效果。
1039 0
|
Shell Linux C语言
Windows 下使用 GNUstep 编译并运行 Objective-C 程序
今晚上开始看《Objective-C 程序设计(第4版)》这本书(OSChina 正在做此书的书评活动,详情请看这里),到现在为止看到第 7 章,于是想动手试试写两简单的程序编译跑跑看。
1297 0
|
NoSQL iOS开发 编译器
xcode反汇编调试iOS模拟器程序(三)查看Objective-C函数与参数
在Objective-C函数的入口处(第一行)加断点,可用esp指针来探查参数。 以esp为基址,往后的偏移分别是: 0:函数执行完毕后的返回地址(不是返回值的地址哦) 4:对象实例的指针,即self指针 8:selector,实际是一个...
1187 0