iOS支持arm64

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等。

    针对以上列出的潜在风险,这里有两点建议:

    1. 使用相同的数据类型。尽量减少显式和隐式类型转换,使用相同的数据类型。
    2. 避免在指针与整形之间互相转换。尽量避免将指针赋给转型变量,或者显式地强制转换。如果真的需要,请使用uintptr_t类型。
  • 在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系统中持久化改模型或者将其传到网络上供其他软件访问,将不会得到正确的结果。 针对这个问题,这里提供一些建议:
    1. 使用一致的数据类型。

      也即尽量使用与系统无关的数据类型,如果该软件同时存在32-bit和64-bit的版本,建议在32-bit和64-bit中使用相同的数据类型。比如,不管在32-bit软件还是64-bit软件上,尽量都使用int32_t或者int64_t。

    2. 创建内存模型一致的数据模型。

      即数据模型大小和其中的元素偏移量都相同。针对上面提到的第二个问题,有以下两种解决方案:

       struct second {
       int32_t milliSecond;
       int32_t microSecond;
       };  
      
       #pagram pack(4)
       struct second {
       int32_t milliSecond;
       int64_t microSecond;
       };
       #pragma options align=reset
      
    3. 使用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。

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

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。