背景
自从买了 iPhone Xs Max,就一直处于等待应用适配新设备的状态。不过本次增加的新屏幕(Xs Max)与 iPhone X 的屏幕比例相似,所以即使应用没有第一时间适配 Xs Max 机型,iOS 也会把应用的窗口以 iPhone X 为基础拉伸到满屏,以便最大限度的展示应用界面。相比之前推出 X 和 Plus 系列时,用户的过度成本降低了许多。
但是在苦苦等待过程中,腾讯的 TIM 一直没有得到更新。即便是等待苹果开售 iPhone Xr(Xr 在 Xs 开售后一个月才开售,所以开发者可能会等到 Xr 开售后才一起适配 Xs Max 和 Xr),还是等待苹果修复新的 Assets 压缩算法在 iOS 9 下闪退的 bug,微信也在年底前完成了 Xs Max 的适配,并且推出了微信 7.0。然而直到作者编写此文(2019年01月26日)时,TIM 依旧没有更新,粗糙的 UI 元素让人觉得不是一个 1 万块钱手机该有的界面。处于强迫症考虑,能否在没有三方应用源码的情况下让三方应用适配新的机型呢?
本文将通过以下几点的研究,来学习苹果的应用适配之道。
- 在发布新设备之后,苹果如何让没有适配新设备的应用正常运行在新的设备上
- 在发布新设备之后,开发者如何让自己的应用适配新的设备
- 在发布新设备之后,如何让三方应用适配新的设备(以 TIM 为例)
另外简单介绍一下 TIM:
TIM 是腾讯推出的一款精简版的 QQ。腾讯已推出多个版本的QQ,如:QQ、国际版、HD 版、TIM。TIM 是一个面向工作、白领的一款产品。拥有强大的文件管理,以及精简的 UI 界面。由于他使用简单,办公高效,得到极多喜欢简约风格的用户(包括作者)一致好评。
PS:本文只探讨 iPhone,不探讨 iPad。
iPad 原理如同 iPhone。
iOS 如何兼容没有适配新屏幕的应用
即使苹果在开发者协议中注明:任何时候,开发者上传到 App Store 的应用必须适配最新的设备,否则被拒(你爸爸还是你爸爸)。苹果也无法保证在开售新设备的时候,已经在 App Store 上的应用全部都已被更新过。所以每当新设备(新屏幕)发布时,苹果都要在新的 iOS 系统中让新的设备兼容旧的应用。
先回忆一下苹果发布的几款机型(主要探讨屏幕),以及用户过渡期的处理方式(兼容已经在 App Store 上还未适配新设备的应用)。
从 2007 年第一代 iPhone 发布一直到 2018 年的 iPhone Xs Max,苹果一共发布了 8 款 iPhone 屏幕,它们分别如下(以搭载该屏幕的第一款设备命名,以发布时间及市场定位排序):
Name | Pixel | Size | PPI | Scale | 过渡期的自适配方式 |
---|---|---|---|---|---|
iPhone | 320 x 480 | 320 x 480 | 326 | 1 | iPhone 祖宗,无需适配 |
iPhone 4 | 640 x 960 | 320 x 480 | 326 | 2 | 宽度拉伸,高度拉伸,无黑边 |
iPhone 5 | 640 x 1136 | 320 x 568 | 326 | 2 | 宽度不变,垂直居中,上下黑边 |
iPhone 6 | 750 x 1334 | 375 x 667 | 326 | 2 | 宽度拉伸,垂直居中,上下黑边 |
iPhone 6 Plus | 1242 x 2208 | 414 x 736 | 401 | 3 | 宽度拉伸,垂直居中,上下黑边 |
iPhone X | 1125 x 2436 | 375 x 812 | 458 | 3 | 宽度拉伸,垂直居中,上下黑边 |
iPhone Xr | 1242 x 2688 | 414 x 896 | 326 | 2 | 宽度拉伸,高度拉伸,无黑边 |
iPhone Xs Max | 828 x 1792 | 414 x 896 | 458 | 3 | 宽度拉伸,高度拉伸,无黑边 |
如果新的设备的长宽比和旧的设备相同(Size 相同,或者 Size.width / Size.height 近似相同),如:iPhone 到 iPhone 4,iPhone X 到 iPhone Xs Max,就单纯的按照旧的设备来渲染(UIScreen.mainScreen.bounds
还是旧的设备的值),并且等比例拉伸到全屏,所以会变得有的线条有点粗犷 = =。
如果新的设备的长款比和旧的设备不同,如:iPhone 4 到 iPhone 5(脸被拉长了),iPhone 5 到 iPhone 6 和 Plus(不仅脸长了,还胖了)。就以宽度为基准尽可能的等比例放大,长度不够的黑边凑,同时整体垂直居中(这样就导致上下留有黑边)。
以上两种处理虽然最终都不能完美解决问题,但是至少保证旧的应用在渲染 UI 的时候不会因屏幕分辨率而导致布局错乱。
为什么有 Autolayout 还不能完美自适应呢?
因为你 Autolayout 再怎么牛逼,也躲不过无刘海到有刘海的变化。。。
应用开发者如何适配新的屏幕
对于使用 UIKit.framework 提供的几个常用接口来开发 UI 的应用,在适配新屏幕时就非常的简便,一般只有以下三步骤:
- 下载新的 Xcode
- 加入新屏幕对应大小的启动图
- 重新构建应用
对于大量自定义 UI 框架的应用(如自己重写了 UINavigationController
),需要在代码中加入新设备的判断(此文不考虑这种情况)。
在这里必须吐槽一下,每当有新屏幕出现,网络上就有大量的博客:《教你如何适配新的 iPhone》,《适配新的 iPhone 看我就够了》,《你真的适配新的 iPhone 了吗》。。。
甚至有的公司为了适配新的 iPhone 花了至少一个工作日的时间。如果应用使用苹果标准 API 构建界面,真的只需要添加一个启动图然后重新打包即可解决问题。
我们对每一个步骤进行一次分析:
1. 使用最新的 Xcode
使用最新的 Xcode 无疑是正确的。最新的 Xcode 可以给我们带来两个功能:
- 新的 iOS SDK
- 新屏幕的启动图坑位
这两个功能在适配新的设备时给了很大的帮助。新的 SDK 能够让应用在新的 iOS 系统上完美运行。新的启动图能够让应用获取到正确的屏幕大小。
2. 加入屏幕对应大小的启动图
如果某个应用使用纯代码开发(没有使用 Storyboard
和 xib
),那么这个应用的启动代码应该如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchedWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.rootViewController = [[ViewController alloc] init];
[self.window makeKeyAndVisible];
}
适配新的屏幕,本质就是这个 self.window
对象的大小等于新的屏幕,然后子元素根据新的窗口大小来创建布局约束。
代码中动态使用 [UIScreen mainScreen].bounds
的大小来创建 window
对象,似乎是完美适配了任何大小的屏幕。但实际上,如果没有添加新的设备的启动图,UIScreen
获取到的屏幕大小并非真实设备的屏幕大小,而是 iOS 兼容模式所用的旧设备的屏幕大小。这是 iOS 兼容模式的一种优化,防止新的屏幕分辨率的出现导致应用 UI 控件布局错乱(宁愿等比例拉伸)。
3. 重新构建应用
不用多说了。
猜测——判断应用是否需要在兼容模式
首先有一个客观事实:
当新的设备发布后,如果使用旧的 Xcode 打包的 ipa 一样无法适配新机
如果使用新的 Xcode 并且没有加入新的启动图的 ipa 一样无法适配新机。因此是否适配取决于 Xcode 版本号以及是否包含启动图,与打包的时间无关。
之前说明,开发者适配新设备的方法是:使用最新的 Xcode,加入新的设备的启动图后重新打包应用。所以 iOS 只需判断这个应用是否经过这样方式处理,就可以知道这个应用是否需要兼容模式下运行。
1. 判断是否包含启动图
判断是否是启动图是一件很简单的事情,只需要判断当前屏幕分辨率对应的图片是否存在在 .app 文件夹中即可,这个判断会经历从 Info.plist 读取启动图配置、查找启动图的几个阶段,此文不在阐述。
总之肯定会多一个 LaunchImage-xxx-xxx@nx.png 图片文件,并且分辨率和别的启动图不同。
2. 判断是否是新的 Xcode 打包
如何判断这个 ipa 是用什么版本的 Xcode 打的包呢?
在一个 ipa 包中有几个特殊文件:
- Binary
- Info.plist
- _CodeSignature
clang 中不包含有 Xcode 版本信息,同时在编译的脚本中也没有将 Xcode 的版本信息作为参数传递给 clang,所以二进制(Binary)文件中不含有有关 Xcode 信息的数据。
_CodeSignature 是签名产物,签名时也没有 Xcode 信息,所以也不可能有 Xcode 信息的数据。
Info.plist 是 App 的 Manifest 文件,包含 App 的各种信息。这是我们着重考虑的方向。
分析 Info.plist 文件
打开一个自己做的 app 的 Info.plist 文件,可以查看到一下内容:
其中红色标记的就是作者查找出来带有编译环境的信息:
- DTSDKName 开发所用的 SDK 名称
- DTXcode 开发所用的 Xcode 版本号(Xcode 10.1.0)
- DTSDKBuild 开发所用的 SDK Build 号(SDK 内部版本号)
- BuildMachineOSBuild 开发所用的机器 Build 号(Mac 的内部版本号)
- DTPlatformVersion 开发所用的平台版本号
- DTXcodeBuild 开发所用的 Xcode Build 号(Xcode 内部版本号)
- DTPlatformBuild 开发所用的平台 Build 号(平台的内部版本号)
已经很完美了,Xcode、MacOS、SDK 版本号全都在。
同时我们也可以看见,如果应用使用的是启动图,而不是 Storyboard
,在 Info.plist 文件中会包含一项 UILaunchImages
:
由此我们可以猜测苹果的兼容方案。
苹果的兼容方案
当 iOS 要启动一个 App 时,会先读取 Info.plist 对应编译环境版本号,判断是否是新版 Xcode 打的包。如果是新版本的 Xcode,再判断是否包含启动图。如果包含启动图,则说明该 ipa 适配了新机,无需兼容。反之,使用兼容模式运行此 app。
验证——将三方应用适配新机(以 TIM 为例)
鉴于以上的分析,只需要将 Info.plist 中的对应版本号提高,并且加入新的启动图即可实现三方应用适配新机。
1. 砸壳
由于所有在 App Store 上的应用均被苹果加壳,并且我们将来需要修改 Info.plist 文件,所以必须要重签名。因此我们所修改的 TIM 需要砸壳处理。
砸壳方式有很多种,此文不阐述。
参考:Clutch、frida-ios-dump 等等。
2. 修改 Info.plist 中的版本号
由于 Info.plist 中版本号有点复杂,有部分为内部版本号(需要查询),所以我将已经适配好 Xs Max 的应用拿来做参考,进行数据覆盖。
此处使用的是 WeChat 7.0.0 的 Info.plist 作为参考依据。将上述提到的 7 个字段的值,从微信的 Info.plist 文件里提取出来,覆盖到 TIM 的 Info.plist 文件中。分别是:DTSDKName
、DTXcode
、DTSDKBuild
、BuildMachineOSBuild
、DTPlatformVersion
、DTXcodeBuild
、DTPlatformBuild
。
除版本号外,Info.plist 也包含支持设备的硬件号,字段为
UISupportedDevices
通过助手类软件可以读出 Xs Max 设备号为
iPhone11,6
,将其加入到UISupportedDevices
字段中,防止安装时报错(不支持此设备)
3. 添加启动图
经过与微信的比较,发现 Xs Max 和 Xr 的启动图分别是:
名称 | 大小 | 机型 |
---|---|---|
LaunchImage-1200-Portrait-1792h@2x.png | 828 x 1792 | Xr |
LaunchImage-1200-Portrait-2688h@3x.png | 1242 x 2688 | Xs Max |
终于祭出了自己的美工技术了。提取 X 的启动图,修改为上述两个文件,并添加到包中。
打开 Info.plist 文件,比较后在 UILaunchImages
的数组里添加两项启动图配置:
<dict>
<key>UILaunchImageOrientation</key>
<string>Portrait</string>
<key>UILaunchImageName</key>
<string>LaunchImage-1200-Portrait-2688h</string>
<key>UILaunchImageSize</key>
<string>{414, 896}</string>
<key>UILaunchImageMinimumOSVersion</key>
<string>12.0</string>
</dict>
<dict>
<key>UILaunchImageOrientation</key>
<string>Portrait</string>
<key>UILaunchImageName</key>
<string>LaunchImage-1200-Portrait-1792h</string>
<key>UILaunchImageSize</key>
<string>{414, 896}</string>
<key>UILaunchImageMinimumOSVersion</key>
<string>12.0</string>
</dict>
保存。
4. 重签名
重签名的方式有很多种,此文不再详述。
至于重签名后无法正常推送,不在此文的探讨范围,我就不详细说明了。
对比图
左侧为适配前,右侧为适配后
适配后的 UI 变得非常的细腻。舒服~~
总结
Xcode 在打包的时候讲打包环境信息(各种版本号)保存到 Info.plist 中。iOS 运行应用时,通过 Info.plist 中的版本号进行比较,来判断开发者是否适配了新设备,从而判断是否需要兼容模式运行此应用。