Apple要求2015/2/1之后提交的包必须包含arm64,否则要被拒。因此,对于64-bit的支持可谓迫在眉睫,尤其是对于有很多遗留代码的项目,更要提早开工。
如何支持arm64
为了支持arm64结构,需要满足一下几个条件:
- 在Architectures设置项里添加arm64条目,如果使用的Xcode是6.0以上的版本,使用默认的配置项即可。
- 在Valid Architectures设置项里添加arm64条目。
- 讲Deployment Target改为大于等于5.1.1即可,因为arm64最低支持5.1.1系统。
- 支持64-bit的运行时环境,也即打开针对64-bit的编译器警告和错误,可以帮助你顺利地迁移到arm64。如果设置完成后没有明显地提示升级Xcode编译配置项,可以先build一下code,第一条警告就是建议支持64-bit的运行时环境,enable该设置项即可。
完成这些步骤之后,就可以build一个同时包含32-bit和64-bit的IPA文件。build完成之后你会发现有成千上百的警告和错误,这时候才是真正工作的开始。
另外,经过以上步骤之后,会发现当前的项目无法通过Xcode在iOS6以下的系统上联调,或者iTunes等直接安装打包的IPA文件。不过将打包的IPA文件通过AppStore发布是可以安装在iOS5.1.1上的,有人推断AppStore会对提交的IPA文件做一些magic的事情。如果想在iOS6以下的系统上联调,则需要在Valid Architectures里去掉arm64。
arm64适配
根据Apple官方介绍,arm64会带来以下的改变:
- Data Type.
- Function Calling.
- ARM Instruction.
下面分别介绍这三方面的具体变化。
Data Type
arm64带来的最大改变就是寻址空间和寄存器从32-bit增长为64-bit,系统可以提供更多的内存和更大的寄存器空间。下面两张图列出了转移到64-bit后Data Type的变化。
ILP32表示32-bit系统,LP64表示64-bit系统。图中的黑体表示64-bit相对于32-bit的不同,其改变可以概括为以下几点:
- 指针的大小从4Byte增长为8Byte。
- long,NSIteger,size_t,time_t,CFIndex,CGFloat都从4Byte增长为8Byte。
- long long,fpos_t,off_t,double的对齐都从4Byte增长为8Byte。
从以上总结可知,如果同时为32-bit和64-bit的系统开发软件,不可避免地会在32-bit和64-bit的数据之间产生运算和赋值等操作。面临的风险主要有以下:
- 32-bit和64-bit数据之间的操作。
这里的操作包括数学运算和赋值等运算,运算过程中可能会遇到数据截断,数据的溢出以及一些独特的边界情况。主要有以下几种情况:
- 32-bit和64-bit数据之间的赋值。将一个64-bit的数据赋给一个32-bit的变量,比如:
int intValue = NSItegerMax;
这将会导致数据截断,并不会得到期望的结果。同理如果将一个32-bit的数据赋给一个64-bit的变量,将会获得意想不到的结果,比如:
NSUInteger biggerIntValue = -1;
- 指向32-bit数据的指针变量和指向64-bit数据的指针变量互相赋值。如下操作:
int *pointerToInt = pointerToLong; pointerToLong = pointerToInt;
由于pointerToInt + 1实际上是+4,而pointerToLong + 1实际上是+8,所以转换之后再进行指针运算所得结果是错的。
- 指针和变量之间的赋值。如下:
int currentAddress = pointerToLong; NSInteger *pointerToNSInteger = currentAddress + 1;
这里的指针地址不但被截断了,并且+1操作也不会得到期望的结果。
- 隐式的枚举类型转换。编译器会为每个枚举类型分配一个合适的存储类型,可能是int也可能是NSInteger,根据其需要分配。因此,隐式地将枚举类型赋给其他类型的变量时,可能导致数据被截断,或者被错误地提升。
- 与系统相关的数据类型。NSIntegerMax在32-bit和64-bit所表示的值是不一样大地。另外代码中常见的hardCode也存在问题,比如左移操作中常见的32,24等。
针对以上列出的潜在风险,这里有两点建议:
- 使用相同的数据类型。尽量减少显式和隐式类型转换,使用相同的数据类型。
- 避免在指针与整形之间互相转换。尽量避免将指针赋给转型变量,或者显式地强制转换。如果真的需要,请使用uintptr_t类型。
- 32-bit和64-bit数据之间的赋值。将一个64-bit的数据赋给一个32-bit的变量,比如:
- 在32-bit和64-bit的软件之间交换数据。
32-bit和64-bit的软件很可能通过网络读写同一份文件,甚至用户也会用32-bit软件的数据覆盖64-bit软件下的同一份数据,这些都会导致无法预测地错误。比如NSInteger在32-bit是4Byte,而在64-bit上则是8Byte。如果这时在32-bit软件里访问由64-bit软件生成的内容为NSInteger类型的文件,结果无法预测。
struct second { int milliSecond; long microSecond; };
在32-bit软件中second的大小为8Byte,microSecond的偏移量为4。而在64-bit软件中second的大小为16Byte,而microSecond得偏移量为8。如果这是在32-bit和64-bit系统中持久化改模型或者将其传到网络上供其他软件访问,将不会得到正确的结果。 针对这个问题,这里提供一些建议:
-
使用一致的数据类型。
也即尽量使用与系统无关的数据类型,如果该软件同时存在32-bit和64-bit的版本,建议在32-bit和64-bit中使用相同的数据类型。比如,不管在32-bit软件还是64-bit软件上,尽量都使用int32_t或者int64_t。
- 创建内存模型一致的数据模型。
即数据模型大小和其中的元素偏移量都相同。针对上面提到的第二个问题,有以下两种解决方案:
struct second { int32_t milliSecond; int32_t microSecond; }; #pagram pack(4) struct second { int32_t milliSecond; int64_t microSecond; }; #pragma options align=reset
- 使用plist,XML和JSON进行序列化和持久化。
当使用NSCoder在64-bit软件上encode一个NSInteger数据,而后在32-bit软件上decode该NSInteger数据,同时该整型数值正好超出了32-bit int类型可以表示的范围时,将会抛出一个异常。
-
- 消耗更多的内存。
由以上的分析可知,很多的基本类型和指针地址都从4Byte增长为8Byte,这也预示着64-bit软件将消耗更多的内存。不但一些基本类型消耗了更多的内存,甚至常用的Foundation Object都要消耗更多的内存,由于其强大的功能,比如NSArray,NSDictionary。针对这个问题有以下几点建议:
- 选用合适的Foundation Object。
如果在NSArray里只存储一个简单的对象,然后产生成千上百这也的对象,那么消耗的内存将是巨大的。因此,尽量在合适的场合使用合适的类。
- 选择紧凑的数据模型。
尽量选择更合适的数据模型来表示你的数据。假定你要表示一个date类型,使用的数据模型如下:
struct date { NSInteger second; NSInteger minute; NSInteger hour; NSInteger day; NSInteger month; NSInteger year; };
在32-bit的软件上date的大小是24Byte,在64-bit的软件上date是48Byte,惊人吧!简单地改变一下设计,在达到目标的同时还可以节省很多的内存使用,结构如下:
struct date { long seconds; };
seconds表示流逝的总秒数,通过简单的计算即可得到year,month,day......
- 消除多余的padding。
为了性能的原因,编译器通常会在基本数据类型之间添加padding,以使他们对齐,避免多次访问内存。比如:
struct morePadding { //32-bit char second; //offset 0 int minute; //offset 4 char hour; //offset 8 NSInteger day; //offset 12 }; //total size 16
morePadding的实际大小为10Byte,而占据的内存大小为16Byte。经过重新设计将其改为以下结构:
struct morePadding { //32-bit int minute; //offset 0 NSInteger day; //offset 4 char second; //offset 8 char hour; //offset 9 }; //total size 10
- 使用尽量少的指针变量。
避免在数据模型中过度使用指针,考虑以下模型:
struct node{ node *previous; node *next; uint32_t value; };
在64-bit软件中node总大小为20Byte,而有效数据只有4Byte,80%的空间都被指针占据,可以考虑使用其他的数据结构代替。
- 在可以表达的范围内使用更小的数据类型。
如果只是表达几千几百的数字,则int就可以满足需求了,不需要使用NSInteger。宗旨就是使用够用的数据类型表示数字,没有必要64-bit软件就一定要使用int64_t类型。
- 只cache必须的数据。
为了性能优化的目的,我们的代码经常使用cache机制,即拿空间换时间,cache确实可以在很多地方提高软件的响应速度,甚至节省网络流量等。比如缓存网络图片避免下次联网,缓存经过滤镜处理以后的图片,避免CPU多次执行同意操作。64-bit软件消耗了更多的内存,如果缓存了过多的无关紧要的东西,可能反而会降低软件的整体性能。因此,建议以下的情况不要使用缓存:
- 可以容易地重新计算产生的。
- 很容易从其他地方获取的。
- 廉价地重新生成的。
- 只读数据可以通过mmap()访问的。
所以,应该经常地测试cache确实提升了性能。
- 合理使用@autoreleasepool。
尽快释放不再需要的autorelease对象,避免内存耗尽迫使系统发出UIApplicationDidReceiveMemoryWarningNotification通知。尤其是for循环和递归调用的场合,需要给出特别的关注。
- 处理UIApplicationDidReceiveMemoryWarningNotification。
所有相关的对象都必须处理UIApplicationDidReceiveMemoryWarningNotification通知,尤其是各个Controller,cache Manager需要第一时间响应该通知,避免导致低内存的crash。
- 选用合适的Foundation Object。
Function calling
如果没有使用汇编语言的话,转换到64-bit的影响并不是很大,只有一点,可变参数的函数的调用规则在64-bit软件上是不一样的。因此,对于函数调用建议如下:
- 实参和形参使用一致的数据类型。
- 避免在函数签名不一样的函数之间强制转换。
在不同的函数签名的指针变量之间互相传递,很容易导致调用函数的时候传递不合适的参数,从而无法得到预期的结果,尤其在固定参数的函数和可变参数的函数之间强制转换,如下所示:
int MyFunction(int a, int b, ...); int (*action)(int, int, int) = (int (*)(int, int, int)) MyFunction; action(1,2,3); // Error!
- 给可变参数的函数传递正确的参数。
由于可变参数列表通常未提供类型信息,如果这时传递了错误的参数值,将不会得到正确的结果。所以,可以考虑给可变参数添加格式字符串,提供一定的类型信息,比如printf()。
Objective-C Runtime
不要直接访问OC对象的isa,在64-bit软件里边isa不再是一个指向class object的指针,它包含一些指针数据和一些运行时信息。如果需要得到class object,使用object_getClass函数。
ARM64 Instruction
arm64的指令极大地不同于32-bit的指令,因此,汇编代码需要重写。arm64的函数调用约定跟标准的arm不太一样,可以参考iOS ABI Function Call Guide。