零. 前言
在寫完前面一篇LinkMap初探的文章過后,成功分析出來的各個文件占據的體積大小,然后腦子一個靈感突然擊中我:既然都分析出來體積了, 能不能順便幫IPA包瘦瘦身呢,于是我發(fā)現了LSUnusedResources這個好東西,這個軟件可以直接拿來分析出工程中可能沒用到的資源。
當時沒想太多,直接拿來用了,導出來文件目錄后自己寫了個腳本過濾了一遍,但后面答辯的時候,領導問我,你有沒有分析過這個源碼,為什么這個軟件查出來的無用圖片,很多是實際上有用到的,以至于你自己寫了一個腳本再過濾一遍,我無言以對= =
最近肺炎在家也閑得無聊,沒什么工作要干,而且這個源碼是Mac應用,也是用OC來寫的,所以我們就來看看這個源碼到底是怎么實現的吧!
這次分析源碼,我會帶著以下問題去進行,中間源碼分析可能會占用一段時間,如果想直接得到結論可以翻到四. 后續(xù)分析,但是有些細節(jié)問題還是要通過源碼分析來解決:
這個源碼是怎么做到查出所有的圖片資源的?畢竟圖片資源有很多,格式也不一樣。
這個源碼又是怎么看出該圖片有沒有被引用的?畢竟不同后綴名的文件的引用方法不同,如.m是
imageNamed:@"",.xib則是image name="",等等。那為什么這個工程的誤刪率這么高,還要我自己寫個腳本再過濾一遍?
一. 簡介
首先看看這個界面長啥樣

此軟件分為三部分,第一部分是查找文件的目錄,第二部分是查找規(guī)則,第三部分是查找結果,因為我們這次是為了探索這個軟件是怎么過濾無用圖片的,所以重點分析一下第二部分和第三部分。
首先來看文件架構:

二. 找出所有的圖片資源
這部分主要是根據使用者輸入的Project Path和Exclude Folder,在directoryPath內,搜索出filetype類型的所有路徑名,filetype就是上面Resource Suffix的輸入內容,即imageset | jpg | gif | png這四種iOS工程中的圖片后綴。
- (NSArray *)searchDirectory:(NSString *)directoryPath excludeFolders:(NSArray *)excludeFolders forFiletype:(NSString *)filetype {
// Create a find task
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: @"/usr/bin/find"];
// Search for all files
NSMutableArray *argvals = [NSMutableArray array];
[argvals addObject:directoryPath];
[argvals addObject:@"-name"];
[argvals addObject:[NSString stringWithFormat:@"*.%@", filetype]];
for (NSString *folder in excludeFolders) {
[argvals addObject:@"!"];
[argvals addObject:@"-path"];
[argvals addObject:[NSString stringWithFormat:@"*/%@/*", folder]];
}
[task setArguments: argvals];
NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file = [pipe fileHandleForReading];
// Run task
[task launch];
// Read the response
NSData *data = [file readDataToEndOfFile];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// See if we can create a lines array
if (string.length) {
NSArray *lines = [string componentsSeparatedByString:@"\n"];
return lines;
}
return nil;
}
上面這塊代碼一執(zhí)行下來,就可以得到一個所有圖片后綴的目錄數組,實現原理是用了pipe之類的,這里我不打算深究,只需要知道他的目的就可以了。
獲取到所有圖片的文件路徑后,下面的代碼對本身在.imageset、.launchimage、.appiconset、.bundle文件夾里面的圖片進行過濾。
if (pathList.count) {
for (NSString *path in pathList) {
// ignore if the resource file is in xxx/xxx.imageset/; xx/LaunchImage.launchimage; xx/AppIcon.appiconset; xx.bundle/xx
if (![self isInImageSetFolder:path]
&& [path rangeOfString:kSuffixBundle].location == NSNotFound) {
[resources addObject:path];
}
}
對這些圖片信息進行遍歷,并且形成了一個個的ResourceFileInfo類,并用了以名字為key,info為value的字典,存取了這些信息
// 圖片資源的信息
@interface ResourceFileInfo : NSObject
// 舉例:/Users/xxx/image1.imageset
@property (strong, nonatomic) NSString *name; // 名字,如image1
@property (strong, nonatomic) NSString *path; // 路徑,如/Users/xxx/image1.imageset
@property (assign, nonatomic) BOOL isDir; // 是否為文件夾,如YES
@property (assign, nonatomic) uint64_t fileSize; // 文件大小,如1kB
- (NSImage *)image;
@end
NSArray *resPaths = [self resourceFilesInDirectory:projectPath excludeFolders:excludeFolders resourceSuffixs:resourceSuffixs];
NSMutableDictionary *tempResNameInfoDict = [NSMutableDictionary dictionary];
for (NSString *path in resPaths) {
NSString *name = [path lastPathComponent];
if (!name.length) {
continue;
}
// 獲得文件名稱
NSString *keyName = [StringUtils stringByRemoveResourceSuffix:name];
if (!tempResNameInfoDict[keyName]) {
BOOL isDir = NO;
ResourceFileInfo *info = [ResourceFileInfo new];
info.name = name;
info.path = path;
info.fileSize = [FileUtils fileSizeAtPath:path isDir:&isDir];
info.isDir = isDir;
tempResNameInfoDict[keyName] = info;
}
}
自此,我們就獲得了該工程里面的所有圖片資源信息,他被存儲在resNameInfoDict這個字典信息中。
但是好戲才剛剛開始,因為我們要對這些資源進行遍歷,看看哪些資源是被工程引用的,而哪些工程是真正可以刪去的。
三. 驗證資源是否有被引用
iOS工程中用到的后綴文件各式各樣,.h .m .swift .xib,甚至有用到.html的,他們引用資源的方式各不相同,怎么知道圖片有沒有被這些文件引用,的確是個難題。
// 要查找的后綴名信息
@interface ResourceStringPattern : NSObject
@property (strong, nonatomic) NSString *suffix; // 查找后綴名,如.h/.m等
@property (assign, nonatomic) BOOL enable; // 是否勾選
@property (strong, nonatomic) NSString *regex; // 對應的正則表達式
@property (assign, nonatomic) NSInteger groupIndex; // 第幾組
// 將dict轉換成該類方法
- (id)initWithDictionary:(NSDictionary *)dict;
@end
可以看到,每個后綴文件的結構體有個最關鍵的東西,那就是正則表達式,這可是驗證資源是否被引用的利器。
1. 正則表達式
魯迅說過,正則表達式是個好東西,雖然他長得不太順眼,但是能用好他的話,一行甚至就能達到幾十行代碼的效果,在這個界面中,每個后綴名都有自己對應的正則表達式。
沒有接觸過正則表達式的我,只能靠一本字典和一個驗證器來一點點探索了,過程有點艱苦= =
這個軟件會默認生成一堆后綴名和他們對應的正則表達式,使用的時候我們可以直接用,但是要是想看看他的工作機制,那么還是看一下每一個正則的意思吧。
NSArray *fileSuffixs = @[@"h", @"m", @"mm", @"swift", @"xib", @"storyboard", @"strings", @"c", @"cpp", @"html", @"js", @"json", @"plist", @"css"];
NSString *cPattern = [NSString stringWithFormat:@"([a-zA-Z0-9_-]*)\\.(%@)", [resSuffixs componentsJoinedByString:@"|"]]; // *.(png|gif|jpg|jpeg)
NSString *ojbcPattern = @"@\"(.*?)\""; // @"imageNamed:@\"(.+)\"";//or: (imageNamed|contentOfFile):@\"(.*)\" // http://www.raywenderlich.com/30288/nsregularexpression-tutorial-and-cheat-sheet
NSString *xibPattern = @"image name=\"(.+?)\""; // image name="xx"
NSArray *filePatterns = @[cPattern, // .h
ojbcPattern, // .m
ojbcPattern, // .mm
@"\"(.*?)\"",// swift.
xibPattern, // .xib
xibPattern, // .storyboard
@"=\\s*\"(.*)\"\\s*;", // .strings
cPattern, // .c
cPattern, // .cpp
@"img\\s+src=[\"\'](.*?)[\"\']", // .html, <img src="xx"> <img src='xx'>
@"[\"\']src[\"\'],\\s+[\"\'](.*?)[\"\']", // .js, "src", "xx"> 'src', 'xx'>
@":\\s*\"(.*?)\"", // .json, "xx"
@">(.*?)<", // .plist, "<string>xx</string>"
cPattern]; // .css
下面來對每個格式的正則表達式進行分析:
- C格式(.h .c .cpp .css)
正則表達式:([a-zA-Z0-9_-]*)\.(imageset|jpg|gif|png)
意義:以.為界限,.前面可以是任意字母、數字、下劃線或者橫線,.后面是上述四種格式之一。個人認為應該可以加多個@,表示@2x,@3x也可以匹配到。
這里.h應該用objc的,因為.h文件有可能是這樣的
#define HobenImageStr [UIImage imageNamed:@"Hoben"]
- OC格式(.m .mm)
正則表達式:@"(.*?)"
意義:匹配@"xxx",xxx可為任意內容(引號除外)
- xib格式(.xib .storyboard)
正則表達式:image name="(.+?)"
意義:image name="xxx",xxx不能為空且他們之間不能有換行符
- swift格式
正則表達式:"(.*?)"
意義:匹配"xxx",xxx可為任意內容(引號除外)
- strings格式(InfoPlist.strings,感覺很少用到。。)
正則表達式:=\s*"(.*)"\s*;
意義:= (中間任意空格) "xxx" (中間任意空格) ; 其中=、""、;不可缺少
- html格式
正則表達式:img\s+src=["'](.*?)["']
意義:img(中間至少一個空格)src="xxx"
或者:img(中間至少一個空格)src=‘xxx’
- js格式
正則表達式:["']src["'],\s+["'](.*?)["']
意義:"src",(中間至少一個空格)"xxx"
或者:'src',(中間至少一個空格)'xxx'
- json格式
正則表達式::\s*"(.*?)"
意義::(中間任意空格)"xxx"
- plist格式
正則表達式:>(.*?)<
意義:>xxx<,xxx可為空,應該是對應<string>xxx</string>的情況
分析完這些正則之后,總感覺有點怪怪的感覺,比如objcPattern里面,只要符合@"xxx",就會被視為是有用的資源了,而html格式src="xxx",感覺中間可以插入若干空格,會不會也有部分資源因為不匹配這個而視為無用資源。當然這只是我分析完之后的疑問,我們先繼續(xù)往下看。
2. 開始掃描
在前面,我們獲取到了所有資源的名稱、后綴、路徑、大小,也獲取到了所有后綴文件和他們的掃描規(guī)則(當然這里是可以自己設置的,是否掃描某個后綴,掃描規(guī)則,掃描的資源類型后綴),下面我們就可以開始進行掃描了!
- (BOOL)handleFilesAtPath:(NSString *)dir {
// Get all files at the dir
NSError *error = nil;
NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dir error:&error];
if (files.count == 0) {
return NO;
}
for (NSString *file in files) {
if ([file hasPrefix:@"."]) {
continue;
}
if ([self.excludeFolders containsObject:file]) {
continue;
}
NSString *tempPath = [dir stringByAppendingPathComponent:file];
if ([self isDirectory:tempPath]) {
// 還是文件夾,繼續(xù)找里面的文件
[self handleFilesAtPath:tempPath];
} else {
// 是文件了,則獲取其后綴名進行相應正則掃描
// 以/Users/xxx/File.m為例
// ext就是m
// resourcePattern就是m對應的結構體(包含正則表達式)
// tempPath就是/Users/xxx/File.m
NSString *ext = [[file pathExtension] lowercaseString];
ResourceStringPattern *resourcePattern = self.fileSuffixToResourcePatterns[ext];
if (!resourcePattern) {
continue;
}
[self parseFileAtPath:tempPath withResourcePattern:resourcePattern];
}
}
return YES;
}
掃描文件用了遞歸的方式,直到這個文件路徑屬于文件,且后綴名是可以處理的,才會對其進行處理。
現在,我們有路徑名和其對應的正則處理方法了,下面可以根據路徑名獲得文件內容(即下面的content),再按正則處理了!
處理方法如注釋所示:
- (NSSet *)getMatchStringWithContent:(NSString *)content pattern:(NSString*)pattern groupIndex:(NSInteger)index {
NSRegularExpression *regexExpression = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
NSArray* matchs = [regexExpression matchesInString:content options:0 range:NSMakeRange(0, content.length)];
if (matchs.count) {
// 找出所有符合正則的關鍵詞
NSMutableSet *set = [NSMutableSet set];
for (NSTextCheckingResult *checkingResult in matchs) {
// 根據正則表達式的第index(默認1)個分組來獲取
// 且只獲取名字,即@2x/@3x這些會被隱去
// 比如對于.h文件,其正則為([a-zA-Z0-9@_-]*)\.(imageset|jpg|gif|png)
// 則匹配到了Hoben@2x.jpg字符之后,經過下面的處理后獲取到的res為Hoben
NSString *res = [content substringWithRange:[checkingResult rangeAtIndex:index]];
if (res.length) {
res = [res lastPathComponent];
res = [StringUtils stringByRemoveResourceSuffix:res];
[set addObject:res];
}
}
return set;
}
return nil;
}
為了理解上面的rangeAtIndex:index方法,找了很久的博客,終于找到了這篇對方法的說明和這篇對group的說明,原來是和正則里面的group是一個意思...
于是,我們通過遍歷了工程中所有的圖片,也獲得了一個被使用的資源列表resStringSet。
開始根據這個規(guī)則來遍歷:
- (BOOL)containsResourceName:(NSString *)name {
if ([self.resStringSet containsObject:name]) {
return YES;
} else {
if ([name pathExtension]) {
NSString *nameWithoutSuffix = [StringUtils stringByRemoveResourceSuffix:name];
return [self.resStringSet containsObject:nameWithoutSuffix];
}
}
return NO;
}
該軟件還提供了一個近似名字檢索的功能:Ignore similar name (eg: tag_1.png, using with "tag_%d" or "tag" will be considered to be used )
這個近似檢索使用的正則表達式為([-_]?\d+),意思是,如果一張圖片的名字前面或后面跟著_或者-,且后面有至少一個數字,則可以被視為近似名字,比較近似名字的方法是取前綴/后綴,再進行比較。
- (BOOL)containsSimilarResourceName:(NSString *)name {
NSString *regexStr = @"([-_]?\\d+)";
name = @"_1Hoben";
NSRegularExpression* regexExpression = [NSRegularExpression regularExpressionWithPattern:regexStr options:NSRegularExpressionCaseInsensitive error:nil];
NSArray* matchs = [regexExpression matchesInString:name options:0 range:NSMakeRange(0, name.length)];
if (matchs != nil && [matchs count] == 1) {
NSTextCheckingResult *checkingResult = [matchs objectAtIndex:0];
NSRange numberRange = [checkingResult rangeAtIndex:1];
NSString *prefix = nil;
NSString *suffix = nil;
BOOL hasSamePrefix = NO;
BOOL hasSameSuffix = NO;
// _1Hoben:hasSamePrefix && !hasSameSuffix,suffix = @"Hoben"
// Hoben_1:!hasSamePrefix && hasSameSuffix,prefix = @"Hoben"
if (numberRange.location != 0) {
prefix = [name substringToIndex:numberRange.location];
} else {
hasSamePrefix = YES;
}
if (numberRange.location + numberRange.length < name.length) {
suffix = [name substringFromIndex:numberRange.location + numberRange.length];
} else {
hasSameSuffix = YES;
}
for (NSString *res in self.resStringSet) {
if (hasSameSuffix && !hasSamePrefix) {
if ([res hasPrefix:prefix]) {
return YES;
}
}
if (hasSamePrefix && !hasSameSuffix) {
if ([res hasSuffix:suffix]) {
return YES;
}
}
if (!hasSamePrefix && !hasSameSuffix) {
if ([res hasPrefix:prefix] && [res hasSuffix:suffix]) {
return YES;
}
}
}
}
return NO;
}
除此之外,.imageset文件夾里面還有其他圖片,里面的圖片可能被工程所引用,所以也要排除一下。
- (BOOL)usingResWithDiffrentDirName:(ResourceFileInfo *)resInfo {
if (!resInfo.isDir) {
return NO;
}
NSDirectoryEnumerator *fileEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:resInfo.path];
// A.imageset里面有B@2x.png的情況
// B也有可能被其他文件引用,所以要遍歷.imageset里面的所有圖片
for (NSString *fileName in fileEnumerator) {
if (![StringUtils isImageTypeWithName:fileName]) {
continue;
}
NSString *fileNameWithoutExt = [StringUtils stringByRemoveResourceSuffix:fileName];
if ([fileNameWithoutExt isEqualToString:resInfo.name]) {
return NO;
}
if ([[ResourceStringSearcher sharedObject] containsResourceName:fileNameWithoutExt]) {
return YES;
}
}
return NO;
}
了解上面三個關鍵的比較方法之后,就可以開始遍歷了:
NSArray *resNames = [[[ResourceFileSearcher sharedObject].resNameInfoDict allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
for (NSString *name in resNames) {
if (![[ResourceStringSearcher sharedObject] containsResourceName:name]) {
if (!self.ignoreSimilarCheckbox.state
|| ![[ResourceStringSearcher sharedObject] containsSimilarResourceName:name]) {
//TODO: if imageset name is A but contains png with name B, and using as B, should ignore A.imageset
ResourceFileInfo *resInfo = [ResourceFileSearcher sharedObject].resNameInfoDict[name];
if (!resInfo.isDir
|| ![self usingResWithDiffrentDirName:resInfo]) {
[self.unusedResults addObject:resInfo];
}
}
}
}
此時得到的unusedResults即為經過了檢驗的無用資源,后面的導出和刪除就略過不講了。
四. 后續(xù)分析
終于分析完源碼和他的工作機制了!下面來談談之前思考的幾個問題。
- 這個源碼是怎么做到查出所有的圖片資源的?
思路倒是挺簡單的,遞歸遍歷,直到遍歷到后綴名為預存的圖片后綴,取他的名字生成圖片資源類,放進數組即可。
- 這個源碼又是怎么看出該圖片有沒有被引用的?畢竟不同后綴名的文件的引用方法不同,如.m是
imageNamed:@"",.xib則是image name="",等等。
一個后綴名對應一個正則表達式,如.m文件匹配@"",.xib文件匹配image name="",在掃描該后綴名的文件時,取出對應正則,對上面獲得的圖片資源進行正則匹配,匹配得出來就是有用的資源啦!
- 那為什么這個工程的誤刪率這么高,還要我自己寫個腳本再過濾一遍?
這個問題是因為我沒勾選相似圖片忽略,導致很多帶數字的圖片,如image_1,他就在原工程上面查不到,因為原工程是這樣的:[UIImage imageNamed:@"image_%ld", 1];

勾選了之后,看了看,的確誤刪率沒那么高了。
五. 源碼優(yōu)化
雖然他刪除的文件為幾M,但是刪除后生成的IPA包也是可以瘦身十多M的,的確是能正確刪除文件的,看來這個開源項目勾選了之后的確好用,但還是會有些有可能漏刪的情況,下面這種情況,原邏輯就可能會錯過資源:
圖片名字為Hoben.jpg、Hoben_1.jpg...Hoben_10.jpg,工程中引用了Hoben,但是對于之后的連續(xù)圖片,沒有引用了,按照原邏輯取下劃線前綴的做法,則會取到前綴Hoben,誤以為帶序號的圖片也被引用了,從而導致漏刪。
思路:Hoben_1.jpg如果需要被主工程引用,則有以下幾種調用方法:
// 方法一:
[UIImage imageNamed:@"Hoben_1"]
// 方法二
[UIImage imageNamed:@"Hoben_%ld", 1]
// 方法三
NSString *str = @"Hoben"
[UIImage imageNamed:@"%@_%ld", str, 1]
對于方法一和方法二,我們完全可以根據Hoben_為前綴,再判斷后面是否跟著數字或者百分號即可,但是,我們并不可以完全以這個為依據判斷這個為無用資源,因為方法三還是可能會被調用的,所以,我們加個定義,這個為疑似無用資源,即需要使用者根據導出的文件,自行判斷是否需要刪掉。
如果序號在前,如1_Hoben.jpg,則調用方法可能為以下情況
// 方法一:
[UIImage imageNamed:@"1_Hoben"]
// 方法二
[UIImage imageNamed:@"%ld_Hoben", 1]
// 方法三
NSString *str = @"Hoben"
[UIImage imageNamed:@"%ld_%@", 1, str]
如果為序號在前的圖片,則需要用正則來判斷了:%(.*?)_Hoben,這個正則的意思是百分號和前綴之間可以有任意字符。
不過由于序號在前還是比較少見的,如果一個圖片有多處序號,如Hoben2017_1,則我們還是默認取Hoben2017_作為前綴,這個處理方法可能會漏掉序號在前的圖片,但大多數圖片還是可以檢測出來的。
綜上,我們可以改一下源碼,來揪出一些可能被漏刪的無用資源,首先是要對判斷方法的修改,將正則改為(\\d+),無需加入下劃線判斷了,其次,由于可能匹配多處數字,所以匹配的次數不一定只有1個,可以為多個且取最后一個匹配。
但考慮到第三種情況的調用,為了避免誤刪,刪除的時候還是要去除下劃線來檢測,帶下劃線的歸入疑似圖片,讓使用者自行判斷是否需要刪除
- (BOOL)containsSimilarResourceName:(NSString *)name {
NSString *regexStr = @"(\\d+)";
NSRegularExpression* regexExpression = [NSRegularExpression regularExpressionWithPattern:regexStr options:NSRegularExpressionCaseInsensitive error:nil];
NSArray* matchs = [regexExpression matchesInString:name options:0 range:NSMakeRange(0, name.length)];
if (matchs != nil && [matchs count] > 0) {
NSTextCheckingResult *checkingResult = [matchs lastObject];
NSRange numberRange = [checkingResult rangeAtIndex:1];
NSString *prefix = nil;
NSString *suffix = nil;
BOOL hasSamePrefix = NO;
BOOL hasSameSuffix = NO;
if (numberRange.location != 0) {
prefix = [name substringToIndex:numberRange.location];
} else {
hasSamePrefix = YES;
}
if (numberRange.location + numberRange.length < name.length) {
suffix = [name substringFromIndex:numberRange.location + numberRange.length];
} else {
hasSameSuffix = YES;
}
// _1Hoben:取后綴,hasSamePrefix && !hasSameSuffix,suffix = @"Hoben"
// Hoben_1:取前綴,!hasSamePrefix && hasSameSuffix,prefix = @"Hoben"
NSString *prefixWithUnderLine = prefix;
NSString *suffixWithUnderLine = suffix;
while ([[prefix substringFromIndex:[prefix length] - 1] isEqualToString:@"_"] || [[prefix substringFromIndex:[prefix length] - 1] isEqualToString:@"-"]) {
prefix = [prefix substringToIndex:prefix.length - 1];
}
while ([[suffix substringToIndex:1] isEqualToString:@"_"] || [[suffix substringToIndex:1] isEqualToString:@"-"]) {
suffix = [suffix substringFromIndex:1];
}
for (NSString *res in self.resStringSet) {
if (hasSameSuffix && !hasSamePrefix) {
if ([res hasPrefix:prefix]) {
[self checkIfSeeminglyResWithRes:res
prefixOrSuffix:prefixWithUnderLine
hasSameSuffix:hasSameSuffix
hasSamePrefix:hasSamePrefix];
return YES;
}
}
if (hasSamePrefix && !hasSameSuffix) {
if ([res hasSuffix:suffix]) {
[self checkIfSeeminglyResWithRes:res
prefixOrSuffix:suffixWithUnderLine
hasSameSuffix:hasSameSuffix
hasSamePrefix:hasSamePrefix];
return YES;
}
}
if (!hasSamePrefix && !hasSameSuffix) {
if ([res hasPrefix:prefix] && [res hasSuffix:suffix]) {
return YES;
}
}
}
}
return NO;
}
檢查疑似無用資源方法如下:
// 用于檢測是否為疑似無用資源
- (void)checkIfSeeminglyResWithRes:(NSString *)res
prefixOrSuffix:(NSString *)prefixOrSuffix
hasSameSuffix:(BOOL)hasSameSuffix
hasSamePrefix:(BOOL)hasSamePrefix {
BOOL isSeeminglyRes = YES;
if (hasSameSuffix && !hasSamePrefix) {
for (int i = 0; i <= 9; i++) {
NSString *numPrefix = [NSString stringWithFormat:@"%@%d", prefixOrSuffix, i];
if ([res hasPrefix:numPrefix]) {
isSeeminglyRes = NO;
}
}
NSString *percentPrefix = [NSString stringWithFormat:@"%@%%", prefixOrSuffix];
if ([res hasPrefix:percentPrefix]) {
isSeeminglyRes = NO;
}
} else if (hasSamePrefix && !hasSameSuffix) {
for (int i = 0; i <= 9; i++) {
NSString *numPrefix = [NSString stringWithFormat:@"%d%@", i, prefixOrSuffix];
if ([res hasSuffix:numPrefix]) {
isSeeminglyRes = NO;
}
}
// 匹配%ld_suffix
NSString *regexStr = [NSString stringWithFormat:@"%%(.*?)%@", prefixOrSuffix];
NSRegularExpression* regexExpression = [NSRegularExpression regularExpressionWithPattern:regexStr options:NSRegularExpressionCaseInsensitive error:nil];
NSArray* matchs = [regexExpression matchesInString:res options:0 range:NSMakeRange(0, res.length)];
if (matchs.count > 0) {
isSeeminglyRes = NO;
}
} else {
return;
}
if (isSeeminglyRes) {
[self.seeminglyResStringSet addObject:prefixOrSuffix];
}
}
最后,在Export點擊的時候,再導出seeminglyResStringSet字段,讓使用者自行檢查、刪除即可,為了方便使用者手工檢查,導出來的資源為不帶后綴名的圖片名字。
NSMutableString *outputResults = [[NSMutableString alloc] init];
NSString *projectPath = [self.pathTextField stringValue];
[outputResults appendFormat:@"Unused Resources In Project: \n%@\n\n", projectPath];
for (ResourceFileInfo *info in self.unusedResults) {
NSArray *strArray = [info.name componentsSeparatedByString:@"."];
[outputResults appendFormat:@"%@ %@\n", [strArray firstObject], [strArray lastObject]];
}
[outputResults appendFormat:@"\n\nSeemingly Unused Results:\n\n"];
for (NSString *seeminglyName in [ResourceStringSearcher sharedObject].seeminglyResStringSet) {
[outputResults appendFormat:@"%@\n", seeminglyName];
}
六. 注意事項
通過這次工程清理,發(fā)現一些容易被誤刪的情況,感覺這個是沒辦法避免的
情況一:中間穿插占位符
比如資源名稱為Hoben_blue_icon,而工程中的引用為[UIImage imageNamed:@"Hoben_%@_icon", @"blue"];
情況二:前后都有數字,且中間的數字才是序號
資源名稱為Hoben_1_640p,工程的引用為[UIImage imageNamed:@"Hoben_%ld_%@", index, @"640p"];
這些情況,都不會匹配工程檢出的字符串,但是他的確是被引用的..所以就算導出來的是不帶序號的圖片,還要自己手動一個個核對一下,看到可疑的圖片,要取部分前綴去尋找,以避免誤刪,雖然占用時間較長,但起碼是個一勞永逸的過程,不影響原有工程的功能才是最重要的。
七. 項目成果
