iOS瘦身——移除无用资源的LSUnusedResources源码分析与优化

零. 前言

在写完前面一篇LinkMap初探的文章过后,成功分析出来的各个文件占据的体积大小,然后脑子一个灵感突然击中我:既然都分析出来体积了, 能不能顺便帮IPA包瘦瘦身呢,于是我发现了LSUnusedResources这个好东西,这个软件可以直接拿来分析出工程中可能没用到的资源。

当时没想太多,直接拿来用了,导出来文件目录后自己写了个脚本过滤了一遍,但后面答辩的时候,领导问我,你有没有分析过这个源码,为什么这个软件查出来的无用图片,很多是实际上有用到的,以至于你自己写了一个脚本再过滤一遍,我无言以对= =

最近肺炎在家也闲得无聊,没什么工作要干,而且这个源码是Mac应用,也是用OC来写的,所以我们就来看看这个源码到底是怎么实现的吧!

这次分析源码,我会带着以下问题去进行,中间源码分析可能会占用一段时间,如果想直接得到结论可以翻到四. 后续分析,但是有些细节问题还是要通过源码分析来解决:

  1. 这个源码是怎么做到查出所有的图片资源的?毕竟图片资源有很多,格式也不一样。

  2. 这个源码又是怎么看出该图片有没有被引用的?毕竟不同后缀名的文件的引用方法不同,如.m是imageNamed:@"",.xib则是image name="",等等。

  3. 那为什么这个工程的误删率这么高,还要我自己写个脚本再过滤一遍?

一. 简介

首先看看这个界面长啥样

此软件分为三部分,第一部分是查找文件的目录,第二部分是查找规则,第三部分是查找结果,因为我们这次是为了探索这个软件是怎么过滤无用图片的,所以重点分析一下第二部分和第三部分。

首先来看文件架构:

二. 找出所有的图片资源

这部分主要是根据使用者输入的Project PathExclude 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;
}

上面这块代码一执行下来,就可以得到一个所有图片后缀的目录数组,实现原理是用了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",感觉中间可以插入若干空格,会不会也有部分资源因为不匹配这个而视为无用资源。当然这只是我分析完之后的疑问,我们先继续往下看。

2. 开始扫描

在前面,我们获取到了所有资源的名称、后缀、路径、大小,也获取到了所有后缀文件和他们的扫描规则(当然这里是可以自己设置的,是否扫描某个后缀,扫描规则,扫描的资源类型后缀),下面我们就可以开始进行扫描了!

- (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]) {
            // 还是文件夹,继续找里面的文件
            [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

开始根据这个规则来遍历:

- (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即为经过了检验的无用资源,后面的导出和删除就略过不讲了。

四. 后续分析

终于分析完源码和他的工作机制了!下面来谈谈之前思考的几个问题。

  1. 这个源码是怎么做到查出所有的图片资源的?

思路倒是挺简单的,递归遍历,直到遍历到后缀名为预存的图片后缀,取他的名字生成图片资源类,放进数组即可。

  1. 这个源码又是怎么看出该图片有没有被引用的?毕竟不同后缀名的文件的引用方法不同,如.m是imageNamed:@"",.xib则是image name="",等等。

一个后缀名对应一个正则表达式,如.m文件匹配@"",.xib文件匹配image name="",在扫描该后缀名的文件时,取出对应正则,对上面获得的图片资源进行正则匹配,匹配得出来就是有用的资源啦!

  1. 那为什么这个工程的误删率这么高,还要我自己写个脚本再过滤一遍?

这个问题是因为我没勾选相似图片忽略,导致很多带数字的图片,如image_1,他就在原工程上面查不到,因为原工程是这样的:[UIImage imageNamed:@"image_%ld", 1];

勾选了之后,看了看,的确误删率没那么高了。

五. 源码优化

虽然他删除的文件为几M,但是删除后生成的IPA包也是可以瘦身十多M的,的确是能正确删除文件的,看来这个开源项目勾选了之后的确好用,但还是会有些有可能漏删的情况,下面这种情况,原逻辑就可能会错过资源:

图片名字为Hoben.jpgHoben_1.jpg...Hoben_10.jpg,工程中引用了Hoben,但是对于之后的连续图片,没有引用了,按照原逻辑取下划线前缀的做法,则会取到前缀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];
}

六. 注意事项

通过这次工程清理,发现一些容易被误删的情况,感觉这个是没办法避免的

情况一:中间穿插占位符

比如资源名称为Hoben_blue_icon,而工程中的引用为[UIImage imageNamed:@"Hoben_%@_icon", @"blue"];

情况二:前后都有数字,且中间的数字才是序号

资源名称为Hoben_1_640p,工程的引用为[UIImage imageNamed:@"Hoben_%ld_%@", index, @"640p"];

这些情况,都不会匹配工程检出的字符串,但是他的确是被引用的..所以就算导出来的是不带序号的图片,还要自己手动一个个核对一下,看到可疑的图片,要取部分前缀去寻找,以避免误删,虽然占用时间较长,但起码是个一劳永逸的过程,不影响原有工程的功能才是最重要的。

七. 项目成果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容