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)該是圖片做了特殊處理,防止拷貝