iOS项目资源管理
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图片每一次读取像素点都不完全一样,无法通过上面方法判断图片是否相同。这应该是图片做了特殊处理,防止拷贝