iOS瘦身——移除無(wú)用資源的LSUnusedResources源碼分析與優(yōu)化

零. 前言

在寫(xiě)完前面一篇LinkMap初探的文章過(guò)后,成功分析出來(lái)的各個(gè)文件占據(jù)的體積大小,然后腦子一個(gè)靈感突然擊中我:既然都分析出來(lái)體積了, 能不能順便幫IPA包瘦瘦身呢,于是我發(fā)現(xiàn)了LSUnusedResources這個(gè)好東西,這個(gè)軟件可以直接拿來(lái)分析出工程中可能沒(méi)用到的資源。

當(dāng)時(shí)沒(méi)想太多,直接拿來(lái)用了,導(dǎo)出來(lái)文件目錄后自己寫(xiě)了個(gè)腳本過(guò)濾了一遍,但后面答辯的時(shí)候,領(lǐng)導(dǎo)問(wèn)我,你有沒(méi)有分析過(guò)這個(gè)源碼,為什么這個(gè)軟件查出來(lái)的無(wú)用圖片,很多是實(shí)際上有用到的,以至于你自己寫(xiě)了一個(gè)腳本再過(guò)濾一遍,我無(wú)言以對(duì)= =

最近肺炎在家也閑得無(wú)聊,沒(méi)什么工作要干,而且這個(gè)源碼是Mac應(yīng)用,也是用OC來(lái)寫(xiě)的,所以我們就來(lái)看看這個(gè)源碼到底是怎么實(shí)現(xiàn)的吧!

這次分析源碼,我會(huì)帶著以下問(wèn)題去進(jìn)行,中間源碼分析可能會(huì)占用一段時(shí)間,如果想直接得到結(jié)論可以翻到四. 后續(xù)分析,但是有些細(xì)節(jié)問(wèn)題還是要通過(guò)源碼分析來(lái)解決:

  1. 這個(gè)源碼是怎么做到查出所有的圖片資源的?畢竟圖片資源有很多,格式也不一樣。

  2. 這個(gè)源碼又是怎么看出該圖片有沒(méi)有被引用的?畢竟不同后綴名的文件的引用方法不同,如.m是imageNamed:@"",.xib則是image name="",等等。

  3. 那為什么這個(gè)工程的誤刪率這么高,還要我自己寫(xiě)個(gè)腳本再過(guò)濾一遍?

一. 簡(jiǎn)介

首先看看這個(gè)界面長(zhǎng)啥樣

此軟件分為三部分,第一部分是查找文件的目錄,第二部分是查找規(guī)則,第三部分是查找結(jié)果,因?yàn)槲覀冞@次是為了探索這個(gè)軟件是怎么過(guò)濾無(wú)用圖片的,所以重點(diǎn)分析一下第二部分和第三部分。

首先來(lái)看文件架構(gòu):

二. 找出所有的圖片資源

這部分主要是根據(jù)使用者輸入的Project PathExclude Folder,在directoryPath內(nèi),搜索出filetype類(lèi)型的所有路徑名,filetype就是上面Resource Suffix的輸入內(nèi)容,即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í)行下來(lái),就可以得到一個(gè)所有圖片后綴的目錄數(shù)組,實(shí)現(xiàn)原理是用了pipe之類(lèi)的,這里我不打算深究,只需要知道他的目的就可以了。

獲取到所有圖片的文件路徑后,下面的代碼對(duì)本身在.imageset、.launchimage、.appiconset、.bundle文件夾里面的圖片進(jìn)行過(guò)濾。

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];
        }
    }

對(duì)這些圖片信息進(jìn)行遍歷,并且形成了一個(gè)個(gè)的ResourceFileInfo類(lèi),并用了以名字為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;
    }
    
    // 獲得文件名稱(chēng)
    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;
    }
}

自此,我們就獲得了該工程里面的所有圖片資源信息,他被存儲(chǔ)在resNameInfoDict這個(gè)字典信息中。

但是好戲才剛剛開(kāi)始,因?yàn)槲覀円獙?duì)這些資源進(jìn)行遍歷,看看哪些資源是被工程引用的,而哪些工程是真正可以刪去的。

三. 驗(yàn)證資源是否有被引用

iOS工程中用到的后綴文件各式各樣,.h .m .swift .xib,甚至有用到.html的,他們引用資源的方式各不相同,怎么知道圖片有沒(méi)有被這些文件引用,的確是個(gè)難題。

// 要查找的后綴名信息
@interface ResourceStringPattern : NSObject

@property (strong, nonatomic) NSString *suffix; // 查找后綴名,如.h/.m等
@property (assign, nonatomic) BOOL enable;      // 是否勾選
@property (strong, nonatomic) NSString *regex;  // 對(duì)應(yīng)的正則表達(dá)式
@property (assign, nonatomic) NSInteger groupIndex;  // 第幾組

// 將dict轉(zhuǎn)換成該類(lèi)方法
- (id)initWithDictionary:(NSDictionary *)dict;

@end

可以看到,每個(gè)后綴文件的結(jié)構(gòu)體有個(gè)最關(guān)鍵的東西,那就是正則表達(dá)式,這可是驗(yàn)證資源是否被引用的利器。

1. 正則表達(dá)式

魯迅說(shuō)過(guò),正則表達(dá)式是個(gè)好東西,雖然他長(zhǎng)得不太順眼,但是能用好他的話(huà),一行甚至就能達(dá)到幾十行代碼的效果,在這個(gè)界面中,每個(gè)后綴名都有自己對(duì)應(yīng)的正則表達(dá)式。

沒(méi)有接觸過(guò)正則表達(dá)式的我,只能靠一本字典一個(gè)驗(yàn)證器來(lái)一點(diǎn)點(diǎn)探索了,過(guò)程有點(diǎn)艱苦= =

這個(gè)軟件會(huì)默認(rèn)生成一堆后綴名和他們對(duì)應(yīng)的正則表達(dá)式,使用的時(shí)候我們可以直接用,但是要是想看看他的工作機(jī)制,那么還是看一下每一個(gè)正則的意思吧。

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

下面來(lái)對(duì)每個(gè)格式的正則表達(dá)式進(jìn)行分析:


  • C格式(.h .c .cpp .css)

正則表達(dá)式:([a-zA-Z0-9_-]*)\.(imageset|jpg|gif|png)

意義:以.為界限,.前面可以是任意字母、數(shù)字、下劃線(xiàn)或者橫線(xiàn),.后面是上述四種格式之一。個(gè)人認(rèn)為應(yīng)該可以加多個(gè)@,表示@2x,@3x也可以匹配到。

這里.h應(yīng)該用objc的,因?yàn)?h文件有可能是這樣的

#define HobenImageStr [UIImage imageNamed:@"Hoben"]
  • OC格式(.m .mm)

正則表達(dá)式:@"(.*?)"

意義:匹配@"xxx",xxx可為任意內(nèi)容(引號(hào)除外)

  • xib格式(.xib .storyboard)

正則表達(dá)式:image name="(.+?)"

意義:image name="xxx",xxx不能為空且他們之間不能有換行符

  • swift格式

正則表達(dá)式:"(.*?)"

意義:匹配"xxx",xxx可為任意內(nèi)容(引號(hào)除外)

  • strings格式(InfoPlist.strings,感覺(jué)很少用到。。)

正則表達(dá)式:=\s*"(.*)"\s*;

意義:= (中間任意空格) "xxx" (中間任意空格) ; 其中=、"";不可缺少

  • html格式

正則表達(dá)式:img\s+src=["'](.*?)["']

意義:img(中間至少一個(gè)空格)src="xxx"

或者:img(中間至少一個(gè)空格)src=‘xxx’

  • js格式

正則表達(dá)式:["']src["'],\s+["'](.*?)["']

意義:"src",(中間至少一個(gè)空格)"xxx"

或者:'src',(中間至少一個(gè)空格)'xxx'

  • json格式

正則表達(dá)式::\s*"(.*?)"

意義::(中間任意空格)"xxx"

  • plist格式

正則表達(dá)式:>(.*?)<

意義:>xxx<,xxx可為空,應(yīng)該是對(duì)應(yīng)<string>xxx</string>的情況


分析完這些正則之后,總感覺(jué)有點(diǎn)怪怪的感覺(jué),比如objcPattern里面,只要符合@"xxx",就會(huì)被視為是有用的資源了,而html格式src="xxx",感覺(jué)中間可以插入若干空格,會(huì)不會(huì)也有部分資源因?yàn)椴黄ヅ溥@個(gè)而視為無(wú)用資源。當(dāng)然這只是我分析完之后的疑問(wèn),我們先繼續(xù)往下看。

2. 開(kāi)始掃描

在前面,我們獲取到了所有資源的名稱(chēng)、后綴、路徑、大小,也獲取到了所有后綴文件和他們的掃描規(guī)則(當(dāng)然這里是可以自己設(shè)置的,是否掃描某個(gè)后綴,掃描規(guī)則,掃描的資源類(lèi)型后綴),下面我們就可以開(kāi)始進(jìn)行掃描了!

- (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 {
            // 是文件了,則獲取其后綴名進(jìn)行相應(yīng)正則掃描
            // 以/Users/xxx/File.m為例
            // ext就是m
            // resourcePattern就是m對(duì)應(yīng)的結(jié)構(gòu)體(包含正則表達(dá)式)
            // 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;
}

掃描文件用了遞歸的方式,直到這個(gè)文件路徑屬于文件,且后綴名是可以處理的,才會(huì)對(duì)其進(jìn)行處理。

現(xiàn)在,我們有路徑名和其對(duì)應(yīng)的正則處理方法了,下面可以根據(jù)路徑名獲得文件內(nèi)容(即下面的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) {
        // 找出所有符合正則的關(guān)鍵詞
        NSMutableSet *set = [NSMutableSet set];
        for (NSTextCheckingResult *checkingResult in matchs) {
            // 根據(jù)正則表達(dá)式的第index(默認(rèn)1)個(gè)分組來(lái)獲取
            // 且只獲取名字,即@2x/@3x這些會(huì)被隱去
            // 比如對(duì)于.h文件,其正則為([a-zA-Z0-9@_-]*)\.(imageset|jpg|gif|png)
            // 則匹配到了Hoben@2x.jpg字符之后,經(jīng)過(guò)下面的處理后獲取到的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方法,找了很久的博客,終于找到了這篇對(duì)方法的說(shuō)明這篇對(duì)group的說(shuō)明,原來(lái)是和正則里面的group是一個(gè)意思...

于是,我們通過(guò)遍歷了工程中所有的圖片,也獲得了一個(gè)被使用的資源列表resStringSet。

開(kāi)始根據(jù)這個(gè)規(guī)則來(lái)遍歷:

- (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;
}

該軟件還提供了一個(gè)近似名字檢索的功能:Ignore similar name (eg: tag_1.png, using with "tag_%d" or "tag" will be considered to be used )

這個(gè)近似檢索使用的正則表達(dá)式為([-_]?\d+),意思是,如果一張圖片的名字前面或后面跟著_或者-,且后面有至少一個(gè)數(shù)字,則可以被視為近似名字,比較近似名字的方法是取前綴/后綴,再進(jìn)行比較。

- (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;
}

了解上面三個(gè)關(guān)鍵的比較方法之后,就可以開(kāi)始遍歷了:

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];
            }
        }
    }
}

此時(shí)得到的unusedResults即為經(jīng)過(guò)了檢驗(yàn)的無(wú)用資源,后面的導(dǎo)出和刪除就略過(guò)不講了。

四. 后續(xù)分析

終于分析完源碼和他的工作機(jī)制了!下面來(lái)談?wù)勚八伎嫉膸讉€(gè)問(wèn)題。

  1. 這個(gè)源碼是怎么做到查出所有的圖片資源的?

思路倒是挺簡(jiǎn)單的,遞歸遍歷,直到遍歷到后綴名為預(yù)存的圖片后綴,取他的名字生成圖片資源類(lèi),放進(jìn)數(shù)組即可。

  1. 這個(gè)源碼又是怎么看出該圖片有沒(méi)有被引用的?畢竟不同后綴名的文件的引用方法不同,如.m是imageNamed:@"",.xib則是image name="",等等。

一個(gè)后綴名對(duì)應(yīng)一個(gè)正則表達(dá)式,如.m文件匹配@"",.xib文件匹配image name="",在掃描該后綴名的文件時(shí),取出對(duì)應(yīng)正則,對(duì)上面獲得的圖片資源進(jìn)行正則匹配,匹配得出來(lái)就是有用的資源啦!

  1. 那為什么這個(gè)工程的誤刪率這么高,還要我自己寫(xiě)個(gè)腳本再過(guò)濾一遍?

這個(gè)問(wèn)題是因?yàn)槲覜](méi)勾選相似圖片忽略,導(dǎo)致很多帶數(shù)字的圖片,如image_1,他就在原工程上面查不到,因?yàn)樵こ淌沁@樣的:[UIImage imageNamed:@"image_%ld", 1];

勾選了之后,看了看,的確誤刪率沒(méi)那么高了。

五. 源碼優(yōu)化

雖然他刪除的文件為幾M,但是刪除后生成的IPA包也是可以瘦身十多M的,的確是能正確刪除文件的,看來(lái)這個(gè)開(kāi)源項(xiàng)目勾選了之后的確好用,但還是會(huì)有些有可能漏刪的情況,下面這種情況,原邏輯就可能會(huì)錯(cuò)過(guò)資源:

圖片名字為Hoben.jpg、Hoben_1.jpg...Hoben_10.jpg,工程中引用了Hoben,但是對(duì)于之后的連續(xù)圖片,沒(méi)有引用了,按照原邏輯取下劃線(xiàn)前綴的做法,則會(huì)取到前綴Hoben,誤以為帶序號(hào)的圖片也被引用了,從而導(dǎo)致漏刪。

思路:Hoben_1.jpg如果需要被主工程引用,則有以下幾種調(diào)用方法:

// 方法一:
[UIImage imageNamed:@"Hoben_1"]

// 方法二
[UIImage imageNamed:@"Hoben_%ld", 1]

// 方法三
NSString *str = @"Hoben"
[UIImage imageNamed:@"%@_%ld", str, 1]

對(duì)于方法一和方法二,我們完全可以根據(jù)Hoben_為前綴,再判斷后面是否跟著數(shù)字或者百分號(hào)即可,但是,我們并不可以完全以這個(gè)為依據(jù)判斷這個(gè)為無(wú)用資源,因?yàn)榉椒ㄈ€是可能會(huì)被調(diào)用的,所以,我們加個(gè)定義,這個(gè)為疑似無(wú)用資源,即需要使用者根據(jù)導(dǎo)出的文件,自行判斷是否需要?jiǎng)h掉。

如果序號(hào)在前,如1_Hoben.jpg,則調(diào)用方法可能為以下情況

// 方法一:
[UIImage imageNamed:@"1_Hoben"]

// 方法二
[UIImage imageNamed:@"%ld_Hoben", 1]

// 方法三
NSString *str = @"Hoben"
[UIImage imageNamed:@"%ld_%@", 1, str]

如果為序號(hào)在前的圖片,則需要用正則來(lái)判斷了:%(.*?)_Hoben,這個(gè)正則的意思是百分號(hào)和前綴之間可以有任意字符。

不過(guò)由于序號(hào)在前還是比較少見(jiàn)的,如果一個(gè)圖片有多處序號(hào),如Hoben2017_1,則我們還是默認(rèn)取Hoben2017_作為前綴,這個(gè)處理方法可能會(huì)漏掉序號(hào)在前的圖片,但大多數(shù)圖片還是可以檢測(cè)出來(lái)的。

綜上,我們可以改一下源碼,來(lái)揪出一些可能被漏刪的無(wú)用資源,首先是要對(duì)判斷方法的修改,將正則改為(\\d+),無(wú)需加入下劃線(xiàn)判斷了,其次,由于可能匹配多處數(shù)字,所以匹配的次數(shù)不一定只有1個(gè),可以為多個(gè)且取最后一個(gè)匹配。

但考慮到第三種情況的調(diào)用,為了避免誤刪,刪除的時(shí)候還是要去除下劃線(xiàn)來(lái)檢測(cè),帶下劃線(xiàn)的歸入疑似圖片,讓使用者自行判斷是否需要?jiǎng)h除

- (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;
}

檢查疑似無(wú)用資源方法如下:

// 用于檢測(cè)是否為疑似無(wú)用資源
- (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點(diǎn)擊的時(shí)候,再導(dǎo)出seeminglyResStringSet字段,讓使用者自行檢查、刪除即可,為了方便使用者手工檢查,導(dǎo)出來(lái)的資源為不帶后綴名的圖片名字。

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];
}

六. 注意事項(xiàng)

通過(guò)這次工程清理,發(fā)現(xiàn)一些容易被誤刪的情況,感覺(jué)這個(gè)是沒(méi)辦法避免的

情況一:中間穿插占位符

比如資源名稱(chēng)為Hoben_blue_icon,而工程中的引用為[UIImage imageNamed:@"Hoben_%@_icon", @"blue"];

情況二:前后都有數(shù)字,且中間的數(shù)字才是序號(hào)

資源名稱(chēng)為Hoben_1_640p,工程的引用為[UIImage imageNamed:@"Hoben_%ld_%@", index, @"640p"];

這些情況,都不會(huì)匹配工程檢出的字符串,但是他的確是被引用的..所以就算導(dǎo)出來(lái)的是不帶序號(hào)的圖片,還要自己手動(dòng)一個(gè)個(gè)核對(duì)一下,看到可疑的圖片,要取部分前綴去尋找,以避免誤刪,雖然占用時(shí)間較長(zhǎng),但起碼是個(gè)一勞永逸的過(guò)程,不影響原有工程的功能才是最重要的。

七. 項(xiàng)目成果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容