问题:TestFlight 包获取不到资源图片!!!
关键字:
Xcode13、manageAppVersionAndBuildNumber、MonkeyDev
问题回顾
客户端发布版本时候,上传到 TestFlight 包后,在做版本回归测试时候,发现个别页面的图标ICON不展示。
问题排查分析
项目是模块化的,出问题界面是单独一个Pod库。另一项目,用的同一个SDK,没有问题,就我们这个项目不行。
资源问题
由于在 debug、adHoc 环境下正常,唯独 TestFfight 包有问题,初步定位是否打包出了问题,是 ipa 资源缺失,于是对 ipa 中出现异常界面对应的库 —— PayService.framework 中 bundle 资源包中的 asset.car 文件进行解包,经确认资源没有问题。既然资源没有缺失,那就是没取到,继续检查各项设置都没发现问题。
临时方案
把 PayService 中的资源文件拷贝入 mainBundle 一份,确保可以正常获取。
运行时参数
由于问题本身没有解决,于是需要继续寻找根本原因,继续排查。
// 对应图片获取方法
self.icon.image = ImageNamed(self.payment.iconName);
// 宏定义
#define ImageNamed(__named__) [UIImage imageNamed:__named__ class:[self class] filePath:[NSString stringWithFormat:@"%s",__FILE__]]
// 具体实现
+ (UIImage *)imageNamed:(NSString *)name class:(Class)aClass filePath:(NSString *)filePath language:(NSString *)language {
// 初始化缓存相关逻辑
....
....
// 取缓存逻辑
...
NSError *error = NULL;
NSRegularExpression *regex = [NSRegularExpression
regularExpressionWithPattern:@"\w+/Classes/"
options:NSRegularExpressionCaseInsensitive
error:&error];
NSRange range = [regex rangeOfFirstMatchInString:filePath options:0 range:NSMakeRange(0, [filePath length])];
if (range.location != NSNotFound && !error) {
NSString *path = [filePath substringFromIndex:range.location + range.length];
NSString *bundleName = [path substringToIndex:[path rangeOfString:@"/"].location];
// 取bundle
NSBundle *currentBundle = [NSBundle bundleForClass:aClass];
NSString *bundlePath = [currentBundle pathForResource:bundleName ofType:@"bundle"];
NSBundle *imageBundle = [NSBundle bundleWithPath:bundlePath];
UIImage *image = [UIImage imageNamed:name inBundle:imageBundle compatibleWithTraitCollection:nil];
if (image) {
[bundlePathCache setObject:bundlePath forKey:cacheKey];
return image;
} else {
// 在主pod同名bundle下找资源
NSString *pString = [filePath substringWithRange:range];
NSString *kitName = [pString substringFromIndex:1];
kitName = [kitName substringToIndex:[kitName rangeOfString:@"/"].location];
NSString *kitBundlePath = [currentBundle pathForResource:kitName ofType:@"bundle"];
NSBundle *kitImageBundle = [NSBundle bundleWithPath:kitBundlePath];
image = [UIImage imageNamed:name inBundle:kitImageBundle compatibleWithTraitCollection:nil];
if (image) {
[bundlePathCache setObject:kitBundlePath forKey:cacheKeyWithName];
} else {
NSLog(@">>>>===图片名:%@ 文件未找到。",name);
}
return image;
}
} else {
UIImage *image = [UIImage imageNamed:name inBundle:[NSBundle bundleForClass:aClass] compatibleWithTraitCollection:nil];
if (!image) NSLog(@">>>>==== 图片名:%@ 文件未找到。",name);
return image;
}
}
以上是取图片的逻辑,没有问题,并且还有一个应用使用的同一份 SDK,没有问题。至此,可以说是毫无思路。
继续尝试:
本地 debug 环境资源获取正常,无法重现。怀疑是运行时取图函数入参出了问题,可是问题出现在 TF 包,只能通过逆向的方式来调试。正好上传TF的包也是没有壳的,借助工具 MonkeyDev 把 TF 包重签,并且hook上述关键 (UIImage *)imageNamed:(NSString *)name class:(Class)aClass filePath:(NSString *)filePath language:(NSString *)language 函数,拦截参数,确认是否有问题。 上图是 TF 包的入参,与正常调试的入参完全一致。后详细对比正常包和TF包,发现资源一致,info.plist 稍有出入,TF包的版本号和build版本都被修改和主工程一致,尝试修改 bundle version 字段和 bundle 包里一致,发现可以解决问题,但依旧是没有依据性。
面向 Google 搜索解决
问题至此,毫无进展,只能求助 google, 恰好有位同志也遇到了这个问题(点击这里)。帖子里遇到同样问题,所有情况正常,TF 包资源问题,提出了两种解决方案。
尝试方案
方案一:修改TF导包 manageAppVersionAndBuildNumber 配置
Xcode13 新增打包选项 manageAppVersionAndBuildNumber,会在上传 TF 包时候修改版本号,默认是开,由于我们打包是用Jeakens。所以通过修改脚本,在导出配置文件 exportOptions.plist 中新增 manageAppVersionAndBuildNumber 字段并设置为 false。后来经过 MonkeyDev 和 上传 TestFight 验证通过。
方案二: 修改 PayService 中 bundle 资源包文件名。
此方案经测试也可以解决此问题。
本质原因分析:
方案一: PayService 中的资源文件拷贝入 mainBundle 一份,确保可以正常获取。
这个方案之所以可以保证获取到资源图片,是因为加载图片资源的方法:
// 获取bundle
NSBundle *currentBundle = [NSBundle bundleForClass:aClass];
NSString *bundlePath = [currentBundle pathForResource:bundleName ofType:@"bundle"];
NSBundle *imageBundle = [NSBundle bundleWithPath:bundlePath];
UIImage *image = [UIImage imageNamed:name inBundle:imageBundle compatibleWithTraitCollection:nil];
这里获取到的imageBundle字段为nil,而图片加载方法
+ (nullable UIImage *)imageNamed:(NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection API_AVAILABLE(ios(8.0))
如果传入的inBundle参数为nil默认从mainbundle加载,所以该方案刚好能解决。
方案二: 修改TF包 manageAppVersionAndBuildNumber 配置
Test Flight上的包之所以不显示图片,是因为Test Flight上的包的构建版本build number必须大于1,而且是Xcode 13打包,默认勾选了manageAppVersionAndBuildNumber配置,将可执行文件的build number同步修改了相关framework 的plist里面的bundle version,而framework内部的图片资源bundle的plist的bundle version并没有改变仍然为1导致的结果。
Payservice 库下面两个target,一个主target(Payservice),一个资源target(Payservice-Payservice)
从PayService Pod库,我们可以看到PayService和PayService-PayService 两个 target 的Bundle Identifier都是 org.cocoapods.PayService 也就是说这两个是归类到同一个应用里面的。 我们从IPA包里面的PayService.framework看到,PayService 的info.plist放在外层,而PayService-PayService的info.plist则放到PayService.bundle里面.
如果PayService的build为2,而PayService-PayService的build为1, 这里进行图片资源查找的时候,会有一个向上查找的原则,首先依据PayService的build号跟PayService-PayService的build号进行对比,如果PayService-PayService的build号,大于或者等于PayService的build,则认为PayService-PayService里面的图片资源是最新的,会去进行查找,如果小于PayService的build,则会认为PayService-PayService 过期,不进行查找。
这也就是为什么,当打包的时候,勾选了manageAppVersionAndBuildNumber选项,而此时项目的构建号大于1,这就是加载不出图片的原因。
方案三: 修改 PayService 中 bundle 资源包文件名,不同于 PayService
我们将
PayService中的bundle资源包名称改为PayServiceBundle。 查看对应库包我们可以看到PayService的Bundle Identifier是org.cocoapods.PayService,而PayService-PayService 的Bundle Identifier是 org.cocoapods.PayServiceBundle 也就是说这两个是不同的应用里面的,所以两者的build是独立的,不会互相影响。
参考工具文献
MonkeyDev
作者: Alone_Monkey,《iOS应用逆向与安全》作者。 简介:逆向小工具全家桶,除了砸壳,方便好使,功能强大,直接配置在 Xcode 中,对非越狱机器友好,甚至还维护了逆向脚本的 spec。 缺点: 很久没更新,安装起来会有很多问题,善用搜索引擎,或者爬 issues, 都有答案。
Xcode13 小坑
Manager Version and Build Number 选项自动修改 App 版本号是个很 nice 的功能,但是改变所有内嵌 framework 的 info.plist ,直接改变了 SDK 的版本信息,过于暴力,而放在 bundle 资源包里的 info.plist里版本号又忽略了,实在是不理解...
原因总结
1、同一个Pod库,为啥就这个项目出问题 ———— 是因为我们打了几次TF包上传验证,Build number也手动增加,Xcode13打包也自动增加了,导致pod库和资源文件两个buildnum 对不上。
2、Xcode 13 新增Manager Version and Build Number默认选项,太过于暴力而不全面,不知道算不算bug。
完结
至此,TestFlight包不显示图片问题分析并解决了。