iOS研发适配:32bit/64bit & iOS10 - iOS13

4,285 阅读11分钟

在 iOS 开发的过程中,版本和机型适配是大家都会遇到的“坑”。如果不注意不同版本API和编译器的差异,那么只会Bug频出,屡屡返工啦。

参考资料总结了面对不同位数的机器、不同 iOS 版本时可能遇到的“坑”,在学习整理、避免自己踩坑的同时也希望能够及时输出分享,为大家创造价值。

欢迎大家留言讨论,一起进步(〃’▽’〃)!


目录

1. 32 位 / 64 位设备适配

1.1  32 位、64 位的概念
1.2  哪些 iOS 设备是 32 位 & 怎样判断
1.3  32 位和 64 位的不同 & 如何解决
	1.3.1 数据基本类型长度和对齐方式
	1.3.2 BOOL 值的编译差异
	1.3.3 指令集的编译差异
	1.3.4 64bit 编译时的容错优化

2. iOS10 -> iOS13 适配一览

2.1 iOS13
	2.1.1 DarkMode
	2.1.2 模态弹出默认交互改变
	2.1.3 私有 KVC 使用会 crash
	2.1.4 App 启动过程中,部分 View 会无法实时获取到 frame
	2.1.5 DeviceToken 格式变化
	2.1.6 UIWebView 的废弃
2.2 iOS11
	2.2.1 UIScrollView: contentInsetAdjustmentBehavior
	2.2.2 UITableView: Self-Sizing
	2.2.3 授权相关
2.3 iOS10
	2.3.1 跳转到app内的隐私数据设置页面
	2.3.2 UIFeedbackGenerator 触觉反馈

一、32 位 / 64 位设备适配

1.1 32位、64位的概念

“位”:CPU 中通用寄存器的数据宽度

相对于 32 位而言,64 位的 CPU 位宽增加一倍,使其能够处理更多更精确的数据,因此有一定加快数据处理的作用。

1.2 哪些 iOS 设备是32位 & 怎样判断

32 bit:iphone5 及之前

64 bit:iPhone5s 及之后(iPad Air之后)

判断是多少位设备:两种方法

///* 方法一 *///
    if (sizeof(void *) == 4) {
        NSLog(@"32-bit App");
    } else if (sizeof(void *) == 8) {
        NSLog(@"64-bit App");
    }
    ///* 方法二 *///
#if __LP64__
#define kNum 64
#else
#define kNum 32
#endif
    NSLog(@"kNum: %d",kNum);

__LP64__ 是由预处理器定义的宏,代表当前操作系统是 64 位

1.3 32位和64位的不同 & 如何解决

1.3.1 数据基本类型长度和对齐方式

如表所示为 32bit、64bit 的数据类型长度和对齐方式,主要不同在于

  • long 长整数类型
  • pointer 指针类型
  • NSInteger:32bit 时与 int 长度相同,64 bit 时与 long 长度相同

开发Tips

(1) 常规不用 int,用 NSInteger;大数(>=10位) 用 long long

NSInteger 根据位数自动返回不同类型,适配不同位数的存储方式,使更高位的 CPU 能发挥出性能。 在 iOS 12.4 > usr/include > objc > NSObjCRuntime.h 中对 NSInteger 定义为:

#if __LP64__ || 0 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif

32bit 时,NSInteger 能表示 int 的 4 字节:-21 4748 3648 ~ 21 4748 3647 (2的31次方 - 1)

64bit 时,NSInteger 能表示 long 的 8 字节:-922 3372 0368 5477 5808 ~ 922 3372 0368 5477 5807(2的63次方 - 1)

虽然 64bit 时范围大,但涉及到设备适配,当要存储的数 >= 10 位时,建议使用 long long 类型,来保证在 32 位机器上不会崩溃。

Eg. 涉及到时间戳为毫秒的情况下,定义相应字段是 long long类型,通过 longLongValue 获取到值,就不会存在溢出的情况

(2) 注意不同数据类型间的转换,包括隐式转换

  • 不要把长整型数据 (long) 赋值给整型 (int)

在 32 位系统没有问题(long 和 int 都是 4 字节); 在 64 位系统,long 8 字节,int 4 字节,将 long 值赋予 int 将导致数据丢失

  • 不要将指针类型 (pointer) 赋值给整型 (int) 在 32 位系统没有问题(Pointer 和 int 都是4字节); 在 64 位系统 Pointer 8字 节,int 4 字节,将 Pointer 值赋予 int 将导致地址数据丢失
  • 注意隐式转换:
NSArray *items = @[@1, @2, @3];
for (int i = -1; i < items.count; i++) {
    NSLog(@"%d", i);
}

数组的count是NSUInteger类型的,-1与其比较时隐式转换成NSUInteger,变成了一个很大的数字。 因此,老式for循环建议写成:

for (NSUInteger index = 0; index < items.count; index++) {

}

(3) 注意和数位相关的数值计算

比如掩码技术,如果使用一个 long 类型的掩码,转到 64 位系统后高位都是 0,计算出来的结果可能不符合预期。还有无符号整数和有符号整数的混用等也存在问题。

(4) 注意对齐方式带来的变化,多使用 sizeof 帮助计算

如果在 32 位系统上定义一个结构包含两个 long 类型,第二个 long 数值的偏移地址是 4,可以通过结构地址 +4 的方式获取,但是在 64 位系统第二个 long 数值的偏移地址是 8。

(5) NSLog、[NSString stringWithFormat:] 的格式

NSInteger aInt = 1804809223;
///> 下面的代码在64Bit会有编译器警告提示需强转long,但在32Bit无警告
NSLog(@"%i", aInt);
    
///> 最好的解决方法:尽量使用NSNumber,轻松适配32&64
NSLog(@"Number is %@", @(aInt));
///> 另外一种解决方法:根据警告强转
NSLog(@"Number is %ld", (long)aInt);

1.3.2 BOOL 值的编译差异

在 iOS 12.4 > usr/include > objc > objc.h 中:

#   if TARGET_OS_OSX || 0 || (TARGET_OS_IOS && !__LP64__ && !__ARM_ARCH_7K)
#      define OBJC_BOOL_IS_BOOL 0
#   else
#      define OBJC_BOOL_IS_BOOL 1
#   endif

#if OBJC_BOOL_IS_BOOL
    typedef bool BOOL;
#else
#   define OBJC_BOOL_IS_CHAR 1
    typedef signed char BOOL; 
#endif

由代码可知: 在 64 位 iOS、iWatch上,BOOL 是 bool 类型(非 0 即真) 在MacOS、32 位 iOS 上,BOOL 是 signed char 类型(1 个字节, -128 ~ 127)

开发Tips

  • NSNumber/NSString等类型,对象不能强转BOOL,要通过boolValue(32bit和64bit都会出问题)
NSMutableDictionary *testDict = [NSMutableDictionary dictionary];
[testDict setObject:[NSNumber numberWithBool:NO] forKey:@"key"];
BOOL testValue = [testDict objectForKey:@"key"];
if (testValue) {
    NSLog(@"[NSNumber numberWithBool:NO]判断为真"); ///< 32bit和64bit都会走到这行代码中,得出错误结果
} else {
    NSLog(@"[NSNumber numberWithBool:NO]判断为假");
}

testValue = [[testDict objectForKey:@"key"] boolValue];
if (testValue) {
    NSLog(@"[NSNumber numberWithBool:NO]判断为真");
} else {
    NSLog(@"[NSNumber numberWithBool:NO]判断为假"); ///< 使用正确的方式后,才能走到这行代码,得到正确结果
}

如代码示例,第一个testValue没使用boolValue,判断为真;第二个testValue是正确用法,使用了booValue,判断为假。

原因:NSNumber强转BOOL,会取指针的低8位(1个字节),非全0即判真,因此得出的结果是不正确的。

同理,也不能用NSNumber来设置BOOL属性:

[view performSelector:@selector(setHidden:) withObject:@(NO)];

这样的代码是不生效的,因为NSNumber并没有转换成正确的BOOL值,建议直接使用setHidden

  • 数字不能直接用来做BOOL判断(32bit会出问题)
BOOL a = 200 + 56;
if (a) {
    NSLog(@"BOOL a = 256判断为真"); ///< 64bit会走到这行代码
} else {
    NSLog(@"BOOL a = 256判断为假"); ///< 32bit会走到这行代码
}

32bit下编译器才提示会被转为0,并且判断为假,原因是signed char取低8位(一字节),全为0则判假;

64bit下bool非0即1,指针地址非0,因此判真

1.3.3 指令集的编译差异

指令集

  • ARM 处理器的指令集(手机)
    • Armv6
    • Armv7 / Armv7s(真机 32 位处理器)
    • Arm64:iphone5s 以上(真机 64 位处理器)
  • Mac 处理器的指令集(电脑)
    • i386:针对 Intel 通用微处理器 32 位处理器(模拟器 32 位处理器)
    • x86_64:针对 x86架构的 64 位处理器(模拟器 64 位处理器)

Xcode Build Setting 中指令集相关选项释义
(1) Architectures:工程编译后支持的指令集类型。
支持的指令集越多,指令集代码的数据包越多,对应生成二进制包就越大,也就是 ipa 包会变大。
(2) Valid Architectures:限制可能被支持的指令集的范围
编译出哪种指令集的包,将由 Architectures 与 Valid Architectures 的交集来确定
(3) Build Active Architecture Only:指定是否只对当前连接设备所支持的指令集编译
当设为 YES,只对支持的指令集版本编译,编译速度更快;当设为 NO,会编译所有的版本
一般 debug 时候可选为 YES,release 时为 NO 以适应不同设备。

开发Tips

  • 如果在代码中嵌入了汇编代码,需要参考 64 位系统的指令集(Arm64)重写汇编代码。

1.3.4 64位编译时的容错优化

解决:避免依赖于 64bit 的编译优化 在 32bit 真机上 NSLog 中传入 NULL 会 crash

二、iOS10 -> iOS13 适配一览

判断版本: if (@available(iOS 11.0, *))

2.1 iOS 13

参考:WWDC 2019 - Videos - Apple Developer

2.1.1 DarkMode

Apps on iOS 13 are expected to support dark mode Use system colors and materials Create your own dynamic colors and images Leverage flexible infrastructure
审核会关注是否适配黑夜模式

  • 适配黑夜模式:
if (@available(iOS 13.0, *)) {
	if (darkMode) {
        [UIApplication sharedApplication].keyWindow.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
	} else {
        [UIApplication sharedApplication].keyWindow.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
    }
}
  • 在VC关闭/打开DarkMode:
if (@available(iOS 13.0, *)) {
	self.view.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; // Disable dark mode
	self.view.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; // Enable dark mode
}
  • 全局关闭: 在 Info.plist 设置 UIUserInterfaceStyle 属性为 Light;
  • 内嵌的 WebView 需要手动修改 css 样式才能适配

2.1.2 模态弹出默认交互改变

iOS 13的 presentViewController 默认有视差效果,模态出来的界面默认都下滑返回。 新效果动图

原因是 iOS13 中 modalPresentationStyle 的默认值改为 UIModalPresentationAutomatic,iOS13 之前默认是 UIModalPresentationFullScreen

如果想改回原来的动效需要手动加上这行代码:

self.modalPresentationStyle = UIModalPresentationFullScreen;

2.1.3 私有KVC使用会crash

在 iOS13 中运行以下代码会crash:

[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[_textField setValue:[UIFont systemFontOfSize:14] forKeyPath:@"_placeholderLabel.font"];	

原因:私有API内部属性有检索校验,一旦命中立刻crash

解决方式:

_textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"姓名" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:14],NSForegroundColorAttributeName:[UIColor redColor]}];

2.1.4 App启动过程中,部分View会无法实时获取到frame

可能是为了优化启动速度,App 启动过程中,部分View可能无法实时获取到正确的frame

只有等执行完 UIViewController 的 viewDidAppear 方法以后,才能获取到正确的值,在 viewDidLoad 等地方 frame Size 为 0

2.1.5 DeviceToken格式变化

#include <arpa/inet.h>
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    if (![deviceToken isKindOfClass:[NSData class]]) return;
    const unsigned *tokenBytes = (const unsigned *)[deviceToken bytes];
    NSString *hexToken = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x",
                          ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
                          ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
                          ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
    NSLog(@"deviceToken:%@",hexToken);
}

2.1.6 UIWebView的废弃

UIWebView - UIKit | Apple Developer Documentation

iOS13 UIWebView Support

2.2 iOS 11

2.2.1 UIScrollView: automaticallyAdjustsScrollViewInsets -> contentInsetAdjustmentBehavior

iOS 11 废弃了 UIViewController 的 automaticallyAdjustsScrollViewInsets 属性,新增了 contentInsetAdjustmentBehavior 属性,当超出安全区域时系统会自动调整 SafeAreaInsets,进而影响 adjustedContentInset 在iOS11中 adjustedContentInset 决定 tableView 内容与边缘距离,所以需要设置 UIScrollView 的 contentInsetAdjustmentBehavior 属性为 Never,避免不必要的自动调整。

if (@available(iOS 11.0, *)) {
    // 作用于指定的UIScrollView
    self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    // 作用于所有的UIScrollView
    UIScrollView.appearance.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
else {
    self.automaticallyAdjustsScrollViewInsets = NO;
}

2.2.2 UITableView: Self-Sizing

iOS11 开始 UITableView 开启了自动估算行高(Self-Sizing),Headers、footers、cells 的 estimated 高度默认值(estimatedRowHeightestimatedSectionHeaderHeightestimatedSectionFooterHeight)都从 iOS11 之前的 0 改变为 UITableViewAutomaticDimension 如果不实现-tableView: viewForFooterInSection:-tableView: viewForHeaderInSection:,那么三个高度估算属性的改变会导致高度计算不对,产生空白。 解决方法是实现对应方法或把这三个属性设为 0:

self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;

TableView 和 SafeArea(安全区) 有以下几点需要注意:

  • SeparatorInset 自动关联 SafeAreaInsets,因此默认情况下,表视图的内容会避免其根 VC 在安全区域的插入。
  • UITableviewCell 和 UITableViewHeaderFooterView的 contentview 在安全区域内,因此应该始终在 contentview 中使用add-subviews操作。
  • 所有的 headers 和 footers 都应该使用 UITableViewHeaderFooterView,包括 table headers 和 footers、section headers 和 footers。

2.2.3 授权相关

在 iOS 11 中必须支持 When In Use 授权模式(NSLocationWhenInUseUsageDescription)

为了避免开发者只提供请求 Always 授权模式这种情况,加入此限制,如果不提供 When In Use 授权模式,那么 Always 相关授权模式也无法正常使用。

如果要支持老版本,即 iOS 11 以下系统版本,那么建议在 info.plist 中配置所有的 Key(即使 NSLocationAlwaysUsageDescription在 iOS 11及以上版本不再使用):

NSLocationWhenInUseUsageDescription
NSLocationAlwaysAndWhenInUseUsageDescription
NSLocationAlwaysUsageDescription
NSLocationAlwaysAndWhenInUseUsageDescription  // 为iOS 11中新引入的一个 Key。

2.3 iOS10

2.3.1 跳转到app内的隐私数据设置页面

NSURL *urlLocation = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
if ([[UIApplication sharedApplication] canOpenURL:urlLocation]){
    if (@available(iOS 10.0, *)) {
        [[UIApplication sharedApplication] openURL:urlLocation options:@{} completionHandler:nil];
    } else {
        [[UIApplication sharedApplication] openURL:urlLocation];
    }
}

2.3.2 UIFeedbackGenerator 触觉反馈

类似于付款成功的震动反馈,只在 iOS 10 以上可用,因此需要判断if (@available(iOS 10.0, *))

译 如何使用 UIFeedbackGenerator 让应用支持 iOS 10 的触觉反馈 - iOS - 掘金


扩展阅读:

iOS开发同学的arm64汇编入门 - 刘坤的技术博客 64位和32位的寄存器和汇编的比较 - jmp esp - CSDN博客 iOS标记指针(Tagged Pointer)技术 - 掘金