lemon-cleaner
柠檬清理,开源没几天就获得了3k个点赞,大家对macos
下的清理工具兴趣还是很大的。
github下的地址在这里。
腾讯柠檬清理是针对macOS系统专属制定的清理工具。主要功能包括重复文件和相似照片的识别、软件的定制化垃圾扫描、可视化的全盘空间分析、内存释放、浏览器隐私清理以及设备实时状态的监控等。重点聚焦清理功能,对上百款软件提供定制化的清理方案,提供专业的清理建议,帮助用户轻松完成一键式清理。
lemon-cleaner
的主要代码分了几个部分,我们看两个部分的内容,1.他是怎么找到相同重复文件的,2.开机启动管理。
找重复文件一个好的方法就是文件的md5
。
#pragma mark-
#pragma mark 查找重复文件
// insert file to hash dictionary
// key of dictionary is hash string
// fileList是返回的 array
- (void)insertHashDictionary:(NSArray *)fileList process:(float)process {
@autoreleasepool {
if ([fileList count] < 2)
return;
//resultDict 用 md5做 key,item 做 value
NSMutableDictionary *resultDict = [NSMutableDictionary dictionary];
// 文件大小
uint32 chunkSize = 1024 * 1024;
// 读取前面10MB数据
@try{
// for (QMFileItem *fileItem in fileList) {
//
// }
if ([fileList count] == 0)
return;
// 多线程计算md5(前10m 的数据)
// dispatch_apply 阻塞的 apply 对 sync group 的封装
dispatch_apply(fileList.count, dispatch_get_global_queue(0, 0), ^(size_t index) {
@autoreleasepool{
if (index >= [fileList count]) {
return;
}
QMFileItem *fileItem = [fileList objectAtIndex:index];
if (fileItem.hashStr)
return;
UInt64 fileSize = [fileItem fileSize];
NSFileHandle *innerFileHandle;
innerFileHandle = [NSFileHandle
fileHandleForReadingAtPath:fileItem.filePath];
NSString *md5Str = nil;
@autoreleasepool{
NSMutableData *headData = [NSMutableData dataWithCapacity:MIN(fileSize, 10 * chunkSize)];
// 读取10m 的数据
uint64 offset = 0;
NSData *readData = nil;
do {
@autoreleasepool{
@try {
readData = [innerFileHandle readDataOfLength:chunkSize];
} @catch (NSException *exception) {
NSLog(@"%s, exception = %@", __FUNCTION__, exception);
readData = nil;
} @finally {
}
if (readData)
[headData appendData:readData];
else
break;
offset += [readData length];
[innerFileHandle seekToFileOffset:offset];
}
} while ([readData length] > 0 && offset < 10 * chunkSize);
[innerFileHandle closeFile];
if (headData.length == 0)
return;
if (!self->showDelegate) {
self->_stopped = YES;
return;
}
if (self.stopped)
return;
//计算 md5
md5Str = (__bridge_transfer NSString *) (FileMD5HashWithData(headData, 0));
// 小文件用 md5 做 hash, 大文件用 md5 + crc 做 hash
}
fileSize = [fileItem fileSize];
BOOL bigFile = NO;
if (fileSize > 1024 * 1024 * 10)
bigFile = YES;
else
fileItem.hashStr = md5Str;
// 大文件需要计算crc
if (bigFile) {
// 头部hash和crc作为新的key
dispatch_sync(self->md5Queue, ^{
NSMutableArray *sameHashArray = resultDict[md5Str];
if (sameHashArray == nil) {
sameHashArray = [NSMutableArray arrayWithCapacity:10];
resultDict[md5Str] = sameHashArray;
}
[sameHashArray addObject:fileItem];
});
}
}
});
// 大文件进行crc算法比对
if (resultDict.count > 0) {
for (NSString *md5Str in [resultDict allKeys]) {
NSMutableArray *sameHashArray = resultDict[md5Str];
if ([sameHashArray count] <= 1)
continue;
for (QMFileItem *fileItem in sameHashArray) {
@autoreleasepool{
// 计算文件的crc
NSFileHandle *fileHandle = [NSFileHandle
fileHandleForReadingAtPath:fileItem.filePath];
uLong crc = crc32(0L, Z_NULL, 0);
uint64 offset = 0;
uint32 itemChunkSize = 1024 * 1024; //Read 1M
[fileHandle seekToFileOffset:10 * itemChunkSize];
NSData *data = [fileHandle readDataOfLength:itemChunkSize];
while ([data length] > 0) {
@autoreleasepool {
//Make sure for the next line you choose the appropriate string encoding.
crc = crc32(crc, data.bytes, (uInt) data.length);
/* PERFORM STRING PROCESSING HERE */
/* END STRING PROCESSING */
offset += [data length];
[fileHandle seekToFileOffset:offset];
data = [fileHandle readDataOfLength:itemChunkSize];
if (!showDelegate) {
_stopped = YES;
return;
}
// 显示文件比对进度.
CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
if(now - _lastFileCompareTime >= 0.3){
NSString *dotString = @"";
if(_compareDotNum % 3 == 0){
dotString = @".";
}else if(_compareDotNum % 3 == 1){
dotString = @"..";
}else if(_compareDotNum % 3 == 2){
dotString = @"...";
}
NSString *processString = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"QMDuplicateFiles_insertHashDictionary_NSString_1", nil, [NSBundle bundleForClass:[self class]], @""),[SizeHelper getFileSizeStringBySize:fileItem.fileSize], dotString];
if(showDelegate){
[showDelegate progressRate:process progressStr:processString];
}
_lastFileCompareTime = now;
_compareDotNum ++;
}
if (self.stopped) {
return;
}
}
}
// 头部hash和crc作为新的key
NSString *md5Hash = [NSString stringWithFormat:@"%@_%lu", md5Str, crc];
fileItem.hashStr = md5Hash;
[fileHandle closeFile];
if (self.stopped)
return;
}
}
}
}
}@catch(NSException *exception){
NSLog(@"duplicate file compare error : %@", exception);
}@finally{
}
}
}
获取开机启动项的信息,在界面上添加。
#pragma mark -- Launch service
/**
获取Launch plist数据
*/
- (NSArray *)getLaunchServiceItems {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSMutableArray *launchServiceItemArray = [[NSMutableArray alloc]init];
for (NSString *directoryPath in self.launchServiceFilePathArray) {
NSArray *fileNames = [fileManager contentsOfDirectoryAtPath:directoryPath error:NULL];
for (NSString *fileName in fileNames) {
if ([self needFilterFile:fileName]) {
continue;
}
if ([self.delegate respondsToSelector:@selector(needFilterLaunchServiceFile:)]) {
if ([self.delegate needFilterLaunchServiceFile:fileName]) {
continue;
}
}
NSString *filePath = [directoryPath stringByAppendingPathComponent:fileName];
NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:filePath];
NSString *label = dict[LAUNCH_SERVICE_LABEL];
if (!label || [label isEqualToString:@""]) {
continue;
}
QMAppLaunchItem *launchItem = [[QMAppLaunchItem alloc] initWithLaunchFilePath:filePath itemType:LoginItemTypeService];
launchItem.label = label;
launchItem.isEnable = [self serviceIsEnabledWithItem:launchItem];;
// NSLog(@"%s, launch login itme filePath : %@",__FUNCTION__, filePath);
//将launch plist文件与App进行匹配;
//1. 先通过bundleId进行匹配,如果plist文件名包含bundleId,则匹配成功
//2. 分析plist文件,与其中包含的App进行匹配
QMLocalApp *localApp = [self getAppInfoWithFileName:fileName];
if (localApp) {
launchItem.appName = localApp.appName;
launchItem.appPath = localApp.bundlePath;
} else if(dict) {
NSArray *array = dict[LAUNCH_SERVICE_PROGRAM_ARGUMENTS];
if(array && array.count > 0){
NSString *programPath = array[0];
//判断是否包含app
if (([programPath containsString:@".app/"] || [programPath hasSuffix:@"app"])) {
NSRange range = [programPath rangeOfString:@".app"];
if (range.location != NSNotFound) {
NSUInteger index = range.location + 4;
NSString *appPath = [programPath substringToIndex:index];
launchItem.appPath = appPath;
launchItem.appName = [appPath lastPathComponent];
}
}
}
}
[launchServiceItemArray addObject:launchItem];
}
}
NSLog(@"%s, launchItemArray count : %lu",__FUNCTION__, (unsigned long)launchServiceItemArray.count);
return launchServiceItemArray;
}
添加各个部分:
#pragma mark -- handle data
- (void)handleData {
//以App进行分类:将同一个App的启动项包装成一个LMAppLoginItemInfo,
//LMAppLoginItemInfo中添加launchd service item数据
for (QMBaseLoginItem *loginItem in self.launchItemArray) {
if(loginItem.appName) {
LMAppLoginItemInfo *itemInfo = [self getLoginItemInfoWithAppName:loginItem.appName];
itemInfo.appPath = loginItem.appPath;
[itemInfo addLaunchItem:loginItem];
} else {
//如果没有匹配到App,则归纳为未知应用
if (!self.otherLoginItemInfo) {
NSString *localString = LMLocalizedString(@"LemonLoginItemManagerViewController_unknown_app", self.class);
self.otherLoginItemInfo = [[LMAppLoginItemInfo alloc] initWithAppName:localString];
}
[self.otherLoginItemInfo addLaunchItem:loginItem];
}
}
//添加app login item数据
for (QMBaseLoginItem *loginItem in self.appLoginItemArray) {
LMAppLoginItemInfo *itemInfo = [self getLoginItemInfoWithAppName:loginItem.appName];
itemInfo.appPath = loginItem.appPath;
[itemInfo addAppLoginItem:loginItem];
}
//添加系统登录项数据
for (QMBaseLoginItem *loginItem in self.systemLoginItemArray) {
LMAppLoginItemInfo *itemInfo = [self getLoginItemInfoWithAppName:loginItem.appName];
itemInfo.appPath = loginItem.appPath;
[itemInfo addAppLoginItem:loginItem];
}
//将未知应用添加到数据源末尾
if (self.otherLoginItemInfo) {
[self.dataArray addObject:self.otherLoginItemInfo];
}
for (LMAppLoginItemInfo *itemInfo in self.dataArray) {
NSArray *loginItemDataArray = itemInfo.getLoginItemData;
NSArray *launchItemDataArray = itemInfo.getLaunchItemData;
//统计App启动项的数量
itemInfo.totalItemCount = loginItemDataArray.count + launchItemDataArray.count;
[itemInfo updateEnableStatus];
//统计App启动项类型和数量,以及每类对应的数据
if (loginItemDataArray) {
[itemInfo addAppLoginItemTypeInfoWithArray:loginItemDataArray];
}
if (launchItemDataArray) {
[itemInfo addLaunchItemTypeInfoWithArray:launchItemDataArray];
}
}
}