iOS項(xiàng)目資源管理

APP瘦身一個(gè)重要方式是刪除冗余內(nèi)容,包括類文件,圖片等

Apple 為了在優(yōu)化 iPhone 設(shè)備讀取 png 圖片速度,將 png 轉(zhuǎn)換成 CgBI 非標(biāo)準(zhǔn)的 png 格式。這種優(yōu)化對于大多數(shù)應(yīng)用來說都是包大小的負(fù)優(yōu)化。所以簡單的壓縮(有損,無損)處理并不能達(dá)到很好的瘦身效果。

經(jīng)過測試,以下文件會被負(fù)優(yōu)化:

  • 放在根目錄下png格式的圖片。
  • 放在Asset Catalog中的png、jpg格式的圖片,其中jpg會轉(zhuǎn)成png。

放在根目錄下的jpg,bundle中的png不會被優(yōu)化

jpg和png區(qū)別
jpg有損壓縮,png為無損壓縮。jpg的圖片更小。 jpg圖像沒有透明的背景,而png圖像可以保留透明的背景

針對大圖(如大于60KB)的處理:

  • 優(yōu)先轉(zhuǎn)網(wǎng)絡(luò)下載,使用默認(rèn)圖/純色兜底
  • 不能轉(zhuǎn)下載的使用壓縮過的jpg格式圖片,放到xcode根目錄下。
  • 不能使用jpg的圖片經(jīng)過壓縮后放到 bundle 中使用。

無用類和無用方法可以通過分析Mach-O得到

RN:
主要處理內(nèi)置包大小。
深層次的RN單獨(dú)作為一個(gè)模塊,不內(nèi)置。采用用戶點(diǎn)擊時(shí)觸發(fā)下載流程,下載完后存儲再進(jìn)入的方式
iphoneX的圖片不要打進(jìn)安卓內(nèi)置包里面,

其他如 iconfont,圖片webP化

工具:CATClearProjectTool,檢查項(xiàng)目中未使用的類文件
原理:查找項(xiàng)目中所有的類文件,以及每一個(gè)類文件中的”xxx”這樣的字符串,根據(jù)OC導(dǎo)入類的規(guī)則import “xxx.h”,就是找出這里的xxx字符串,對比上面兩個(gè)結(jié)果找出未使用類文件。

工具:LSUnusedResources,檢查項(xiàng)目中未使用的圖片
原理:正則匹配(如@“.*?”)項(xiàng)目中所有使用的字符串,生成hash表。再讀取項(xiàng)目所有圖片,看是否存在于hash表中,沒有則沒有使用。

LSUnusedResources的不足
1、效率低下,我項(xiàng)目中有1405個(gè)圖片資源,13991個(gè)常量字符串,耗時(shí)約65s
2、無法查找項(xiàng)目中存在的名字/格式不同,但是內(nèi)容完全一致的圖片
3、項(xiàng)目中可能存在一個(gè)圖片,以及它的多個(gè)非常相似或者壓縮后的圖片,希望能找出來

ResourceManager

說明:
1、checkUnUsedImages,查找項(xiàng)目中沒有使用的圖片,png/jpg/webP
找出路徑下所有文件中的常量字符串,以及路徑下所有圖片,不在前者中的圖片則未使用
只支持OC語言,且沒有適配xib,結(jié)果中可能會包含OC動態(tài)生成使用的圖片,刪除時(shí)需注意排查,可以刪除圖片后Hook UIImage imageNamed方法,運(yùn)行程序跑一跑,看是否存在圖片名有值,而生成的圖片為nil的現(xiàn)象,判斷是否誤刪圖片

2、checkUnUsedClasses,查找項(xiàng)目中沒有使用的類文件
找出路徑下所有文件中的“xxx.h”,以及路徑下所有.h文件名,不在前者中的文件則未使用
只支持OC文件,結(jié)果中可能會包含OC動態(tài)生成使用的類,刪除時(shí)需注意排查

3、checkUnUsedClasses2,查找項(xiàng)目中沒有使用的類文件
找出路徑下所有.m文件名,以及文件中字符串[xxx ]中的xxx,不在后者中的文件則未使用。主要是為了找出項(xiàng)目中只import但是未使用的那種文件
注意:只支持OC語言,結(jié)果中存在較多的錯誤結(jié)果,例如基類,分類,文件名和類名不一致的等

4、checkSimilarityImages,檢測路徑下所有相似的圖片
similarity值越大,獲得的圖片相似程度越高,當(dāng)為1.0時(shí),獲取完全相同的圖片,建議取值0.9-1.0之間
查找相同圖片時(shí),對比所有圖片的md5
查找相似圖片時(shí),對比每一個(gè)像素點(diǎn)的rgba值在一定誤差范圍內(nèi)

5、getAllEmptyDirectoryPaths,檢測空文件夾

6、性能瓶頸在正則匹配所有文件中的指定字符串,通過多線程提升匹配效率

7、一個(gè)像素點(diǎn)模型包括RGBA四個(gè)成員變量,每個(gè)占用1個(gè)字節(jié)。OC模型占用16字節(jié),swift只占用4個(gè)字節(jié),內(nèi)存更小

8、 siwft對結(jié)構(gòu)體的優(yōu)化,方法靜態(tài)調(diào)用內(nèi)聯(lián)優(yōu)化等,處理數(shù)組中大量結(jié)構(gòu)體時(shí)速度更快

性能測試:
環(huán)境 iPhone x 模擬器
項(xiàng)目圖片資源數(shù)量 304
所有圖片像素點(diǎn)個(gè)數(shù) 2300萬+
所有像素點(diǎn)模型加載進(jìn)內(nèi)存消耗 OC: 750M swift: 150M
單線程處理耗時(shí): OC: 6.2s swift: 5s

不足:找出未使用的類、圖片等,刪除后再掃描又會找出一批未使用的圖片、類等。
優(yōu)化方向:記錄下被引用者是否在未使用范圍內(nèi)。例如圖片A只被B類使用,而B是個(gè)未使用的類,則A也算未使用圖片

實(shí)現(xiàn):

import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
       let basePath = "/Users/ex-zhangmaliang001/Desktop/LRU"

               checkUnUsedClasses(basePath)
//        checkUnUsedClasses2(basePath)
//        checkUnUsedImages(basePath)
//        checkSimilarityImages(basePath)
//        checkEmptyDirectorys(basePath)
    }
    
    /// 獲取項(xiàng)目中空的文件夾
    func checkEmptyDirectorys(_ basePath: String) {
        let paths = FileManager.getAllEmptyDirectoryPaths(basePath) { !$0.contains(".git") }
        if paths.count > 0 {
            print("以下是空文件")
        }
        for path in paths {
            print(path)
        }
    }
    
    /// 獲取項(xiàng)目中未使用的類文件
    func checkUnUsedClasses(_ basePath: String) {
        ResourceManager.shared.checkUnUsedClasses(basePath: basePath, pathFilter: {
            !$0.contains("Pod") && !$0.contains("高德地圖") && !$0.contains("framework")
        }) { result in
            guard let classNames = result else { return }
            print("以下類沒有使用")
            for className in classNames {
                print(className)
            }
        }
    }

  func checkUnUsedClasses2(_ basePath: String) {
        ResourceManager.shared.checkUnUsedClasses2(basePath: basePath, pathFilter: {
            !$0.contains("Pod") && !$0.contains("高德地圖") && !$0.contains("framework")
        }) { result in
            guard let classNames = result else { return }
            print("以下類沒有使用")
            for className in classNames {
                print(className)
            }
        }
    }
    
    /// 測試: 項(xiàng)目中1405個(gè)圖片資源,13991個(gè)常量字符串,耗時(shí)約4.5s
    /// LSUnusedResources 需要約65s,相差15倍, 結(jié)果一致
    func checkUnUsedImages(_ basePath: String) {
        ResourceManager.shared.checkUnUsedImages(basePath: basePath) { result in
            guard let images = result else { return }
            print("以下圖片沒有使用")
            for image in images {
                print(image)
            }
        }
    }
    
    /// 獲取項(xiàng)目中相同的圖片
    func checkSimilarityImages(_ basePath: String) {
        ResourceManager.shared.checkSimilarityImages(path: basePath, similarity: 1.0) { result in
            guard let allImageModels = result else { return }
            for imageModels in allImageModels {
                print("以下圖片相似")
                for model in imageModels {
                    print(model.path!)
                }
            }
        }
    }
    
}

import UIKit

class ResourceManager {
    private init(){}
    static let shared: ResourceManager = { ResourceManager() }()
    var imageSimilarityLevel = 1.0
    /// 默認(rèn)過濾掉.bundle文件中的圖片等資源
    var pathFilter: FileManager.PathFilter = { !$0.contains(".bundle") }
}


extension ResourceManager {

    /// 查找項(xiàng)目中沒有使用的圖片,png/jpg/webP
    /// 原理:找出路徑下所有文件中的常量字符串,以及路徑下所有圖片,不在前者中的圖片則未使用
    /// 注意:只支持OC語言,對OC動態(tài)生成的字符串會出錯,使用時(shí)需注意排查
    func checkUnUsedImages(basePath: String,
                           pathFilter: FileManager.PathFilter? = nil,
                           callback: @escaping ([String]?) -> ()) {
        if pathFilter != nil {
            self.pathFilter = pathFilter!
        }
        let imagePaths = Set(FileManager.getAllImagePaths(basePath, self.pathFilter).map { StringContainer($0) })
        
        self.findConstantStrs(basePath) { constantStrs in
            let intersection = imagePaths.intersection(constantStrs)
            let unusedImages = imagePaths.subtracting(intersection)
            callback(unusedImages.map { $0.string })
        }
    }
    
    /// 正則匹配出所有文本中的所有常量字符串 -- 主要耗時(shí)代碼,多線程
    func findConstantStrs(_ basePath: String, _ callback: @escaping (Set<StringContainer>) -> ()) {
        let filePaths = FileManager.getAllFilePaths(basePath, self.pathFilter)
        var resultStrs = Set<StringContainer>()
        
        let workingGroup = DispatchGroup()
        let workingQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
        let lock: NSLock = NSLock()
        
        for path in filePaths {
            workingGroup.enter()
            workingQueue.async {
                do {
                    let content = try String(contentsOfFile: path as String, encoding: String.Encoding.utf8)
                    let results = self.findMatchStrs(content)
                    lock.lock()
                    resultStrs = resultStrs.union(results)
                    lock.unlock()
                    workingGroup.leave()
                }catch {
                    workingGroup.leave()
                }
            }
        }
        
        workingGroup.notify(queue: workingQueue) {
            callback(resultStrs)
        }
    }
    
    /// 正則匹配
    func findMatchStrs(_ content: String) -> Set<StringContainer> {
        var result = Set<StringContainer>()
        let regex = try! NSRegularExpression(pattern: "@\"(.*?)\"")
        let matches = regex.matches(in: content, range: NSRange(content.startIndex...,in: content))
        for  match in matches {
            // 這里的偏移值2,和3取決于pattern
            let range = NSRange(location: match.range.location + 2, length: match.range.length - 3)
            let matchStr = (content as NSString).substring(with: range)
            result.insert(StringContainer(matchStr))
        }
        return result
    }
    
    /// 封裝字符串,目的是Set中比較時(shí),使 “xxx/xxx/image@2x.png” 和 “image” 相等
    struct StringContainer: Hashable {
        var string: String
        init(_ str: String) {
            self.string = str
        }
        
        static func == (lhs: StringContainer, rhs: StringContainer) -> Bool {
            var lhsStr = (lhs.string as NSString).lastPathComponent
            var rhsStr = (rhs.string as NSString).lastPathComponent
            if lhsStr.compare(rhsStr) == .orderedSame { return true }
            
            lhsStr = lhsStr.removeSuffix(suffixs: [".jpg",".png",".webP"])
            rhsStr = rhsStr.removeSuffix(suffixs: [".jpg",".png",".webP"])
            if lhsStr.compare(rhsStr) == .orderedSame { return true }
            
            lhsStr = lhsStr.removeSuffix(suffixs: [".h",".m",".mm",".pch"])
            rhsStr = rhsStr.removeSuffix(suffixs: [".h",".m",".mm",".pch"])
            if lhsStr.compare(rhsStr) == .orderedSame { return true }
            
            lhsStr = lhsStr.removeSuffix(suffixs: ["@1x","@2x","@3x"])
            rhsStr = rhsStr.removeSuffix(suffixs: ["@1x","@2x","@3x"])
            if lhsStr.compare(rhsStr) == .orderedSame { return true }
            
            return false
        }
        
        public var hashValue: Int {
            var pathComponent = (self.string as NSString).lastPathComponent
            pathComponent = pathComponent.removeSuffix(suffixs: [".jpg",".png",".webP"])
            pathComponent = pathComponent.removeSuffix(suffixs: [".h",".m",".mm",".pch"])
            pathComponent = pathComponent.removeSuffix(suffixs: ["@1x","@2x","@3x"])
            return pathComponent.hashValue
        }
    }
}

extension ResourceManager {
    
    
    /// 查找項(xiàng)目中沒有使用的類
    /// 原理:找出路徑下所有文件中的“xxx.h”,以及路徑下所有.h文件名,不在前者中的文件則未使用
    /// 找出沒有import的類, 需要注意只有+load方法的類
    /// 注意:只支持OC語言,結(jié)果中可能會包含OC動態(tài)生成使用的類,刪除時(shí)需注意排查
    func checkUnUsedClasses(basePath: String,
                            pathFilter: FileManager.PathFilter? = nil,
                            callback: @escaping ([String]?) -> ()) {
        if pathFilter != nil {
            self.pathFilter = pathFilter!
        }
        let allFilePaths = FileManager.getAllFilePaths(basePath, self.pathFilter).filter({!$0.hasSuffix(".pch")})
        let allFiles = Set(allFilePaths.map { StringContainer($0.removeSuffix(suffixs: [".h",".m"]))} )
        self.findConstantStrs2(basePath) { constantStrs in
            let intersection = allFiles.intersection(constantStrs)
            let unusedFiles = allFiles.subtracting(intersection)
            callback(unusedFiles.map({ $0.string }))
        }
    }
    
    /// 找出所有文本中的所有“xxx.h” -- 主要耗時(shí)代碼,多線程
    func findConstantStrs2(_ basePath: String, _ callback: @escaping (Set<StringContainer>) -> ()) {
        let filePaths = FileManager.getAllFilePaths(basePath, self.pathFilter)
        var resultStrs = Set<StringContainer>()
        
        let workingGroup = DispatchGroup()
        let workingQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
        let lock: NSLock = NSLock()
        
        for path in filePaths {
            workingGroup.enter()
            workingQueue.async {
                do {
                    let content = try String(contentsOfFile: path as String, encoding: String.Encoding.utf8)
                    let results = self.findMatchStrs2(content).filter({
                        var str = $0.string
                        if !str.contains(".h") {
                            return false
                        }
                        str.removeSubrange(str.range(of: ".h")!)
                        return !(path as NSString).lastPathComponent.contains(str)
                    })
                    lock.lock()
                    resultStrs = resultStrs.union(results)
                    lock.unlock()
                    workingGroup.leave()
                }catch {
                    workingGroup.leave()
                }
            }
        }
        
        workingGroup.notify(queue: workingQueue) {
            callback(resultStrs)
        }
    }
    
    /// 正則匹配
    func findMatchStrs2(_ content: String) -> Set<StringContainer> {
        var result = Set<StringContainer>()
        let regex = try! NSRegularExpression(pattern: "\"(.*?)\"")
        let matches = regex.matches(in: content, range: NSRange(content.startIndex...,in: content))
        for  match in matches {
            // 這里的偏移值1,和2取決于pattern
            let range = NSRange(location: match.range.location + 1, length: match.range.length - 2)
            let matchStr = (content as NSString).substring(with: range)
            result.insert(StringContainer(matchStr))
        }
        return result
    }
}

extension ResourceManager {
    
    /// 查找項(xiàng)目中沒有使用的類
    /// 原理:找出路徑下所有.m文件名,以及文件中字符串[xxx ]中的xxx,不在后者中的文件則未使用
    /// 主要是找出項(xiàng)目中只import但是未使用的那種文件,需要注意只有+load方法的類
    /// 注意:只支持OC語言,結(jié)果中存在較多的錯誤結(jié)果,例如基類,分類,文件名和類名不一致的等
    func checkUnUsedClasses2(basePath: String,
                             pathFilter: FileManager.PathFilter? = nil,
                             callback: @escaping ([String]?) -> ()) {
        if pathFilter != nil {
            self.pathFilter = pathFilter!
        }
        let allFilePaths = FileManager.getAllFilePaths(basePath, self.pathFilter).filter({!$0.hasSuffix(".pch") && !$0.hasSuffix(".h")})
        let allFiles = Set(allFilePaths.map { StringContainer($0.removeSuffix(suffixs: [".m"]))} )
        self.findConstantStrs5(basePath) { constantStrs in
            let intersection = allFiles.intersection(constantStrs)
            let unusedFiles = allFiles.subtracting(intersection)
            callback(unusedFiles.map({ $0.string }))
        }
    }
    
    /// 找出所有文本中[xxx ]中的xxx -- 主要耗時(shí)代碼,多線程
    func findConstantStrs5(_ basePath: String, _ callback: @escaping (Set<StringContainer>) -> ()) {
        let filePaths = FileManager.getAllFilePaths(basePath, self.pathFilter)
        var resultStrs = Set<StringContainer>()
        
        let workingGroup = DispatchGroup()
        let workingQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
        let lock: NSLock = NSLock()
        
        for path in filePaths {
            workingGroup.enter()
            workingQueue.async {
                do {
                    let content = try String(contentsOfFile: path as String, encoding: String.Encoding.utf8)
                    
                    // 過濾掉文件本身所含有的指定字符串
                    let results = self.findMatchStrs5(content).filter({
                        return !(path as NSString).lastPathComponent.contains($0.string)
                    })
                    
                    lock.lock()
                    resultStrs = resultStrs.union(results)
                    lock.unlock()
                    workingGroup.leave()
                }catch {
                    workingGroup.leave()
                }
            }
        }
        
        workingGroup.notify(queue: workingQueue) {
            callback(resultStrs)
        }
    }
    
    
    /// 正則匹配,找出字符串中[xxx ]中的xxx
    func findMatchStrs5(_ content: String) -> Set<StringContainer> {
        var result = Set<StringContainer>()
        let regex = try! NSRegularExpression(pattern: "\\[.*?\\s")
        let matches = regex.matches(in: content, range: NSRange(content.startIndex...,in: content))
        for  match in matches {
            // 這里的偏移值1,和1取決于pattern
            let range = NSRange(location: match.range.location + 1, length: match.range.length-1)
            var matchStr = (content as NSString).substring(with: range)
            // 因?yàn)檎齽t沒能寫的很準(zhǔn)確,找出的結(jié)果中含有不期望的字符,過濾
            matchStr = matchStr.replacingOccurrences(of: "[", with: "")
            matchStr = matchStr.replacingOccurrences(of: " ", with: "")
            result.insert(StringContainer(matchStr))
        }
        return result
    }
}



extension ResourceManager {
    
    /// 檢測路徑下所有相似的圖片
    /// similarity值越大,獲得的圖片相似程度越高,當(dāng)為1.0時(shí),獲取完全相同的圖片,建議取值0.9-1.0之間
    /// 原理:查找相同圖片時(shí),對比所有圖片的md5
    /// 原理:查找相似圖片時(shí),對比每一個(gè)像素點(diǎn)的rgba值在一定誤差范圍內(nèi)
    func checkSimilarityImages(path: String,
                               similarity: Double = 1.0,
                               callback: @escaping ([Set<ImageModel>]?) -> ()){
        
        self.imageSimilarityLevel = similarity
        
        let workingGroup = DispatchGroup()
        let workingQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
        let lock: NSLock = NSLock()
        
        let imagePaths = FileManager.getAllImagePaths(path)
        var allImageModels = [String: [ImageModel]]() // 存儲所有圖片資源模型,圖片尺寸為key,值為所有相同尺寸的圖片模型
        for path in imagePaths {
            // [UIImage imageNamed:]加載圖片會有緩存,使內(nèi)存變大
            // [UIImage imageWithContentsOfFile:]加載圖片,圖片尺寸會受到屏幕分辨率scale影響
            // [UIImage imageWithData:] 加載圖片,image.scale固定為1,圖片大小為本身大小
            let imageData = FileHandle(forReadingAtPath: path as String)?.readDataToEndOfFile()
            guard let image = UIImage(data: imageData!) else{ continue  }
            workingGroup.enter()
            workingQueue.async {
                var model = ImageModel()
                model.path = path as NSString
                model.image = image
                if self.isSameCompare {
                    model.imageMD5 = FileManager.md5File(path: path)
                }else {
                    model.points = self.getAllPixelRGBA(image: image)  // 耗時(shí)操作,因?yàn)閯?chuàng)建保存了大量Point對象
                }
                let key = "\(image.size.width)x\(image.size.height)"
                lock.lock()
                if allImageModels.keys.contains(key) {
                    allImageModels[key]?.append(model)
                }else {
                    allImageModels[key] = [model]
                }
                lock.unlock()
                workingGroup.leave()
            }
        }
        
        workingGroup.notify(queue: workingQueue) {
            let resultImageModels = self.handleImageModels(allImageModels)
            callback(resultImageModels)
        }
    }
    
    var isSameCompare: Bool {
        get {
            return self.imageSimilarityLevel >= 1.0
        }
    }
    
    /// 遍歷,找出所有相似的圖片
    func handleImageModels(_ allImageModels: [String: [ImageModel]]?) -> [Set<ImageModel>]? {
        guard var allImageModels = allImageModels else { return nil }
        var resultImageModels = [Set<ImageModel>]()
        for imageModels in allImageModels.values {
            var allSameImageModels: [Set<ImageModel>]?  // 同一尺寸下,所有相似圖片的多個(gè)集合數(shù)組,有重復(fù)
            for i in 0..<imageModels.count{
                var sameImageModels: Set<ImageModel>?  // 跟指定圖片相似的所有圖片集合
                var model = imageModels[i]
                for j in 0..<imageModels.count {
                    if i == j { continue }
                    var model2 = imageModels[j]
                    var isSame = true;
                    if self.isSameCompare {
                        if  model.imageMD5 == nil ||
                            model2.imageMD5 == nil ||
                            model.imageMD5!.compare(model2.imageMD5!) != .orderedSame{
                            isSame = false
                        }
                    }else {
                        if (model.points?.count != model2.points?.count) { continue }
                        guard let count = model.points?.count else { continue }
                        var sameCount = count
                        var sameScale = 1.0;
                        for k in 0..<count {
                            let point = model.points?[k]
                            let point2 = model2.points?[k]
                            if !self.isSimilarity(point, point2) {
                                sameCount -= 1
                                sameScale = Double(sameCount) / Double(count);
                                if (sameScale < self.imageSimilarityLevel) {
                                    isSame = false;
                                    break;
                                }
                            }
                        }
                    }
                    if (isSame) {
                        if sameImageModels == nil {
                            sameImageModels = [model]
                        }
                        sameImageModels!.insert(model2)
                    }
                }
                if sameImageModels != nil {
                    if allSameImageModels == nil {
                        allSameImageModels = [sameImageModels!]
                    }else {
                        allSameImageModels!.append(sameImageModels!)
                    }
                }
            }
            guard let mergeImageModels = self.mergeSameImageModels(allSameImageModels) else { continue }
            resultImageModels += mergeImageModels
        }
        return resultImageModels
    }
    
    /// 合并重復(fù)數(shù)據(jù)。 假定:A和B、C都相似,則B、C也相似
    /// 例:[[1,2],[1,3],[5,6]] -> [[1,2,3],[5,6]]
    func mergeSameImageModels(_ imageModels:[Set<ImageModel>]?) -> [Set<ImageModel>]? {
        guard let count = imageModels?.count else { return nil }
        var result = [Set<ImageModel>]()
        var handleIndexes = [Int]()
        for i in 0..<count {
            if handleIndexes.contains(i) { continue }
            var models1 = imageModels![i]
            for j in (i+1)..<count {
                let models2 = imageModels![j]
                if !models1.isDisjoint(with: models2) {
                    models1 = models1.union(models2)
                    handleIndexes.append(j)
                }
            }
            handleIndexes.append(i)
            result.append(models1)
        }
        return result
    }
    
    /// 比較兩個(gè)像素點(diǎn)是否一致(rgba差值在規(guī)定范圍內(nèi))
    func isSimilarity(_ point1: Point?, _ point2: Point?) -> Bool {
        guard let point1 = point1, let point2 = point2 else { return false }
        let similarity = Int(255 * (1 - imageSimilarityLevel));
        return
            (point1.r > point2.r ? point1.r - point2.r : point2.r - point1.r) <= similarity &&
            (point1.g > point2.g ? point1.g - point2.g : point2.g - point1.g) <= similarity &&
            (point1.b > point2.b ? point1.b - point2.b : point2.b - point1.b) <= similarity &&
            (point1.a > point2.a ? point1.a - point2.a : point2.a - point1.a) <= similarity
    }
    
    /// 獲取圖片所有像素點(diǎn)的RGBA值
    func getAllPixelRGBA(image: UIImage) -> [Point]?{
        let pixelData = image.cgImage?.dataProvider?.data
        let data:UnsafePointer<CUnsignedChar> = CFDataGetBytePtr(pixelData)
        var points = [Point]()
        let length = CFDataGetLength(pixelData!)
        for i in stride(from: 0, to: length, by: 4) {
            let r = data[i + 0]
            let g = data[i + 1]
            let b = data[i + 2]
            let a = data[i + 3]
            let point = Point(r, g, b, a)
            points.append(point)
        }
        return points
    }
    
    /// 圖片上一個(gè)像素點(diǎn)模型
    struct Point {
        var r: CUnsignedChar
        var g: CUnsignedChar
        var b: CUnsignedChar
        var a: CUnsignedChar
        init(_ r: CUnsignedChar,_ g: CUnsignedChar,_ b: CUnsignedChar,_ a: CUnsignedChar) {
            self.r = r
            self.g = g
            self.b = b
            self.a = a
        }
    }
    
    struct ImageModel: Hashable{
        var image: UIImage?
        var path: NSString?
        var points: [Point]?
        var imageMD5: String?
        
        static func == (lhs: ImageModel, rhs: ImageModel) -> Bool {
            guard let lhsPath = lhs.path,let rhsPath = rhs.path else { return false }
            return lhsPath.isEqual(rhsPath)
        }
        
        public var hashValue: Int {
            return path.hashValue
        }
    }
}


extension String {
    func removeSuffix(suffixs: [String]) -> String {
        for suffix in suffixs {
            if self.hasSuffix(suffix) {
                return (self as NSString).substring(to: self.count - suffix.count)
            }
        }
        return self
    }
}



import Foundation
import CommonCrypto


extension FileManager {
    
    typealias PathFilter = (_ path: String) -> Bool
    
    /// 找出所有.jpg/.png/.webP圖片路徑
    static func getAllImagePaths(_ basePath: String, _ filterBlock: PathFilter? = nil) -> [String] {
        let fileEnumerator = FileManager.default.enumerator(atPath: basePath)
        var imagePaths = [String]()
        for path in (fileEnumerator?.allObjects)! {
            let currentPath = path as! NSString
            if  currentPath.lastPathComponent.hasSuffix(".png") ||
                currentPath.lastPathComponent.hasSuffix(".jpg") ||
                currentPath.lastPathComponent.hasSuffix(".webP") {
                let totalPath = (basePath as NSString).appendingPathComponent(path as! String)
                let filter = filterBlock != nil ? filterBlock!(totalPath) : true
                if filter {
                    imagePaths.append(totalPath)
                }
            }
        }
        return imagePaths
    }
    
    /// 找出所有.h/.m/.mm文件路徑
    static func getAllFilePaths(_ basePath: String, _ filterBlock: PathFilter? = nil) -> [String]  {
        let fileEnumerator = FileManager.default.enumerator(atPath: basePath)
        var filePaths = [String]()
        for path in (fileEnumerator?.allObjects)! {
            let currentPath = path as! NSString
            if  currentPath.lastPathComponent.hasSuffix(".h") ||
                currentPath.lastPathComponent.hasSuffix(".pch") ||
                currentPath.lastPathComponent.hasSuffix(".m") ||
                currentPath.lastPathComponent.hasSuffix(".mm") {
                let totalPath = (basePath as NSString).appendingPathComponent(path as! String)
                let filter = filterBlock != nil ? filterBlock!(totalPath) : true
                if filter {
                    filePaths.append(totalPath)
                }
            }
        }
        return filePaths
    }
    
    /// 獲取路徑下所有空的文件夾
    static func getAllEmptyDirectoryPaths(_ basePath: String, _ filterBlock: PathFilter? = nil) -> [String]  {
        let manager = FileManager.default
        let fileEnumerator = manager.enumerator(atPath: basePath)
        var filePaths = [String]()
        for path in (fileEnumerator?.allObjects)! {
            let currentPath = (basePath as NSString).appendingPathComponent(path as! String)
            if self.isDirectory(currentPath) {
                do {
                    let contents = try manager.contentsOfDirectory(atPath: currentPath).filter { !$0.contains(".DS_Store") }
                    if contents.count == 0 {
                        let filter = filterBlock != nil ? filterBlock!(currentPath) : true
                        if filter {
                            filePaths.append(currentPath)
                        }
                    }
                }catch {}
            }
        }
        return filePaths
    }
    
    /// 是否是文件夾
    static func isDirectory(_ path: String) -> Bool {
        var directoryExists = ObjCBool.init(false)
        let fileExists = FileManager.default.fileExists(atPath: path, isDirectory: &directoryExists)
        return fileExists && directoryExists.boolValue
    }
    
    /// 對路徑下的文件內(nèi)容進(jìn)行MD5
    static func md5File(path: String) -> String? {
        guard let file = FileHandle(forReadingAtPath: path) else { return nil }
        var context = CC_MD5_CTX()
        CC_MD5_Init(&context)
        while case let data = file.readDataToEndOfFile(), data.count > 0 {
            data.withUnsafeBytes {
                _ = CC_MD5_Update(&context, $0, CC_LONG(data.count))
            }
        }
        var digest = Data(count: Int(CC_MD5_DIGEST_LENGTH))
        digest.withUnsafeMutableBytes {
            _ = CC_MD5_Final($0, &context)
        }
        return digest.map { String(format: "%02hhx", $0) }.joined()
    }
}

發(fā)現(xiàn):高德地圖內(nèi)部的budle圖片每一次讀取像素點(diǎn)都不完全一樣,無法通過上面方法判斷圖片是否相同。這應(yīng)該是圖片做了特殊處理,防止拷貝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容