• Github每日精选(第23期):macOS下的开源清理工具lemon-cleaner


    lemon-cleaner柠檬清理

    lemon-cleaner 柠檬清理,开源没几天就获得了3k个点赞,大家对macos 下的清理工具兴趣还是很大的。

    github下的地址在这里

    在这里插入图片描述
    腾讯柠檬清理是针对macOS系统专属制定的清理工具。主要功能包括重复文件和相似照片的识别、软件的定制化垃圾扫描、可视化的全盘空间分析、内存释放、浏览器隐私清理以及设备实时状态的监控等。重点聚焦清理功能,对上百款软件提供定制化的清理方案,提供专业的清理建议,帮助用户轻松完成一键式清理。

    在这里插入图片描述

    技术模块

    • Lemon:主工程。
    • LemonMonitor:状态栏。
    • LemonBigOldFile:用于大文件清理。
    • LemonDuplicateFile:用于重复文件清理。
    • LemonPhotoClean:用于相似照片清理。
    • LemonUninstaller:用于应用卸载。
    • LemonPrivacyClean:用于隐私清理。
    • LemonLoginItemManager:用于开机启动管理。
    • LemonSpaceAnalyse:用于磁盘空间分析。
    • LemonFileMove:用于文件搬家。
    • LemonHardware:用于硬件信息获取。
    • LemonNetSpeed:用于网络测速。
    • LemonCleaner:用于实际清理操作。

    主要代码分析

    lemon-cleaner 的主要代码分了几个部分,我们看两个部分的内容,1.他是怎么找到相同重复文件的,2.开机启动管理。

    LemonDuplicateFile用于重复文件清理

    找重复文件一个好的方法就是文件的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{
                
            }
           
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    LemonLoginItemManager用于开机启动管理

    获取开机启动项的信息,在界面上添加。

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    添加各个部分:

    #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];
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
  • 相关阅读:
    逍遥自在学C语言 | 位运算符&的高级用法
    与吴恩达创办Coursera后,她一头扎进数字生物学
    Android程序设计之音乐播放器实现
    服务器假死日志按时间统计排查
    C语言:字符串的连接
    Linux编辑器vim
    Spring框架(一)
    【Mybatis小白从0到90%精讲】07:Mybatis 传递参数方式详解
    学习笔记-排序算法
    Leetcode—5.最长回文子串【中等】
  • 原文地址:https://blog.csdn.net/weixin_40425640/article/details/126029587