在 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
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 高度默认值(estimatedRowHeight
、estimatedSectionHeaderHeight
、estimatedSectionFooterHeight
)都从 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)技术 - 掘金