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

在 iOS 开发的过程当中,版本和机型适配是你们都会遇到的“坑”。若是不注意不一样版本API和编译器的差别,那么只会Bug频出,屡屡返工啦。css

参考资料总结了面对不一样位数的机器、不一样 iOS 版本时可能遇到的“坑”,在学习整理、避免本身踩坑的同时也但愿可以及时输出分享,为你们创造价值。web

欢迎你们留言讨论,一块儿进步(〃’▽’〃)!数组


目录

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 触觉反馈
复制代码

1、32 位 / 64 位设备适配

1.1 32位、64位的概念

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

相对于 32 位而言,64 位的 CPU 位宽增长一倍,使其可以处理更多更精确的数据,所以有必定加快数据处理的做用。bash

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

32 bit:iphone5 及以前架构

64 bit:iPhone5s 及以后(iPad Air以后)app

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

///* 方法一 *///
    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 位ide

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

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

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

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

开发Tipspost

(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

2、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)技术 - 掘金