对于ZIP文件,由于标准的解压方式总是从文件末尾开始读取,所以必须将整个ZIP文件下载并解压后才能访问。当用户通过网络访问ZIP文件时,耗时的下载和解压会大大降低用户体验。那么可以同时下载和解压吗?阿里文娱科技余源将介绍ZIP流式解压的原理和技术实现路径。在Web上打开ZIP文件需要多少步?下载、解压缩并获取所有文件。面对一个ZIP,能不能“边下载边玩”和“点播下载”?今年6月,优酷绘本技术团队研发出一种全新的解压方式——ZIP流式解压技术,并成功应用在优酷绘本二次开放项目中,其中30M+绘本平均加载时间仅为0.91s,加载时间较传统解压方式减少88.3%,线性提升用户阅读体验。实际对比结果如下:优化前和优化后,本文将介绍ZIP流式解压的原理和技术实现路径,希望能给大家带来启发,将更多的ZIP流式解压技术应用到业务中。1什么是ZIPZIP是一种文件格式,它定义了如何将多个文件和数据块组织在一起形成一个完整的文件。比如我们常见的.apk、.ipa、.sketch都是ZIP文件。通常程序会这样创建一个ZIP文件:将单个文件压缩成单个文件数据块;在数据块前后添加文件描述信息;对每个待压缩的文件重复上述步骤,将所有数据拼接成一个更大的数据块;提取所有文件描述信息,生成一个“文件目录”,附加在最后一个数据块的末尾。我们把文件前面的描述信息称为LocalFileHeader,文件后面的描述信息称为DataDescriptor,压缩文件本身称为FileData,最终的文件目录称为CentralDirectory。以上所有内容放在一起就是一个标准的ZIP文件。如下图所示:ZIP文件格式标准的解压方法总是从ZIP文件的末尾开始。我们以上图中的文件数据1为例:首先找到ZIP文件末尾的CentralDirectory数据块;在文件头1中查找文件头1;从FileHeader1读取LocalFileHeader1的偏移量和FileData1的相关信息;根据偏移量找到LocalFileHeader1;读取本地文件头1;解密文件数据1(如有必要);解压文件数据1;读取数据描述符1;使用FileHeader1中保存的CRC-32校验步骤7计算的CRC-32,保证解压数据的完整性。可以发现标准解压方法的缺点。标准解压强烈依赖于最后的中央目录。当一个ZIP文件存放在cdn上时,即使我们只想访问其中一个文件,也必须下载整个ZIP并解压后才能访问。如果ZIP文件有100MB,而我们只需要访问其中一个10KB的文件,那么下载整个ZIP将是一种巨大的流量浪费。2、优酷技术方案:ZIP流解压我们初步的想法之一是能不能边下载边解压?要做到这一点,首先我们需要改变解压方式,让它不能再依赖末尾的CentralDirectory。根据ZIP文件格式标准,每个FileData头的LocaFileHeader部分除了CentralDirectory外,还包含文件的相关信息。如果LocalFileHeader包含足够的信息,我们或许可以根据LocalFileHeader解压文件数据,解压过程可以变成:从头开始,搜索LocalFileHeader1;读取本地文件头1;解密文件数据1(如有必要);解压文件数据1;读取数据描述符1;CRC32的校验和。那么LocalFileHeader中到底存储了什么?是否满足解密解压要求?理解LocalFileHeader,我们根据文档中对LocalFileHeader的描述画出二进制文件中的排列:LocalFileHeader数据结构的关键信息是:SignatureMetadatasignatureCompressMethodCompressionalgorithmCompressedSizeCompressedfilesizeUncompressedSize未压缩文件大小CRC-32文件循环冗余校验值文件名文件名元数据签名是一个MagicNumber,用来标记下一个数据是什么内容。例如LocalFileHeader的签名为0x04034b50,用char表示,即{'P','K','3','4'}。当读取到相应的数据签名时,说明后面的数据结构符合相应元数据的定义,需要使用相应的规则进行解析。CompressMethod表示使用哪种算法对数据块进行压缩,解压需要使用对应的算法。CompressedSize和UnCompressedSize可以帮助确定文件的结束地址和数据描述符的偏移量。这两个Size也是文件解密时HMAC计算的关键。有了MagicNumber作为元数据签名,我们只需要逐字节遍历匹配Number,就可以找到LocaFileHeader,而不需要依赖末尾的位置信息。而LocalFileHeader中存储的元数据足以让我们确定解压算法,计算大小,校验CRC-32。还有一个问题,解压算法支持流式解压吗?是否存在特定的上下文依赖性?通过了解压缩算法的原理[1],我们知道所有的压缩算法都支持从头开始的流式解压。下载方面,从头到尾不断的下载文件,自然配合从头解压的方法,可以初步实现解!加密ZIP文件的问题还算顺利,直到遇到加密的问题。压缩文件。加密ZIP文件的LocalFileHeader中除了签名和文件名外的关键信息是隐藏的,需要从CentralDirectory中读取。我们又一次回到了对中央目录的依赖。能不能继续做流式解压,不丢那么多关键信息?我们首先要深挖ZIP的加密方式。ZIP加密方法ZIP文件支持多种加密方法,最常见的是传统PKWARE加密和AES加密。TraditionalPKWAREEncryption是ZIP定制的基于密码的对称加密方式。每个字节的加密只与密码有关,加密前后的数据长度不变。这种上下文无关的加密方式可以实现我们需要的流解密。AES加密使用CTR模式。CTR模式对明文进行分组并生成一个计数器。使用密钥加密计数器以生成二进制字节流。利用这个字节流和明文进行异或运算进行加密。解密方法也是一样的。该方法还支持流式解密。两种常用的加密方式都支持流式解密,因此LocalFileHeader中是否存储了加解密所需的密钥信息就成为流式解密的关键。流式解密的关键信息无论是TraditionalPKWAREEncryption还是AESEncryption,在解密时都需要一些密码以外的关键信息,比如salt值,加密算法强度等。另外,在AES加密的ZIP文件中,LocalFileHeader中的CompressMethod字段被擦除,这样我们就无法知道压缩算法,也就无法解压。至此,问题集中在:LocalFileHeader中是否有足够的加密所需的信息。加密的ZIP文件,是否可以在中央目录以外的位置找到压缩方法字段。LocalFileHeader中加密相关信息ZIP格式的设计者在设计ZIP文件格式的初期就提供了文件扩展能力,一些额外的扩展数据可以存储在LocalFileHeader的ExtraField中。ZIPAES加密规范[2]告诉我们,这里存放了AES的相关信息。关键信息如下:SignatureExtraDatasignature(0x9901)AESEncryptionstrengthAESencryptionstrength(128or192or256)ActualcompressMethod真正的压缩算法ExtraData中隐藏了原始的压缩算法。那么盐值存储在哪里呢?答案存储在文件数据的开头和结尾。综上,我们找到了所有解密需要的关键信息,探索了整个流式解密解压的所有技术点。剩下的就是按照原理去实现,打磨细节。三小结说了这么多,流式解压有什么价值呢?由于流式解压实现了边下载边解压,整个操作的时长由下载+解压变为约等于纯下载的时长,直接抹去了解压的耗时。在39.1MB的ZIP包下载解压测试中,耗时从9.08秒减少到4.17秒,快了近100%!同时,无需等待整个ZIP下载解压完毕,而是解压一小部分数据,直接显示UI。在用户端,看起来像是瞬间解压完成。因此,流式解压可以应用于很多时间敏感的操作,也可以用于基于ZIP文件优化相关业务。比如基于ZIP的全局换肤加速、基于ZIP的网页资源缓存加载加速等。前言中的优酷绘本二次开就是基于该技术的。参考[1]https://houbb.github.io/2018/11/09/althgorim-compress-althgorim-12-zip-02[2]AES加密资料:加密规范AE-1和AE-2https://www.winzip.com/win/en/aes_info.html[3]ZIP文件格式规范https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.2.1.TXT[4]开发人员的AES编码技巧https://www.winzip.com/win/cn/aes_tips.html【本文为《阿里巴巴官方技术》专栏作者原创稿件,转载请联系原作者】点此查看该作者更多好文
