快速计算大文件的MD5校验和

17

我正在创建大型视频文件的MD5校验和。我目前使用的代码是:

extension NSData {
func MD5() -> NSString {
    let digestLength = Int(CC_MD5_DIGEST_LENGTH)
    let md5Buffer = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: digestLength)

    CC_MD5(bytes, CC_LONG(length), md5Buffer)
    let output = NSMutableString(capacity: Int(CC_MD5_DIGEST_LENGTH * 2))
    for i in 0..<digestLength {
        output.appendFormat("%02x", md5Buffer[i])
    }

    return NSString(format: output)
    }
}

但这会创建一个内存缓冲区,对于大型视频文件不是理想的。在Swift中是否有一种方法可以计算读取文件流的MD5校验和,从而内存占用将最小化?

答案:

但这样会创建一个内存缓冲区,对于大型视频文件并不理想。在Swift中是否有一种方法可以计算文件流的MD5校验和,以便内存占用最小化?


请考虑使用正确的CC_MD5_InitCC_MD5_UpdateCC_MD5_Final组合。 - rmaddy
3个回答

28
您可以按块计算MD5校验和,例如在这里演示的方式。
下面是使用Swift的可能实现(现已更新为Swift 5)。
import CommonCrypto

func md5File(url: URL) -> Data? {

    let bufferSize = 1024 * 1024

    do {
        // Open file for reading:
        let file = try FileHandle(forReadingFrom: url)
        defer {
            file.closeFile()
        }

        // Create and initialize MD5 context:
        var context = CC_MD5_CTX()
        CC_MD5_Init(&context)

        // Read up to `bufferSize` bytes, until EOF is reached, and update MD5 context:
        while autoreleasepool(invoking: {
            let data = file.readData(ofLength: bufferSize)
            if data.count > 0 {
                data.withUnsafeBytes {
                    _ = CC_MD5_Update(&context, $0.baseAddress, numericCast(data.count))
                }
                return true // Continue
            } else {
                return false // End of file
            }
        }) { }

        // Compute the MD5 digest:
        var digest: [UInt8] = Array(repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
        _ = CC_MD5_Final(&digest, &context)

        return Data(digest)

    } catch {
        print("Cannot open file:", error.localizedDescription)
        return nil
    }
}

需要自动释放池来释放由file.readData()返回的内存,否则整个(可能非常大的)文件将被加载到内存中。感谢Abhi Beckert注意到这一点并提供了实现。

如果您需要摘要作为十六进制编码的字符串,则将返回类型更改为String?并替换即可。

return digest

let hexDigest = digest.map { String(format: "%02hhx", $0) }.joined()
return hexDigest

1
对于使用此代码的任何人,您都需要更新以匹配我刚刚进行的编辑,因为它将整个文件存储在当前自动释放池中,可能会消耗数十GB的内存。 - Abhi Beckert
@AbhiBeckert:确实,这会有很大的区别。感谢您的更新!我稍微修改了一下代码,以摆脱额外的退出变量,但这纯粹是个人选择的问题。 - Martin R
非常好的回答!是否有一种简单的方法可以在运行时获取进度?比如,在UI中更新用户。 - Aaron
谢谢。但是,为什么这么重要的autoreleasepool技术,在文档中从未被提及 - https://developer.apple.com/documentation/foundation/filehandle/1413916-readdata?它是否在其他地方提到过? - Cheok Yan Cheng
@CheokYanCheng:FileHandle是Swift中Foundation框架中Objective-C NSFileHandle的名称。自动释放池仅在与Cocoa API交互时才需要。对于Objective-C,可以在此处找到文档:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html。 - Martin R
如果我没有遇到你的代码片段,我会使用不带自动释放池的 FileHandle。今天早上,我尝试运行一个测试和分析循环,其中执行文件 I/O 和图像缩放操作,我发现使用/不使用 autoreleasepool 在内存使用模式上并没有太大区别。你知道决定是否使用 autoreleasepool 的思考过程应该是什么吗?- https://stackoverflow.com/questions/68826618/how-can-we-decide-whether-we-should-use-autoreleasepool - Cheok Yan Cheng

7

iOS13之后

'CC_MD5_Init'已在iOS 13.0中弃用

您可以使用CryptoKit替换此代码

import Foundation
import CryptoKit

extension URL {

    func checksumInBase64() -> String? {
        let bufferSize = 16*1024

        do {
            // Open file for reading:
            let file = try FileHandle(forReadingFrom: self)
            defer {
                file.closeFile()
            }

            // Create and initialize MD5 context:
            var md5 = CryptoKit.Insecure.MD5()
            
            // Read up to `bufferSize` bytes, until EOF is reached, and update MD5 context:
            while autoreleasepool(invoking: {
                let data = file.readData(ofLength: bufferSize)
                if data.count > 0 {
                    md5.update(data: data)
                    return true // Continue
                } else {
                    return false // End of file
                }
            }) { }

            // Compute the MD5 digest:
            let data = Data(md5.finalize())
            
            return data.base64EncodedString()
        } catch {
            error_log(error)
            
            return nil
        }
    }
}

2

基于Martin R的答案,针对SHA256哈希的解决方案:

func sha256(url: URL) -> Data? {
    do {
        let bufferSize = 1024 * 1024
        // Open file for reading:
        let file = try FileHandle(forReadingFrom: url)
        defer {
            file.closeFile()
        }

        // Create and initialize SHA256 context:
        var context = CC_SHA256_CTX()
        CC_SHA256_Init(&context)

        // Read up to `bufferSize` bytes, until EOF is reached, and update SHA256 context:
        while autoreleasepool(invoking: {
            // Read up to `bufferSize` bytes
            let data = file.readData(ofLength: bufferSize)
            if data.count > 0 {
                data.withUnsafeBytes {
                    _ = CC_SHA256_Update(&context, $0, numericCast(data.count))
                }
                // Continue
                return true
            } else {
                // End of file
                return false
            }
        }) { }

        // Compute the SHA256 digest:
        var digest = Data(count: Int(CC_SHA256_DIGEST_LENGTH))
        digest.withUnsafeMutableBytes {
            _ = CC_SHA256_Final($0, &context)
        }

        return digest
    } catch {
        print(error)
        return nil
    }
}

使用先前创建的类型为URL,名称为fileURL的实例:

if let digestData = sha256(url: fileURL) {
    let calculatedHash = digestData.map { String(format: "%02hhx", $0) }.joined()
    DDLogDebug(calculatedHash)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接