一、背景
涂鸦公版App,相册选择模块,某些iOS系统版本,用户打开相册后会崩溃,Backtrace定位分析到是向PHAssetCollection.localizedTitle发送消息然后在 objc_msgSend +32 里 EXC_BAD_ACCESS 了。
Apple Developer Forums - PHCollection localizedTitle crash in iOS 14.2.1 有人说把 NSString Category 的 +initialize 换成 +load 就解决了,这样确实也解决了。但如若是第三方实现了 +initialize 而我们改不了呢?或者有没别的解决方法呢?
Crash 的原因是因为 NSString Category 实现了 +initialize 方法后,会导致 NSTaggedPointerString 不启用了,objc_msgSend 里处理 Tagged Pointer 时,访问了未允许的内存地址, 于是就 EXC_BAD_ACCESS 了。
二、重现
重现步骤较简单:
打开 iPhone/iPad Photos APP,创建几个名字为英文或数字的比较短(字符长度小于11)的相簿Album,比如这里创建一个名叫 'Abc' 的 Album。
写一个 NSString Category,实现 +initialize 方法 NSString+Category.h:
Code: Select all
@interface NSString (Category)
@end
NSString+Category.m:
Code: Select all
@implementation NSString (Category)
+ (void)initialize {
// empty now ...
}
@end
写个测试代码,向 PHAssetCollection.localizedTitle发送消息:
Code: Select all
for (PHAssetCollection *collection in userAlbums) {
NSString *string = collection.localizedTitle;
NSLog(@">>>>>>>>>>>> album name: %@", [string description]);
}
发生崩溃:Crash with EXC_BAD_ACCESS
三、分析
iOS 官方 Photos 库没理由返回个野指针过来吧,这个 0x87a4c650b3180a9e 指针地址64位占满,且最高位是1,像 Tagged Pointer 了,用objc4的相关api来打印来验证一下,确认是否是Tagged Pointer ?
项目里引入 objc-internal.h (Link),此头文件可从苹果开发者网站获取 objc4 源码 复制出来,注释掉两个宏定义的错误,项目也就可以编译过去了
因为 objc-internal.h 里较多的inline函数,inline函数在编译时就embed了,编译后是没有这些函数symbol的,lldb里是不能直接调用的,所以我们在ObjcUtil.h/m时Wrap了一层常用的inline函数,方便在lldb里执行
当 Crash on EXC_BAD_ACCESS 触发时:
Code: Select all
(lldb) p isTaggedPointer(string)
(bool) $0 = true
(lldb) p [ObjcUtil objcIsTaggedPointer:string] # call _objc_isTaggedPointer
(BOOL) $1 = YES
确实是 Tagged Pointer String 无疑了,事实上 objc_msgSend 里也是根据地址值小 0 来判断是否为 Tagged Pointer。
那么打印一下它的值是0x0000000006362413,对照ASCII表,十六进制63代表字母c,62代表字母b,41代表字母A,最后的3代表长度。'Abc' 这个也是之前创建Album时输入的短名字。
四、解决方式
NSString的分类中不实现 + (void)initialize, 如果要实现,将实现的逻辑放在 + (void)load方法中;
Code: Select all
@implementation NSString (Category)
+ (void)load
{
//实现逻辑放这里
}
//不重写initialize
+ (void)initialize
{
//
}
@end