iOS suggestedFilename 中文文件名乱码问题
日常吐槽:
App 中有个下载附件的功能,预览附件的时候会显示文件名字。但有个人艾特我说,安卓的没问题,IOS 的不行,文件名显示乱码,就问你尴不尴尬?……
通常有人提 Bug 咱都虚心接受,但只要说了 安卓没问题 ,血压立马上升一格。有事儿说事儿,别提安卓……
看到写
IOS
而不是iOS
的人,血压又上升一格……想起来苹果开发最火的那几年,我接到过一个 HR 的电话,”请问您是做 ISO 开发的吗?” ,我当时心想这个公司不能去呀,HR 都这么不严谨,给我干 HR 好不好,拿来吧你……
suggestedFilename
1 | NSString *filename = task.response.suggestedFilename; |
说回正题,下载附件的代码中我们是通过 NSURLResponse 的 suggestedFilename
获取服务端建议的一个文件名,但是得到的却是类似这样的乱码 é��åº�å¸�工伤æ�¥é��ä½�é�¢ä¼�é£�è¡¥å�©è´¹ã��交é��é£�å®¿è´¹å®¡æ ¸ç»�ç®�表.xls
,满脸问号是不是??????
我们知道 suggestedFilename
来自 HTTP 响应的头部信息,只是苹果默认帮我们解析成了一个 NSURLResponse
对象, 乱码问题往往是字符的编码格式没有对应上,或者苹果并不知道应该按照哪个编码规则解析,把问题抛给了开发者自己处理。
那奇怪的是,有的附件正常,有的附件就凌乱了。自然我们就想到比对一下两者 Header 的区别,是骡子是马拉出来溜溜~~
下面是截取的几个与 Content 有关的信息:
- 附件示例:测试素材 - 副本.docx 。文件名显示乱码。
1 | <NSHTTPURLResponse: 0x600003a44fa0> { URL: https://wwww.www.www } { Status Code: 201, Headers { |
- 附件示例:上海隐私条款.docx。文件名显示正常。
1 | <NSHTTPURLResponse: 0x600003d919a0> { URL: https://wwww.www.www } { Status Code: 200, Headers { |
Content-Disposition
和 Content-Type
有点区别。之前我总是误以为 Content-Type
中的 charset=UTF-8
除了指定文件的编码格式,文件名也是由这个指定的。大错特错了~~
第一次关注 Content-Disposition
。所以它到底是何方神圣呢?
Content-Disposition
在常规的 HTTP 应答中,
Content-Disposition
属于响应头的一个属性,用于指示服务器响应给我们的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。
语法
作为主体的响应标题
HTTP 上下文中的第一个参数是 inline
(默认值,表示它可以显示在网页内,或作为网页)或 attachment
(表示它应该下载;大多数浏览器呈现“另存为”对话框,预先填入filename
,如果参数的值存在的话。
1 | Content-Disposition: inline |
作为多部分主体的 header
HTTP 上下文中的第一个参数总是form-data
; 其他参数不区分大小写,并且有参数,在'='
符号后面使用带引号的字符串语法。多个参数用分号(';'
)分隔。
1 | Content-Disposition: form-data |
指令
name
后面是一个表单字段名的字符串,每一个字段名会对应一个子部分。在同一个字段名对应多个文件的情况下(例如,带有
multiple
属性的<input type=file>
元素),则多个子部分共用同一个字段名。如果 name 参数的值为'_charset_'
,意味着这个子部分表示的不是一个 HTML 字段,而是在未明确指定字符集信息的情况下各部分使用的默认字符集。filename
后面是要传送的文件的初始名称的字符串。这个参数总是可选的,而且不能盲目使用:路径信息必须舍掉,同时要进行一定的转换以符合服务器文件系统规则。这个参数主要用来提供展示性信息。当与
Content-Disposition: attachment
一同使用的时候,它被用作”保存为”对话框中呈现给用户的默认文件名。filename *
filename
和filename *
两个参数的唯一区别在于,filename *
采用了RFC 5987 中规定的编码方式。filename
和filename *
同时出现的时候,应该优先采用filename *
,假如二者都支持的话。1
filename*=UTF-8''testfile.docx
PS: 参考资料
回归到问题
回归到我们前面的问题,一个指定的 filename
,一个指定的 filename *
,有问题吗?没有问题。前面说了,都是标准的。只不过后者告诉了我们文件名使用的编码,而且还是普遍的系统和浏览器都支持的 UTF-8。前者既没有指定编码,也不是 UTF-8。我们能骂后台吗?不能,但我还是在心里骂了一句。
1 | //乱码 |
与上面两种形式对应,解决办法也有两种:
- 服务端把
Content-Disposition
中的文件名编码加上。 - iOS 自己单独解析一下
suggestedFilename
。
我运气好,对接的业务方在服务端改了,态度还不错。那如果对方因客观原因改不了怎么办呢?采用方法2,但这个时候问题又来了,我们怎么知道用哪种编码来解析呢?
说到编码,日常我也只是用 NSUTF8StringEncoding
比较多。UTF-8 有点类似于 Haffman 编码,它将 Unicode 编码为 00000000-0000007F 的字符,用单个字节来表示;00000080-000007FF 的字符用两个字节表示;00000800-0000FFFF 的字符用3字节表示。因为目前为止 Unicode-16 规范没有指定 FFFF 以上的字符,所以 UTF-8 最多是使用3个字节来表示一个字符。但理论上来说,UTF-8 最多需要用6字节表示一个字符? 🤓
NSStringEncoding
一组常量,NSString 可能用到的字符串编码。
Constants
NSStringEncoding | Notes |
---|---|
NSASCIIStringEncoding | - 8 位字符内的严格 7 位 ASCII 编码;仅 ASCII 值 0…127。 |
NSNEXTSTEPStringEncoding | - 带有 NEXTSTEP 扩展的 8 位 ASCII 编码。 |
NSJapaneseEUCStringEncoding | - 日语文本的 8 位 EUC 编码。 |
NSUTF8StringEncoding | - Unicode 字符的 8 位表示,适合由基于 ASCII 的系统传输或存储。 |
NSISOLatin1StringEncoding | - 8 位 ISO Latin 1 编码。又叫 ISO-8859-1 编码。 |
NSSymbolStringEncoding | - 8 位 Adobe 符号编码向量。 |
NSNonLossyASCIIStringEncoding | - 7 位详细 ASCII 表示所有 Unicode 字符。?? |
NSShiftJISStringEncoding | - 日语文本的 8 位 Shift-JIS 编码。 |
NSISOLatin2StringEncoding | - 8 位 ISO Latin 2 编码。又叫 ISO-8859-2 编码。 |
NSUnicodeStringEncoding | - 字符串对象的规范 Unicode 编码。是一种2字节编码,能够提供65536个字符,如”A”的Unicode编码为6500,而BigEndianUnicode编码为0065 |
NSWindowsCP1251StringEncoding | - Microsoft Windows 代码页 1251,编码 Cyrillic 字符;相当于 AdobeStandardCyrillic 字体编码。 |
NSWindowsCP1252StringEncoding | - Microsoft Windows 代码页 1252;相当于 WinLatin1。 |
NSWindowsCP1253StringEncoding | - Microsoft Windows 代码页 1253,编码希腊字符。 |
NSWindowsCP1254StringEncoding | - Microsoft Windows 代码页 1254,编码土耳其语字符。 |
NSWindowsCP1250StringEncoding | - Microsoft Windows 代码页 1250;相当于 WinLatin2。 |
NSISO2022JPStringEncoding | - 电子邮件的 ISO 2022 日语编码。GB 2312 遵从于 ISO 2022。 |
NSMacOSRomanStringEncoding | - 经典的 Macintosh 罗马编码。主要用于编码 Classic Mac OS 上的文字。 |
NSUTF16StringEncoding | - 16 位 UTF 编码。同 NSUnicodeStringEncoding。 |
NSUTF16BigEndianStringEncoding | - 指定字节序的 UTF-16 编码。字节序为大尾,也叫大端。 |
NSUTF16LittleEndianStringEncoding | - 指定字节序的 UTF-16 编码。字节序为小尾,也叫小端。 |
NSUTF32StringEncoding | - 32 位 UTF 编码。 |
NSUTF32BigEndianStringEncoding | - 指定字节序的 UTF-32 编码。字节序为大尾,也叫大端。 |
NSUTF32LittleEndianStringEncoding | - 指定字节序的 UTF-32 编码。字节序为小尾,也叫小端。 |
字节序
对于整型、长整型等数据类型,Big-endian 认为第一个字节是最高位字节,即按照从低地址到高地址的顺序存放数据的高位字节到低位字节,称为大端、大尾;而 Little-endian 则相反,它认为第一个字节是最低位字节,按照从低地址到高地址的顺序存放据的低位字节到高位字节,称为小端、小尾。
例如,假设从内存地址 0x0000 开始有以下数据:
0x0000 | 0x0001 | 0x0002 | 0x0003 |
---|---|---|---|
0x12 | 0x34 | 0xab | 0xcd |
如果我们去读取一个地址为 0x0000 的四个字节变量,若字节序为 Big-endian,则读出结果为 0x1234abcd;若字节序为 Little-endian,则读出结果为 0xcdab3412。
如果我们将 0x1234abcd 写入到以 0x0000 开始的内存中,则 Little-endian 和 Big-endian 模式的存放结果如下:
地址 | 0x0000 | 0x0001 | 0x0002 | 0x0003 |
---|---|---|---|---|
Big-endian | 0x12 | 0x34 | 0xab | 0xcd |
Little-endian | 0xcd | 0xab | 0x34 | 0x12 |
端 (endian) 的起源
“endian”一词来源于十八世纪爱尔兰作家乔纳森·斯威夫特的小说《格列佛游记》。小说中,小人国为水煮蛋该从大的一端(Big-End)剥开还是小的一端(Little-End)剥开而争论,争论的双方分别被称为“大端派”和“小端派”。以下是1726年关于大小端之争历史的描述:
我下面要告诉你的是,Lilliput和Blefuscu这两大强国在过去36个月里一直在苦战。战争开始是由于以下的原因:我们大家都认为,吃鸡蛋前,原始的方法是打破鸡蛋较大的一端,可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个手指弄破了。因此他的父亲,当时的皇帝,就下了一道敕令,命令全体臣民吃鸡蛋时打破鸡蛋较小的一端,违令者重罚。老百姓们对这项命令极其反感。历史告诉我们,由此曾经发生过6次叛乱,其中一个皇帝送了命,另一个丢了王位。这些叛乱大多都是由Blefuscu的国王大臣们煽动起来的。叛乱平息后,流亡的人总是逃到那个帝国去寻求避难。据估计,先后几次有11000人情愿受死也不肯去打破鸡蛋较小的一端。关于这一争端,曾出版过几百本大部著作,不过大端派的书一直是受禁的,法律也规定该派任何人不得做官。”
觉得这个鸡蛋的故事很有意思,所以粘在这里了 😂。关于字节序的更多内容见 维基百科-字节序 ,很详细了。
字符集
关于字符集有些我也看不懂,百科上倒是解释的比较清楚。还有很多字符编码集:
撞大运
这么多种编码,iOS 解析的话,我们用哪一种呢?如果对接的业务少,可以和服务端约定好。如果对接的业务多,想兼容所有的”乱码”,我觉得只能撞大运了😂
示例:
1 | NSString *fileName, *str; |
在上面的示例中,如果指定的 NSStringEncoding
不匹配,str 会是 nil,所以我们可以利用 nil 来撞大运了,有毛病吗?可能有,哈哈 😂