iOS项目资源管理

2019-11-04  本文已影响0人  请叫我啊亮

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

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

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

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

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

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

无用类和无用方法可以通过分析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图片每一次读取像素点都不完全一样,无法通过上面方法判断图片是否相同。这应该是图片做了特殊处理,防止拷贝

上一篇下一篇

猜你喜欢

热点阅读