美文网首页
iOS项目资源管理

iOS项目资源管理

作者: 请叫我啊亮 | 来源:发表于2019-11-04 17:47 被阅读0次

    APP瘦身一个重要方式是删除冗余内容,包括类文件,图片等

    Apple 为了在优化 iPhone 设备读取 png 图片速度,将 png 转换成 CgBI 非标准的 png 格式。这种优化对于大多数应用来说都是包大小的负优化。所以简单的压缩(有损,无损)处理并不能达到很好的瘦身效果。

    经过测试,以下文件会被负优化:

    • 放在根目录下png格式的图片。
    • 放在Asset Catalog中的png、jpg格式的图片,其中jpg会转成png。

    放在根目录下的jpg,bundle中的png不会被优化

    jpg和png区别
    jpg有损压缩,png为无损压缩。jpg的图片更小。 jpg图像没有透明的背景,而png图像可以保留透明的背景

    针对大图(如大于60KB)的处理:

    • 优先转网络下载,使用默认图/纯色兜底
    • 不能转下载的使用压缩过的jpg格式图片,放到xcode根目录下。
    • 不能使用jpg的图片经过压缩后放到 bundle 中使用。

    无用类和无用方法可以通过分析Mach-O得到

    RN:
    主要处理内置包大小。
    深层次的RN单独作为一个模块,不内置。采用用户点击时触发下载流程,下载完后存储再进入的方式
    iphoneX的图片不要打进安卓内置包里面,

    其他如 iconfont,图片webP化

    工具:CATClearProjectTool,检查项目中未使用的类文件
    原理:查找项目中所有的类文件,以及每一个类文件中的”xxx”这样的字符串,根据OC导入类的规则import “xxx.h”,就是找出这里的xxx字符串,对比上面两个结果找出未使用类文件。

    工具:LSUnusedResources,检查项目中未使用的图片
    原理:正则匹配(如@“.*?”)项目中所有使用的字符串,生成hash表。再读取项目所有图片,看是否存在于hash表中,没有则没有使用。

    LSUnusedResources的不足
    1、效率低下,我项目中有1405个图片资源,13991个常量字符串,耗时约65s
    2、无法查找项目中存在的名字/格式不同,但是内容完全一致的图片
    3、项目中可能存在一个图片,以及它的多个非常相似或者压缩后的图片,希望能找出来

    ResourceManager

    说明:
    1、checkUnUsedImages,查找项目中没有使用的图片,png/jpg/webP
    找出路径下所有文件中的常量字符串,以及路径下所有图片,不在前者中的图片则未使用
    只支持OC语言,且没有适配xib,结果中可能会包含OC动态生成使用的图片,删除时需注意排查,可以删除图片后Hook UIImage imageNamed方法,运行程序跑一跑,看是否存在图片名有值,而生成的图片为nil的现象,判断是否误删图片

    2、checkUnUsedClasses,查找项目中没有使用的类文件
    找出路径下所有文件中的“xxx.h”,以及路径下所有.h文件名,不在前者中的文件则未使用
    只支持OC文件,结果中可能会包含OC动态生成使用的类,删除时需注意排查

    3、checkUnUsedClasses2,查找项目中没有使用的类文件
    找出路径下所有.m文件名,以及文件中字符串[xxx ]中的xxx,不在后者中的文件则未使用。主要是为了找出项目中只import但是未使用的那种文件
    注意:只支持OC语言,结果中存在较多的错误结果,例如基类,分类,文件名和类名不一致的等

    4、checkSimilarityImages,检测路径下所有相似的图片
    similarity值越大,获得的图片相似程度越高,当为1.0时,获取完全相同的图片,建议取值0.9-1.0之间
    查找相同图片时,对比所有图片的md5
    查找相似图片时,对比每一个像素点的rgba值在一定误差范围内

    5、getAllEmptyDirectoryPaths,检测空文件夹

    6、性能瓶颈在正则匹配所有文件中的指定字符串,通过多线程提升匹配效率

    7、一个像素点模型包括RGBA四个成员变量,每个占用1个字节。OC模型占用16字节,swift只占用4个字节,内存更小

    8、 siwft对结构体的优化,方法静态调用内联优化等,处理数组中大量结构体时速度更快

    性能测试:
    环境 iPhone x 模拟器
    项目图片资源数量 304
    所有图片像素点个数 2300万+
    所有像素点模型加载进内存消耗 OC: 750M swift: 150M
    单线程处理耗时: OC: 6.2s swift: 5s

    不足:找出未使用的类、图片等,删除后再扫描又会找出一批未使用的图片、类等。
    优化方向:记录下被引用者是否在未使用范围内。例如图片A只被B类使用,而B是个未使用的类,则A也算未使用图片

    实现:

    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)
        }
        
        /// 获取项目中空的文件夹
        func checkEmptyDirectorys(_ basePath: String) {
            let paths = FileManager.getAllEmptyDirectoryPaths(basePath) { !$0.contains(".git") }
            if paths.count > 0 {
                print("以下是空文件")
            }
            for path in paths {
                print(path)
            }
        }
        
        /// 获取项目中未使用的类文件
        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)
                }
            }
        }
        
        /// 测试: 项目中1405个图片资源,13991个常量字符串,耗时约4.5s
        /// LSUnusedResources 需要约65s,相差15倍, 结果一致
        func checkUnUsedImages(_ basePath: String) {
            ResourceManager.shared.checkUnUsedImages(basePath: basePath) { result in
                guard let images = result else { return }
                print("以下图片没有使用")
                for image in images {
                    print(image)
                }
            }
        }
        
        /// 获取项目中相同的图片
        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
        /// 默认过滤掉.bundle文件中的图片等资源
        var pathFilter: FileManager.PathFilter = { !$0.contains(".bundle") }
    }
    
    
    extension ResourceManager {
    
        /// 查找项目中没有使用的图片,png/jpg/webP
        /// 原理:找出路径下所有文件中的常量字符串,以及路径下所有图片,不在前者中的图片则未使用
        /// 注意:只支持OC语言,对OC动态生成的字符串会出错,使用时需注意排查
        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 })
            }
        }
        
        /// 正则匹配出所有文本中的所有常量字符串 -- 主要耗时代码,多线程
        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中比较时,使 “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 {
        
        
        /// 查找项目中没有使用的类
        /// 原理:找出路径下所有文件中的“xxx.h”,以及路径下所有.h文件名,不在前者中的文件则未使用
        /// 找出没有import的类, 需要注意只有+load方法的类
        /// 注意:只支持OC语言,结果中可能会包含OC动态生成使用的类,删除时需注意排查
        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” -- 主要耗时代码,多线程
        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 {
        
        /// 查找项目中没有使用的类
        /// 原理:找出路径下所有.m文件名,以及文件中字符串[xxx ]中的xxx,不在后者中的文件则未使用
        /// 主要是找出项目中只import但是未使用的那种文件,需要注意只有+load方法的类
        /// 注意:只支持OC语言,结果中存在较多的错误结果,例如基类,分类,文件名和类名不一致的等
        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 -- 主要耗时代码,多线程
        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)
                // 因为正则没能写的很准确,找出的结果中含有不期望的字符,过滤
                matchStr = matchStr.replacingOccurrences(of: "[", with: "")
                matchStr = matchStr.replacingOccurrences(of: " ", with: "")
                result.insert(StringContainer(matchStr))
            }
            return result
        }
    }
    
    
    
    extension ResourceManager {
        
        /// 检测路径下所有相似的图片
        /// similarity值越大,获得的图片相似程度越高,当为1.0时,获取完全相同的图片,建议取值0.9-1.0之间
        /// 原理:查找相同图片时,对比所有图片的md5
        /// 原理:查找相似图片时,对比每一个像素点的rgba值在一定误差范围内
        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:]加载图片会有缓存,使内存变大
                // [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)  // 耗时操作,因为创建保存了大量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>]?  // 同一尺寸下,所有相似图片的多个集合数组,有重复
                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
        }
        
        /// 合并重复数据。 假定: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
        }
        
        /// 比较两个像素点是否一致(rgba差值在规定范围内)
        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
        }
        
        /// 获取图片所有像素点的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
        }
        
        /// 图片上一个像素点模型
        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
        }
        
        /// 对路径下的文件内容进行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()
        }
    }
    
    

    发现:高德地图内部的budle图片每一次读取像素点都不完全一样,无法通过上面方法判断图片是否相同。这应该是图片做了特殊处理,防止拷贝

    相关文章

      网友评论

          本文标题:iOS项目资源管理

          本文链接:https://www.haomeiwen.com/subject/iiwdbctx.html