IOS 解决问题:相似、截屏照片清理和图片压缩

原创:问题解决型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 公用部分
    • 1、ClearPhotoManager文件
      • a、ClearPhotoManager文件中公共的属性和方法
      • b、ClearPhotoManager单例
      • c、加载照片方法的实现
      • d、删除图片资源方法的实现
      • e、弹出提示框方法的实现
      • f、相册变化状态的通知
  • 2、创建主视图
    • Model:ClearPhotoItem
    • View:ClearPhotoCell
    • ViewController:ClearPhotoViewController
  • 一、清理相似图片
    • 1、运行效果
    • 2、ClearPhotoManager文件
    • 3、创建视图
  • 二、清理截屏图片
    • 1、运行效果
    • 2、ClearPhotoManager文件
    • 3、创建视图
  • 三、压缩图片
    • 1、运行效果
    • 2、ClearPhotoManager文件
    • 3、创建视图
  • 四、内存泄漏问题的修复
    • 1、内存泄漏问题
    • 2、内存泄漏问题的原因
    • 3、暂时的解决方案
    • 4、期待前辈们完善修复方案
  • Demo
  • 参考文献

公用部分

1、ClearPhotoManager文件

用来提供相册的清理相似图片、清理截屏图片、压缩图片三个功能。

额外需要引入的框架:

#import 

Photos引用了系统的AssetsLibrary框架,需要额外导入,否则会报错:

Undefined symbols for architecture x86_64:
  "_ALAssetPropertyAssetURL", referenced from:
      -[Photos]......
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

引入方式:

a、ClearPhotoManager文件中公共的属性和方法
// 获取相簿中的所有PHAsset对象
@property (nonatomic, strong) PHFetchResult *assetArray;
// 获取相簿中的上一张图片资源
@property (nonatomic, strong) PHAsset *lastAsset;
// 上一张图片的缩略图
@property (nonatomic, strong) UIImage *lastExactImage;
// 上一张图片的原图数据
@property (nonatomic, strong) NSData *lastOriginImageData;
// 上一张图片资源和当前图片资源是否是相似图片
@property (nonatomic, assign) BOOL isLastSame;

/// 加载照片
- (void)loadPhotoWithProcess:(void (^)(NSInteger current, NSInteger total))process completionHandler:(void (^)(BOOL success, NSError *error))completion;

/// 删除照片
+ (void)deleteAssets:(NSArray *)assets completionHandler:(void (^)(BOOL success, NSError *error))completion;

/// 确定提示框
+ (void)tipWithMessage:(NSString *)str;
b、ClearPhotoManager单例

作用是无需创建实例,通过类方法直接调用。

#pragma mark - 单例
+ (ClearPhotoManager *)shareManager
{
    static ClearPhotoManager *clearPhotoManager = nil;
    static dispatch_once_t token;
    dispatch_once(&token, ^{
        clearPhotoManager = [[ClearPhotoManager alloc] init];
    });
    return clearPhotoManager;
}
c、加载照片方法的实现

在这个方法中加载的图片包括相似图、截屏图以及可瘦身的图片,所以是公共部分。

第一步:加载照片之前首先要清除旧数据。

// 加载照片之前先清除旧数据
- (void)resetTagData
{
    // 重置相似图片
    self.similarArray = nil;
    self.similarInfo = nil;
    self.similarSaveSpace = 0;
    
    // 重置屏幕截图
    self.screenshotsArray = nil;
    self.screenshotsInfo = nil;
    self.screenshotsSaveSpace = 0;
    
    // 重置瘦身图片
    self.thinPhotoArray = nil;
    self.thinPhotoInfo = nil;
    self.thinPhotoSaveSpace = 0;
    
    // 总共节省的空间
    self.totalSaveSpace = 0;
}

第二步:判断相册授权状态。如果相册授权状态为没决定,则开启权限提示,在info.plist中添加上Privacy - Photo Library Usage Description,提示语句可为:获取相册权限。

// 加载图片
- (void)loadPhotoWithProcess:(void (^)(NSInteger, NSInteger))process completionHandler:(void (^)(BOOL, NSError * _Nonnull))completion
{
    // 清除旧数据
    [self resetTagData];
    
    // 将传入的处理过程的block实现赋值给它
    self.processHandler = process;
    // 将传入的完成过程的block实现赋值给它
    self.completionHandler = completion;
    
    // 获取当前App的相册授权状态
    PHAuthorizationStatus authorizationStatus = [PHPhotoLibrary authorizationStatus];
    // 判断授权状态
    if (authorizationStatus == PHAuthorizationStatusAuthorized)
    {
        // 如果已经授权, 获取图片
        [self getAllAsset];
    }
    // 如果没决定, 弹出指示框, 让用户选择
    else if (authorizationStatus == PHAuthorizationStatusNotDetermined)
    {
        [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
            // 如果用户选择授权, 则获取图片
            if (status == PHAuthorizationStatusAuthorized)
            {
                // 获取相簿中的PHAsset对象
                [self getAllAsset];
            }
        }];
    }
    else
    {
        // 开启权限提示
        [self noticeAlert];
    }
}

如果相册授权状态为拒绝,则弹出提示框,点击前往设置则跳转到设置APP开启权限。

// 开启权限提示
- (void)noticeAlert
{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"此功能需要相册授权" message:@"请您在设置系统中打开授权开关" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *left = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
    UIAlertAction *right = [UIAlertAction actionWithTitle:@"前往设置" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        // 打开设置APP
        NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
        [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
    }];
    [alert addAction:left];
    [alert addAction:right];
    UIViewController *vc = [UIApplication sharedApplication].keyWindow.rootViewController;
    [vc presentViewController:alert animated:YES completion:nil];
}

第三步:如果已经授权则直接获取相册中的数据。

// 如果已经授权, 获取相簿中的所有PHAsset对象
- (void)getAllAsset
{
    // 获取所有资源的集合,并按资源的创建时间排序,这样就可以通过和上一张图片判断日期来分组了
    PHFetchOptions *options = [[PHFetchOptions alloc] init];
    options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
    PHFetchResult *result = [PHAsset fetchAssetsWithOptions:options];
    self.assetArray = result;
    
    // 最初从第一张图片,数组中位置0的图片开始获取
    [self requestImageWithIndex:0];
}

第四步:requestImageWithIndex:方法通过图片索引位置来获取assetArray中对应的图片。这个方法在index+1后不断递归调用自己,直到遍历完整个assetArray。传入缩略图和原图后,即进入了本文章的关键部分,即图片的处理流程了。

// 获取图片: index表示正在获取第几张图片,即图片索引位置
- (void)requestImageWithIndex:(NSInteger)index
{
    // 获取图片的过程:当前正在获取第几张图片,总共有多少张
    // 调用Block,传入index和total,计算进度
    if (self.processHandler)
    {
        self.processHandler(index, self.assetArray.count);
    }
    
    // 这个方法会一直+1后递归调用,直到结束条件,即已经获取到最后一张了
    if (index >= self.assetArray.count)
    {
        // 加载完成
        [self loadCompletion];
        // 完成的回调,没有错误,成功了
        self.completionHandler(YES, nil);
        return;
    }
    
    // 筛选本地图片,过滤视频、iCloud图片
    PHAsset *asset = self.assetArray[index];// 根据索引拿到对应位置图片资源
    if (asset.mediaType != PHAssetMediaTypeImage || asset.sourceType != PHAssetSourceTypeUserLibrary)// 不是图片类型或者不是相册
    {
        // 略过,直接获取下一个资源
        [self requestImageWithIndex:index + 1];
        return;
    }
    
    PHImageManager *imageManager = [PHImageManager defaultManager];
    __weak typeof(self) weakSelf = self;
    // 获取压缩大小后的图片,即缩略图
    [imageManager requestImageForAsset:asset targetSize:CGSizeMake(125, 125) contentMode:PHImageContentModeDefault options:self.imageRequestOptions resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
        
        // 获取原图(原图大小)
        [imageManager requestImageDataAndOrientationForAsset:self.assetArray[index] options:self.imageSizeRequestOptions resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, CGImagePropertyOrientation orientation, NSDictionary * _Nullable info) {
            
            // 处理图片,分别传入缩略图和原图
            [weakSelf dealImageWithIndex:index exactImage:result originImageData:imageData];
        }];
    }];
}

第五步:图片的处理方法dealImageWithIndex:对传入的缩略图和原图进行三步处理,分别判断其是否为相似图片、截屏图片、可以瘦身的图片,如果是则将其加入到对应数组中保存作为数据源。处理完一张图片后将index+1,再调用requestImageWithIndex:处理下一张图片,这是个递归过程,直到全部图片处理完成。

// 处理图片,获取到需要清理的相似图片和截屏图片,以及可以瘦身的图片
- (void)dealImageWithIndex:(NSInteger)index exactImage:(UIImage *)exactImage originImageData:(NSData *)originImageData
{
    NSLog(@"原图大小为:%.2fM,而缩率图尺寸为:%@",originImageData.length/1024.0/1024.0, NSStringFromCGSize(exactImage.size));
    
    // 将相册中最后一个图片资源和目前的图片资源的日期进行比较,看是否是同一天
    // 资源的集合中按资源的创建时间排序
    PHAsset *asset = self.assetArray[index];
    BOOL isSameDay = [self isSameDay:self.lastAsset.creationDate date2:asset.creationDate];
    
    // 1:该图片是相似图片吗
    ...
    
    // 2:该图片是截屏图片吗
    ...
    
    // 3:该图片是否可以瘦身
    ...
    
    // 处理完后变为上一个
    self.lastAsset = asset;
    self.lastExactImage = exactImage;// 缩略图
    self.lastOriginImageData = originImageData;// 原图
    
    // 获取下一张图片
    [self requestImageWithIndex:index + 1];
}

因为图片资源数组assetArray是按照创建时间排序的,所以可以通过和上一个图片的创建时间相比较来对图片按照是否为同一天创建的标准来分组显示。如果是同一天创建的图片,则将其分为一组。是否为同一天的比较方法如下:

// 是否为同一天
- (BOOL)isSameDay:(NSDate *)date1 date2:(NSDate *)date2
{
    // 有一个日期为空则直接返回
    if (!date1 || !date2)
    {
        return NO;
    }
    
    // 从日历上分别获取date1、date2的年月日
    NSCalendar *calendar = [NSCalendar currentCalendar];
    unsigned unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay;
    NSDateComponents *dateComponents1 = [calendar components:unitFlags fromDate:date1];
    NSDateComponents *dateComponents2 = [calendar components:unitFlags fromDate:date2];
    
    // 比较年月日,均相同则返回YES,否则不是同一天
    return (dateComponents1.day == dateComponents2.day) && (dateComponents1.month == dateComponents2.month) && (dateComponents1.year == dateComponents2.year);
}

第六步:图片加载完成,计算相似、截屏、可瘦身的图片的数量和可节省内存的大小,最后将各类可节省内存的大小加起来得到总的可节省内存的大小。

// 加载完成
- (void)loadCompletion
{
    // similarInfo存储了相似图片数量及可以节省的内存空间大小
    self.similarInfo = [self getInfoWithDataArray:self.similarArray saveSpace:self.similarSaveSpace];
    
    // screenshotsInfo存储了屏幕截图数量及可以节省的内存空间大小
    self.screenshotsInfo = [self getInfoWithDataArray:self.screenshotsArray saveSpace:self.screenshotsSaveSpace];
    
    // thinPhotoInfo存储了瘦身图片数量及可以节省的内存空间大小
    self.thinPhotoInfo = @{@"count" : @(self.thinPhotoArray.count), @"saveSpace" : @(self.thinPhotoSaveSpace)};
    
    // 总的可以节省内存的大小
    self.totalSaveSpace = self.similarSaveSpace + self.self.thinPhotoSaveSpace + self.screenshotsSaveSpace;
    
    NSLog(@"删掉相似照片可省 :%.2fMB", self.similarSaveSpace / 1024.0 / 1024.0);
    NSLog(@"删掉屏幕截图可省 :%.2fMB", self.screenshotsSaveSpace / 1024.0 / 1024.0);
    NSLog(@"压缩照片可省 :%.2fMB", self.thinPhotoSaveSpace / 1024.0 / 1024.0);
    
    NSLog(@"图片加载全部完成");
}

其中获取图片数量及可以节省的内存空间大小的getInfoWithDataArray:方法的实现如下:

// 获取图片数量及可以节省的内存空间大小
- (NSDictionary *)getInfoWithDataArray:(NSArray *)dataArray saveSpace:(NSUInteger)saveSpace
{
    NSUInteger similarCount = 0;
    for (NSDictionary *dictionary in dataArray)// 每个字典代表了一个日期下的相似数组
    {
        // 将最后的字典作为数组的一个元素进行初始化
        NSArray *array = dictionary.allValues.lastObject;
        similarCount = similarCount + array.count;
    }
    return @{@"count":@(similarCount), @"saveSpace" : @(saveSpace)};
}
d、删除图片资源方法的实现
// 删除照片
+ (void)deleteAssets:(NSArray *)assets completionHandler:(void (^)(BOOL, NSError * _Nonnull))completion
{
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        // 删除当前图片资源
        [PHAssetChangeRequest deleteAssets:assets];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        // 调用删除后的回调代码块
        if (completion)
        {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(success, error);
            });
        }
    }];
}
e、弹出提示框方法的实现
// 确定提示框
+ (void)tipWithMessage:(NSString *)str
{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:str preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil];
    [alert addAction:action];
    
    UIViewController *vc = [UIApplication sharedApplication].keyWindow.rootViewController;
    [vc presentViewController:alert animated:YES completion:nil];
}
f、相册变化状态的通知

ClearPhotoManager中定义:

typedef NS_ENUM(NSInteger, PhotoNotificationStatus)
{
    PhotoNotificationStatusDefualt  = 0, // 相册变更默认处理
    PhotoNotificationStatusClose    = 1, // 相册变更不处理
    PhotoNotificationStatusNeed     = 2, // 相册变更主动处理
};

@protocol ClearPhotoManagerDelegate 

@optional
/// 相册变动代理方法
- (void)clearPhotoLibraryDidChange;

@end

/// 代理
@property (nonatomic, weak) id delegate;

/// 变更状态
@property (nonatomic, assign) PhotoNotificationStatus notificationStatus;
#pragma mark - 相册变换通知

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        // 相册变换通知
        [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
    }
    return self;
}

- (void)dealloc
{
    // 移除相册变换通知
    [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}

// 相册变换时候会调用
- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
    // 筛选出没必要的变动
    PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetArray];
    if (collectionChanges == nil || self.notificationStatus != PhotoNotificationStatusDefualt)
    {
        return;
    }
    
    // 回到主线程调用相册变动代理方法
    dispatch_async(dispatch_get_main_queue(), ^{
        if ([self.delegate respondsToSelector:@selector(clearPhotoLibraryDidChange)])
        {
            [self.delegate clearPhotoLibraryDidChange];
        }
    });
}

当我们在相册新增加了一张图片的时候,控制台输出如下:

 before= count=6, after= count=7, hasIncremental=1 deleted=(null), inserted=[number of indexes: 1 (in 1 ranges), indexes: (0)], changed=(null), hasMoves=0

Printing description of changeInstance: 

ClearPhotoViewController中使用:页面显示前或者相册发生变动了则更新数据源

@interface ClearPhotoViewController ()

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    // 相册变更主动处理
    if (self.clearPhotoManager.notificationStatus == PhotoNotificationStatusNeed)
    {
        // 加载照片数据源
        [self loadPhotoData];
        // 重置为相册变更默认处理
        self.clearPhotoManager.notificationStatus = PhotoNotificationStatusDefualt;
    }
}

#pragma mark - ClearPhotoManagerDelegate

// 相册变动代理方法
- (void)clearPhotoLibraryDidChange
{
    // 加载照片数据源
    [self loadPhotoData];
}

SimilarPhotoAndScreenShotsViewController中使用:删除后通知更新相册和数据源

        // 相册变更不处理
        [ClearPhotoManager shareManager].notificationStatus = PhotoNotificationStatusClose;
        // 删除选中资源
        [ClearPhotoManager deleteAssets:assetArray completionHandler:^(BOOL success, NSError * _Nonnull error) {
            // 实现删除成功的block
            if (success)
            {
                // 更新删除选中后保留下来的未选中的数据源
                self.dataArray = tempDataArray;
                [self.collectionView reloadData];
                
                // 相册变更主动处理
                [ClearPhotoManager tipWithMessage:@"删除成功"];
                [ClearPhotoManager shareManager].notificationStatus = PhotoNotificationStatusNeed;
            }
        }];

ThinPhotoViewController中使用:压缩后通知更新相册和数据源

// 点击优化按钮
- (void)clickOptmizeButton {
    // 相册变更不处理
    [ClearPhotoManager shareManager].notificationStatus = PhotoNotificationStatusClose;

// 通过Index获取对应图片并对其进行优化
- (void)optmizeImageWithIndex:(NSInteger)index {
    // 压缩结束条件
    if (index >= self.dataArray.count)
    {
        // 压缩完成隐藏提示框
        NSLog(@"恭喜,压缩图片全部顺利完成");
        self.hud.hidden = YES;
        
        // 通知相册变更主动处理
        [ClearPhotoManager shareManager].notificationStatus = PhotoNotificationStatusNeed;

2、创建主视图

创建主视图
Model:ClearPhotoItem
/// 图片类型
typedef NS_ENUM(NSInteger, ClearPhotoType) {
    ClearPhotoTypeUnknow      = 0, // 未知
    ClearPhotoTypeSimilar     = 1, // 相似图片
    ClearPhotoTypeScreenshots = 2, // 截屏图片
    ClearPhotoTypeThinPhoto   = 3, // 图片瘦身
};

@interface ClearPhotoItem : NSObject

/// 图片类型
@property (nonatomic, assign) ClearPhotoType type;
/// 名称
@property (nonatomic, copy) NSString *name;
/// 详情
@property (nonatomic, copy) NSString *detail;
/// 可节约空间字符串
@property (nonatomic, copy) NSString *saveString;
/// 可处理图片的数量
@property (nonatomic, assign) NSInteger count;
/// 图标
@property (nonatomic, copy) NSString *icon;

/// 初始化Model,传入类型和info
- (instancetype)initWithType:(ClearPhotoType)type dataDict:(NSDictionary *)dict;

@end



#import "ClearPhotoItem.h"

@implementation ClearPhotoItem

- (instancetype)initWithType:(ClearPhotoType)type dataDict:(NSDictionary *)dict
{
    self = [self init];
    if (self)
    {
        self.type = type;
        self.count = [dict[@"count"] integerValue];

        if (type == ClearPhotoTypeSimilar)
        {
            self.name = @"相似照片处理";
            self.detail = [NSString stringWithFormat:@"相似/连拍照片 %ld 张", [dict[@"count"] integerValue]];
        }
        else if (type == ClearPhotoTypeScreenshots)
        {
            self.name = @"截屏照片清理";
            self.detail = [NSString stringWithFormat:@"可清理照片 %ld 张", [dict[@"count"] integerValue]];
        }
        else if (type == ClearPhotoTypeThinPhoto)
        {
            self.name = @"照片瘦身";
            self.detail = [NSString stringWithFormat:@"可优化照片 %ld 张", [dict[@"count"] integerValue]];
        }
        
        self.saveString = [NSString stringWithFormat:@"%.2fMB", [dict[@"saveSpace"] unsignedIntegerValue]/1024.0/1024.0];
    }
    return self;
}

@end
View:ClearPhotoCell
/// 选择清理相似、瘦身、裁剪图片
@interface ClearPhotoCell : UITableViewCell

/// 显示Mode的数据
- (void)bindWithMode:(ClearPhotoItem *)item;

@end


#import "ClearPhotoCell.h"

@implementation ClearPhotoCell

- (void)bindWithMode:(ClearPhotoItem *)item
{
    self.textLabel.text = item.name;
    self.detailTextLabel.text = [NSString stringWithFormat:@"%@ 可省 %@", item.detail, item.saveString];
}

@end

ViewController:ClearPhotoViewController

#import "ClearPhotoViewController.h"
#import "ClearPhotoCell.h"
#import "ClearPhotoItem.h"
#import "ClearPhotoManager.h"
#import "MBProgressHUD.h"

#import "SimilarPhotoAndScreenShotsViewController.h"
#import "ThinPhotoViewController.h"

@interface ClearPhotoViewController ()

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray *dataArr;// 数据源
@property (nonatomic, strong) ClearPhotoManager *clearPhotoManager;// 清理照片的Manager
@property (nonatomic, weak) MBProgressHUD *hud;// 提示框

@end

@implementation ClearPhotoViewController

#pragma mark - Life Circle

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.title = @"照片清理";
    self.view.backgroundColor = [UIColor whiteColor];
    
    // 加载照片数据源
    [self loadPhotoData];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    // 相册变更主动处理
    if (self.clearPhotoManager.notificationStatus == PhotoNotificationStatusNeed)
    {
        // 加载照片数据源
        [self loadPhotoData];
        // 重置为相册变更默认处理
        self.clearPhotoManager.notificationStatus = PhotoNotificationStatusDefualt;
    }
}

#pragma mark - Data

// 加载照片数据源
- (void)loadPhotoData
{
    // 已经存在则直接返回
    if (self.hud)
    {
        return;
    }
    
    // 否则创建新的提示框
    MBProgressHUD *hud = [[MBProgressHUD alloc] initWithView:self.view];
    hud.label.text = @"扫描照片中";
    // A round, pie-chart like, progress view.
    hud.mode = MBProgressHUDModeDeterminate;
    // 提示框隐藏后,将提示框从父视图中移除
    hud.removeFromSuperViewOnHide = YES;
    // 显示提示框
    [hud showAnimated:YES];
    [self.view addSubview:hud];
    
    // 加载照片数据源
    __weak typeof(self) weakSelf = self;
    [self.clearPhotoManager loadPhotoWithProcess:^(NSInteger current, NSInteger total) {
        // 实现进度条block,计算进度
        hud.progress = (CGFloat)current / total;
    } completionHandler:^(BOOL success, NSError * _Nonnull error) {
        // 隐藏提示框
        [hud hideAnimated:YES];
        weakSelf.hud = nil;
        
        // 将拿到的数据配置到清理相似照片Model,传入similarInfo
        ClearPhotoItem *similarItem = [[ClearPhotoItem alloc] initWithType:ClearPhotoTypeSimilar dataDict:weakSelf.clearPhotoManager.similarInfo];
        
        // 将拿到的数据配置到清理相似照片Model,传入similarInfo
        ClearPhotoItem *screenshotsItem = [[ClearPhotoItem alloc] initWithType:ClearPhotoTypeScreenshots dataDict:weakSelf.clearPhotoManager.screenshotsInfo];
        
        // 将拿到的数据配置到照片瘦身Model,传入thinPhotoInfo
        ClearPhotoItem *thinItem = [[ClearPhotoItem alloc] initWithType:ClearPhotoTypeThinPhoto dataDict:weakSelf.clearPhotoManager.thinPhotoInfo];
        
        // 数据源
        weakSelf.dataArr = @[similarItem, screenshotsItem, thinItem];
        
        // 拿到数据后,创建表头视图,显示总共可以节约的空间
        [weakSelf createHeadView];
    }];
}

// 创建表头视图,显示总共可以节约的空间
- (void)createHeadView
{
    UILabel *headLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 150)];
    headLabel.text = [NSString stringWithFormat:@"优化后可节约空间 %.2fMB", self.clearPhotoManager.totalSaveSpace / 1024.0/1024.0];
    headLabel.textAlignment = NSTextAlignmentCenter;
    self.tableView.tableHeaderView = headLabel;
    [self.tableView reloadData];
}

#pragma mark - ClearPhotoManagerDelegate

// 相册变动代理方法
- (void)clearPhotoLibraryDidChange
{
    // 加载照片数据源
    [self loadPhotoData];
}

#pragma mark - UITableViewDataSource & UITableViewDelegate

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.dataArr.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    ClearPhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ClearPhotoCell"];
    if (cell == nil)
    {
        cell = [[ClearPhotoCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"ClearPhotoCell"];
    }
    
    // cell显示model中的数据
    ClearPhotoItem *item = self.dataArr[indexPath.row];
    [cell bindWithMode:item];
    
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    ClearPhotoItem *item = self.dataArr[indexPath.row];
    // 可处理图片的数量为0,则不能选中进入处理页面
    if (!item.count)
    {
        return;
    }
    
    switch (item.type)
    {
        case ClearPhotoTypeSimilar:
        {
            SimilarPhotoAndScreenShotsViewController *vc = [SimilarPhotoAndScreenShotsViewController new];
            vc.similarOrScreenshotsArr = self.clearPhotoManager.similarArray;
            vc.isScreenshots = NO;
            [self.navigationController pushViewController:vc animated:YES];
            break;
        }
        case ClearPhotoTypeThinPhoto:
        {
            ThinPhotoViewController *vc = [ThinPhotoViewController new];
            vc.thinPhotoArray = self.clearPhotoManager.thinPhotoArray;
            vc.thinPhotoItem = item;
            [self.navigationController pushViewController:vc animated:YES];
            break;
        }
        case ClearPhotoTypeScreenshots:
        {
            SimilarPhotoAndScreenShotsViewController *vc = [SimilarPhotoAndScreenShotsViewController new];
            vc.similarOrScreenshotsArr = self.clearPhotoManager.screenshotsArray;
            vc.isScreenshots = YES;
            [self.navigationController pushViewController:vc animated:YES];
            break;
        }
        default:
            break;
    }
}

#pragma mark - Getter

- (UITableView *)tableView
{
    if (!_tableView)
    {
        _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
        _tableView.dataSource = self;
        _tableView.delegate = self;
        _tableView.rowHeight = 60;
        [self.view addSubview:_tableView];
    }
    return _tableView;
}

- (ClearPhotoManager *)clearPhotoManager
{
    if (!_clearPhotoManager)
    {
        _clearPhotoManager = [ClearPhotoManager shareManager];
        _clearPhotoManager.delegate = self;
    }
    return _clearPhotoManager;
}

@end

一、清理相似图片

1、运行效果

a、选中某种图片进行删除
点击删除相似图片
删除成功

删除一张图片后的内存变化:

删除前:2020-08-04 11:28:39.537782+0800 Demo[2696:26930931] 删掉相似照片可省 :7.10MB
删除后:2020-08-04 13:35:25.664089+0800 Demo[2696:26930931] 删掉相似照片可省 :6.87MB

注意:这里进行了个小实验,可以看到即使照片相同,但是如果创建日期不同的话,APP也对它进行了单独分组,因为我们是按照日期的标准来划分组的。实际使用过程中,这种情况很少,因为会产生相似图片的原因大都是复制和连拍,而复制的图片、连拍的图片日期都是相同的。

b、删除全部相似图片
将相册中的相似图片全部删除完
清零
2020-08-04 13:44:47.695043+0800 Demo[2925:26991742] 删掉相似照片可省 :0.00MB
c、复制出来一张图片
再复制出来一张图片
复制出来的图片日期和原图相同
切回到APP,收到相册改变通知,作出相应变化
复制的图片、连拍的图片日期都是相同的
2020-08-04 13:47:48.507195+0800 Demo[2925:26991742] 删掉相似照片可省 :1.81MB

2、ClearPhotoManager文件

a、使用到的属性和提供的方法接口
/// 相似照片数组:存储了多个字典,每个字典代表了同一个日期下的相似照片
@property (nonatomic, strong, readonly) NSMutableArray *similarArray;
/// 相似照片信息:存储了相似图片数量及可以节省的内存空间大小
@property (nonatomic, strong, readonly) NSDictionary *similarInfo;
// 删掉相似图片可以节省的空间大小
@property (nonatomic, assign) NSUInteger similarSaveSpace;


// 获取图片的过程:当前正在获取第几张图片,总共有多少张
@property (nonatomic, copy) void (^processHandler)(NSInteger current, NSInteger total);
// 完成的回调
@property (nonatomic, copy) void (^completionHandler)(BOOL success, NSError *error);


// PHImageManager的requestImageForAsset所需要的options
@property (nonatomic, strong) PHImageRequestOptions *imageRequestOptions;
// PHImageManager的requestImageDataForAsset所需要的options
@property (nonatomic, strong) PHImageRequestOptions *imageSizeRequestOptions;


/// 删除照片
+ (void)deleteAssets:(NSArray *)assets completionHandler:(void (^)(BOOL success, NSError *error))completion;

/// 确定提示框
+ (void)tipWithMessage:(NSString *)str;

属性的懒加载方法实现如下:

- (NSMutableArray *)similarArray
{
    if (!_similarArray)
    {
        _similarArray = [NSMutableArray array];
    }
    return _similarArray;
}

- (PHImageRequestOptions *)imageRequestOptions
{
    if (!_imageRequestOptions) {
        _imageRequestOptions = [[PHImageRequestOptions alloc] init];
        // resizeMode 属性控制图像的剪裁
        _imageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeNone;// no resize
        // deliveryMode 则用于控制请求的图片质量
        _imageRequestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
    }
    return _imageRequestOptions;
}

- (PHImageRequestOptions *)imageSizeRequestOptions
{
    if (!_imageSizeRequestOptions) {
        _imageSizeRequestOptions = [[PHImageRequestOptions alloc] init];
        // resizeMode 属性控制图像的剪裁
        _imageSizeRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;// exactly targetSize
        // deliveryMode 则用于控制请求的图片质量
        _imageSizeRequestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
    }
    return _imageSizeRequestOptions;
}
b、dealImageWithIndex: 中清理相似图片的部分
    // 1:该图片是相似图片吗
    if (self.lastAsset && isSameDay) 
    {
        // 图片相似度算法
        BOOL isLike = [ImageCompare isImage:self.lastExactImage likeImage:exactImage];
        if (isLike)
        {
            // 更新相似图片数据,传入当前图片资源、原图、缩略图
            [self updateSimilarArrWithAsset:asset exactImage:exactImage originImageData:originImageData];
            // 必须放在下面,影响创建字典
            self.isLastSame = YES;
        }
        else
        {
            // 即使在同一天,但不满足相似度算法,也不是同一张图片
            self.isLastSame = NO;
        }
    }
    else // 上一张图片不存在或者不是同一天则非同张相片
    {
        self.isLastSame = NO;
    }

这里用到了图片相似度算法,其原理包括五步:
1、缩小尺寸
2、简化色彩
3、计算平均值
4、比较像素的灰度
5、计算哈希值

该算法的具体实现我们不用去关心,只需要导入ImageCompare.h这个文件即可。在该文件内部用到了:

#import 

所以还需要导入这个框架:opencv2.framework。这两份文件在我的demo里都已经提供了。

如果上一张图片存在并且是现在的图片的创建时间是同一天,而且还满足相似度算法,则更新相似图片数据源,否则说明和上一张图片并不相似。

c、更新相似图片数据源
// 更新相似图片数据源
- (void)updateSimilarArrWithAsset:(PHAsset *)asset exactImage:(UIImage *)exactImage originImageData:(NSData *)originImageData
{
    // 相似图片数组中最后一张图片
    NSDictionary *lastDictionary = self.similarArray.lastObject;
    
    // lastDictionary存储的是同一天的相似图片
    // 如果不是同一天或者不相似则isLastSame为NO
    // 此时将旧的lastDictionary清除,再创建新的lastDictionary,self.screenshotsArray则有多个元素,开启下一行日期显示
    if (!self.isLastSame)
    {
        lastDictionary = nil;
    }
    
    // lastDictionary为空则用上一次图片的数据进行创建,存在则直接添加
    // 因为是比较相似,上一次的图片也要在新的日期行显示出来
    if (!lastDictionary)
    {
        // 上一次图片的数据
        NSDictionary *itemDictionary = @{@"asset" : self.lastAsset, @"exactImage" : self.lastExactImage, @"originImageData" : self.lastOriginImageData, @"originImageDataLength" : @(self.lastOriginImageData.length)};
        // 以当前图片资源的创建日期作为key
        NSString *keyString = [self stringWithDate:asset.creationDate];// 2020年07月31日
        // 创建字典,value是只有一个字典元素的可变数组
        // value必须是可变数组,因为itemArray是可变数组,将当前图片信息加入到itemArray后,直接keyString : itemArray来更新lastDictionary
        lastDictionary = @{keyString : @[itemDictionary].mutableCopy};
        
        // 添加到相似数组中
        [self.similarArray addObject:lastDictionary];
    }
    
    // lastDictionary的value是个可变的数组,数组里的元素是字典,最后一个元素是上次新添加进去的字典
    // 将上一次的字典元素放入了itemArray可变数组中
    NSMutableArray *itemArray = lastDictionary.allValues.lastObject;
    
    // 当前图片的信息
    NSDictionary *itemDictionary = @{@"asset" : asset, @"exactImage" : exactImage, @"originImageData" : originImageData, @"originImageDataLength" : @(originImageData.length)};
    // 将当前图片信息加入到itemArray
    [itemArray addObject:itemDictionary];
    
    // lastDictionary的key还是上次的,但是value却更新了,value是个可变数组,数组里的元素是字典,这次多加了一个字典到数组中
    lastDictionary = @{lastDictionary.allKeys.lastObject : itemArray};
    
    // 将相似数组的最后一个元素替换为新的lastDictionary
    [self.similarArray replaceObjectAtIndex:self.similarArray.count - 1 withObject:lastDictionary];
    
    // imageData为当前图片的原图大小,清除相似图片后可节省的内存空间加上其大小
    self.similarSaveSpace = self.similarSaveSpace + originImageData.length;
}

其中以创建日期作为字典的key,其方法stringWithDate:的实现如下:

// NSDate转NSString
- (NSString *)stringWithDate:(NSDate *)date
{
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"yyyy年MM月dd日"];
    return [dateFormatter stringFromDate:date];
}

这是最关键的一步,也是非常绕脑袋的一步,理解的关键是分清楚下面的数组和字典的作用和区别:

  • similarArray:相似照片数组:存储了多个字典,每个字典代表了同一个日期下的相似照片
  • similarInfo:相似照片信息:存储了相似图片数量及可以节省的内存空间大小
  • lastDictionary:lastDictionaryvalue是个可变的数组,数组里的元素是字典,最后一个元素是上次新添加进去的字典。通过isLastSame判断可以将lastDictionary置空,为空则用上一次图片的数据进行创建新字典这样可以在similarArray中添加一个新的字典元素显示新的一行日期,而lastDictionary不为空则直接向原来的字典中添加新元素。
  • itemArray:将上一次的字典元素放入了itemArray可变数组中 + 将当前图片信息加入到itemArray配成similarArray中最后一个lastDictionary即同一个日期下的相似组。

similarArray的内容打印结果如下:

similarArray的内容打印结果如下
2020-08-04 09:52:11.837306+0800 Demo[2127:26821823] similarArray:(
......
        {// index = 1,有3张创建日期为2020年07月31日的相似图片
        "2020\U5e7407\U670831\U65e5" =         (
                        {
                asset = " B12FA148-3B97-498D-8921-E027BE2DE705/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            },
                        {
                asset = " 8CA3667A-ACDD-41DF-A0DD-5C18330A585E/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            },
                        {
                asset = " CF9CDC54-6DD1-41A9-9E15-FD493FA19282/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            }
        );
    },


        {// index = 2,有2张创建日期为2020年07月31日的相似图片
        "2020\U5e7407\U670831\U65e5" =         (
                        {
                asset = " 720062B8-97B5-4E63-930F-2EAF9E5AE5DD/L0/001 mediaType=1/0, sourceType=1, (1506x1129), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 528368, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... b347cd7b 691fffd9 };
                originImageDataLength = 528368;
            },
                        {
                asset = " 7FFF3E4E-FC7F-4929-8538-BCEF222C5834/L0/001 mediaType=1/0, sourceType=1, (1506x1129), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 528368, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... b347cd7b 691fffd9 };
                originImageDataLength = 528368;
            }
        );
    },


        {// index = 3,有2张创建日期为2012年08月09日的相似图片
        "2012\U5e7408\U670809\U65e5" =         (
                        {
                asset = " 6A2F6E9E-8717-4368-945E-8E625C5C0A49/L0/001 mediaType=1/0, sourceType=1, (3000x2002), creationDate=2012-08-08 21:55:30 +0000, location=1, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 1852262, bytes = 0xffd8ffe0 00104a46 49460001 01010048 ... 8a28a1ee 0f73ffd9 };
                originImageDataLength = 1852262;
            },
                        {
                asset = " ED7AC36B-A150-4C38-BB8C-B6D696F4F2ED/L0/001 mediaType=1/0, sourceType=1, (3000x2002), creationDate=2012-08-08 21:55:30 +0000, location=1, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 1852262, bytes = 0xffd8ffe0 00104a46 49460001 01010048 ... 8a28a1ee 0f73ffd9 };
                originImageDataLength = 1852262;
            }
        );
    },
......
)
(lldb) 

similarArray中每一个lastDictionary打印结果如下:

2020-08-04 10:11:38.662829+0800 Demo[2127:26821823] similarArray中每一个lastDictionary:{
// index = 1,有3张创建日期为2020年07月31日的相似图片
        "2020\U5e7407\U670831\U65e5" =         (
                        {
                asset = " B12FA148-3B97-498D-8921-E027BE2DE705/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            },
                        {
                asset = " 8CA3667A-ACDD-41DF-A0DD-5C18330A585E/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            },
                        {
                asset = " CF9CDC54-6DD1-41A9-9E15-FD493FA19282/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            }
        );

(lldb) 

lastDictionary的allValues:

// index = 1,有3张创建日期为2020年07月31日的相似图片
2020-08-04 10:16:37.124388+0800 Demo[2127:26821823] lastDictionary的allValues:(
                        {
                asset = " B12FA148-3B97-498D-8921-E027BE2DE705/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            },
                        {
                asset = " 8CA3667A-ACDD-41DF-A0DD-5C18330A585E/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            },
                        {
                asset = " CF9CDC54-6DD1-41A9-9E15-FD493FA19282/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            }
        );

lastDictionary的allValues的lastObject:

NSMutableArray *itemArray = lastDictionary.allValues.lastObject;
2020-08-04 10:16:37.124388+0800 Demo[2127:26821823] lastDictionary的allValues的lastObject:
                        {
                asset = " B12FA148-3B97-498D-8921-E027BE2DE705/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            },
                        {
                asset = " 8CA3667A-ACDD-41DF-A0DD-5C18330A585E/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            },
                        {
                asset = " CF9CDC54-6DD1-41A9-9E15-FD493FA19282/L0/001 mediaType=1/0, sourceType=1, (1699x1134), creationDate=2020-07-31 02:46:46 +0000, location=0, hidden=0, favorite=0, adjusted=0 ";
                exactImage = "";
                originImageData = {length = 397451, bytes = 0xffd8ffe0 00104a46 49460001 01000048 ... bffc6eba 0bb1ffd9 };
                originImageDataLength = 397451;
            }

去掉了包装在字典外面的数组(),其实就只有一个元素。

3、创建视图

创建视图
Model:PhotoInfoItem
@interface PhotoInfoItem : NSObject

/// 图片资源
@property (nonatomic, strong) PHAsset *asset;
/// 图片
@property (nonatomic, strong) UIImage *exactImage;
/// 图片数据
@property (nonatomic, strong) NSData *originImageData;
/// 图片数据大小
@property (nonatomic, assign) NSUInteger originImageDataLength;
/// 是否选中
@property (nonatomic, assign) BOOL isSelected;

/// 初始化Model,传入info
- (instancetype)initWithDict:(NSDictionary *)dict;

@end


#import "PhotoInfoItem.h"

@implementation PhotoInfoItem

- (instancetype)initWithDict:(NSDictionary *)dict
{
    self = [super init];
    if (self)
    {
        self.asset = dict[@"asset"];
        self.exactImage = dict[@"exactImage"];
        self.originImageData = dict[@"originImageData"];
        self.originImageDataLength = [dict[@"originImageDataLength"] unsignedIntegerValue];
    }
    return self;
}

@end
View:SimilarPhotoCell
/// 图片+选中按钮
@interface SimilarPhotoCell : UICollectionViewCell

/// 显示Mode的数据
- (void)bindWithModel:(PhotoInfoItem *)model;

@end



#import "SimilarPhotoCell.h"

@interface SimilarPhotoCell ()

/// 图标
@property (nonatomic, weak) UIImageView *iconView;
/// 选择按钮
@property (nonatomic, weak) UIButton *selectButton;
/// Model
@property (nonatomic, strong) PhotoInfoItem *item;

@end

@implementation SimilarPhotoCell

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        [self setupUIWithFrame:frame];
    }
    return self;
}

- (void)setupUIWithFrame:(CGRect)frame
{
    // 显示图片
    UIImageView *iconView = [[UIImageView alloc] initWithFrame:self.bounds];
    iconView.contentMode = UIViewContentModeScaleAspectFill;
    iconView.clipsToBounds = YES;
    self.iconView = iconView;
    [self addSubview:self.iconView];
    
    // 选中按钮
    CGFloat selectWH = frame.size.width * 0.3;
    CGFloat selectX = frame.size.width - selectWH;
    UIButton *selectButton = [[UIButton alloc] initWithFrame:CGRectMake(selectX, 0, selectWH, selectWH)];
    [self.selectButton setImage:[UIImage imageNamed:@"choose_unseleced"] forState:UIControlStateNormal];
    [self.selectButton setImage:[UIImage imageNamed:@"choose_seleced"] forState:UIControlStateSelected];
    [self.selectButton addTarget:self action:@selector(clickSelectBtn:) forControlEvents:UIControlEventTouchUpInside];
    self.selectButton = selectButton;
    [self addSubview:self.selectButton];
}

// 点击切换选中状态
- (void)clickSelectBtn:(UIButton *)button
{
    button.selected = !button.selected;
    self.item.isSelected = button.selected;// 更新本地数据源的选中状态
}

// 显示Mode的数据
- (void)bindWithModel:(PhotoInfoItem *)model
{
    self.item = model;
    
    self.iconView.image = model.exactImage;
    self.selectButton.selected = model.isSelected;
}

@end
View:SimilarPhotoHeadView
/// 日期
@interface SimilarPhotoHeadView : UICollectionReusableView

/// 显示传入的字典中的数据
- (void)bindWithModel:(NSDictionary *)model;

@end



#import "SimilarPhotoHeadView.h"

@interface SimilarPhotoHeadView()

@property (nonatomic, weak) UILabel *timeLabel;

@end

@implementation SimilarPhotoHeadView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 0, frame.size.width * 0.5, frame.size.height)];
        self.timeLabel = timeLabel;
        [self addSubview:self.timeLabel];
        
    }
    return self;
}

- (void)bindWithModel:(NSDictionary *)model
{
    self.timeLabel.text = model.allKeys.lastObject;
}

@end
ViewController:SimilarPhotoAndScreenShotsViewController
@interface SimilarPhotoAndScreenShotsViewController : UIViewController

/// 相似图片或者屏幕截图数据源,传入similarArr或者screenshotsArr
@property (nonatomic, strong) NSArray *similarOrScreenshotsArr;

/// 是否是屏幕截图,默认为NO
@property (nonatomic, assign) BOOL isScreenshots;

@end



#import "SimilarPhotoAndScreenShotsViewController.h"
#import "PhotoInfoItem.h"
#import "ClearPhotoManager.h"
#import "SimilarPhotoCell.h"
#import "SimilarPhotoHeadView.h"

@interface SimilarPhotoAndScreenShotsViewController ()

@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) NSMutableArray *dataArray;

@end

@implementation SimilarPhotoAndScreenShotsViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    if (self.isScreenshots)
    {
        self.title = @"删除截屏图片";
    }
    else
    {
        self.title = @"清理相似照片";
    }
    
    // 创建底部删除按钮
    [self createBottomView];
    // 用拿到的数据配置Model
    [self configData];
}

// 创建底部删除按钮
- (void)createBottomView
{
    UIButton *deleteButton = [[UIButton alloc] initWithFrame:CGRectMake(0, self.view.frame.size.height - 100, self.view.frame.size.width, 50)];
    [deleteButton setTitle:self.isScreenshots ? @"删除屏幕截图" : @"删除相似照片" forState:UIControlStateNormal];
    deleteButton.backgroundColor = [UIColor redColor];
    [deleteButton addTarget:self action:@selector(clickDeleteButton) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:deleteButton];
}

// 用拿到的数据配置Model
- (void)configData
{
    [self.dataArray removeAllObjects];
    
    for (NSDictionary *dictionary in self.similarOrScreenshotsArr)
    {
        NSString *keyString = dictionary.allKeys.lastObject;
        NSArray *valueArray = dictionary.allValues.lastObject;
        // 存放配置完成后的Model
        NSMutableArray *mutableValueArray = [NSMutableArray arrayWithCapacity:valueArray.count];
        
        // 用拿到的数据配置Model
        for (int i = 0; i < valueArray.count; I++)
        {
            NSDictionary *infoDiction = valueArray[I];
            PhotoInfoItem *item = [[PhotoInfoItem alloc] initWithDict:infoDiction];
            [mutableValueArray addObject:item];
        }
        
        // 更新数据源为配置后的Model
        NSDictionary *temDictionary = @{keyString : mutableValueArray};
        [self.dataArray addObject:temDictionary];
    }
    
    [self.collectionView reloadData];
}

#pragma mark - Event

// 点击删除按钮
- (void)clickDeleteButton
{
    // 选中的图片数组
    NSMutableArray *assetArray = [NSMutableArray array];
    // 临时的数据源,最后用来赋值给self.dataArray
    NSMutableArray *tempDataArray = [NSMutableArray array];
    
    for (NSDictionary *dictionary in self.dataArray)// 每个日期的dictionary
    {
        // 未选中的图片数组
        NSMutableArray *mutableArray = [NSMutableArray array];
        
        // 配置后的Model
        NSArray *modelArray = dictionary.allValues.lastObject;
        for (PhotoInfoItem *item in modelArray)
        {
            if (item.isSelected)// 删除选中
            {
                [assetArray addObject:item.asset];
            }
            else// 保留未选中
            {
                [mutableArray addObject:item];
            }
        }
        
        // 清理相似照片:mutableArray的数量至少在2张照片以上
        // 清理屏幕截图:mutableArray有照片即可
        if ( (self.isScreenshots && mutableArray.count > 0) || (!self.isScreenshots && mutableArray.count > 1) )
        {
            // 更新删除选中后保留下来的未选中的数据源
            NSDictionary *tempDictionary = @{dictionary.allKeys.lastObject : mutableArray};
            [tempDataArray addObject:tempDictionary];
        }
    }
    
    // 要删除的选中资源数量大于0
    if (assetArray.count)
    {
        // 相册变更不处理
        [ClearPhotoManager shareManager].notificationStatus = PhotoNotificationStatusClose;
        // 删除选中资源
        [ClearPhotoManager deleteAssets:assetArray completionHandler:^(BOOL success, NSError * _Nonnull error) {
            // 实现删除成功的block
            if (success)
            {
                // 更新删除选中后保留下来的未选中的数据源
                self.dataArray = tempDataArray;
                [self.collectionView reloadData];
                
                // 相册变更主动处理
                [ClearPhotoManager tipWithMessage:@"删除成功"];
                [ClearPhotoManager shareManager].notificationStatus = PhotoNotificationStatusNeed;
            }
        }];
    }
}

#pragma mark - UICollectionViewDataSource & UICollectionViewDelegate

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return self.dataArray.count;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    NSDictionary *dictionary = self.dataArray[section];
    NSArray *modelArray = dictionary.allValues.lastObject;
    return modelArray.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    SimilarPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"SimilarPhotoCell" forIndexPath:indexPath];
    cell.backgroundColor = [UIColor yellowColor];
    
    // 显示Mode的数据
    NSDictionary *dictionary = self.dataArray[indexPath.section];
    NSArray *modelArray = dictionary.allValues.lastObject;
    [cell bindWithModel:modelArray[indexPath.row]];
    
    return cell;
}

// 显示节头时间视图
-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
    if (kind == UICollectionElementKindSectionHeader)
    {
        SimilarPhotoHeadView *headerView =  [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"SimilarPhotoHeadView" forIndexPath:indexPath];
        
        // 显示传入的字典中的数据
        NSDictionary *dictionary = self.dataArray[indexPath.section];
        [headerView bindWithModel:dictionary];
        return headerView;
    }
    
    return nil;
}

// 设置段头view大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
{
    return CGSizeMake(0, 40);
}


#pragma mark - Getter

- (NSMutableArray *)dataArray
{
    if (!_dataArray)
    {
        _dataArray = [NSMutableArray array];
    }
    return _dataArray;
}

- (UICollectionView *)collectionView
{
    if (!_collectionView)
    {
        // 布局
        UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
        CGFloat itemCount = 4;
        CGFloat distance = 8;
        CGFloat itemWH = (self.view.frame.size.width - distance * (itemCount + 1)) / itemCount - 1;
        layout.itemSize = CGSizeMake(itemWH, itemWH);
        layout.sectionInset = UIEdgeInsetsMake(distance, distance, distance, distance);
        layout.minimumLineSpacing = distance;
        layout.minimumInteritemSpacing = distance;
        
        _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size .height - 100)
                                             collectionViewLayout:layout];
        _collectionView.dataSource = self;
        _collectionView.delegate = self;
        [_collectionView registerClass:[SimilarPhotoCell class]
            forCellWithReuseIdentifier:@"SimilarPhotoCell"];
        [_collectionView registerClass:[SimilarPhotoHeadView class]
            forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                   withReuseIdentifier:@"SimilarPhotoHeadView"];
        _collectionView.backgroundColor = [UIColor whiteColor];
        [self.view addSubview:_collectionView];
    }
    return _collectionView;
}

@end

二、清理截屏图片

看懂了上面的,接下来就简单了,都是套路。

1、运行效果

a、删除某张屏幕截图
删除某张屏幕截图
删除成功
相册中该屏幕截图确实没了

删除前后内存变化:

删除前:2020-08-04 13:47:48.507267+0800 Demo[2925:26991742] 删掉屏幕截图可省 :6.66MB
删除后:2020-08-04 14:26:12.699202+0800 Demo[3010:27017851] 删掉屏幕截图可省 :6.50MB
b、删除全部屏幕截图
删除全部屏幕截图
清零
2020-08-04 14:29:00.665145+0800 Demo[3010:27017851] 删掉屏幕截图可省 :0.00MB
c、新增一张屏幕截图
新增一张屏幕截图
相册新添加了一张屏幕截图
APP观察到了相册变化
该屏幕截图可以被清除

2、ClearPhotoManager文件

a、使用到的属性和提供的方法接口
/// 截图照片数组:存储了多个字典,每个字典代表了同一个日期下的截图照片
@property (nonatomic, strong, readonly) NSMutableArray *screenshotsArray;
/// 截图照片信息:存储了屏幕截图数量及可以节省的内存空间大小
@property (nonatomic, strong, readonly) NSDictionary *screenshotsInfo;
// 删掉屏幕截图后可以节省的空间大小
@property (nonatomic, assign) NSUInteger screenshotsSaveSpace;

/// 删除照片
+ (void)deleteAssets:(NSArray *)assets completionHandler:(void (^)(BOOL success, NSError *error))completion;

/// 确定提示框
+ (void)tipWithMessage:(NSString *)str;

相关属性懒加载方法的实现:

- (NSMutableArray *)screenshotsArray
{
    if (!_screenshotsArray)
    {
        _screenshotsArray = [NSMutableArray array];
    }
    return _screenshotsArray;
}
b、dealImageWithIndex: 中清理相似图片的部分
    // 2:该图片是截屏图片吗
    if (asset.mediaSubtypes == PHAssetMediaSubtypePhotoScreenshot)
    {
        // 最后一张图
        NSDictionary *lastDictionary = self.screenshotsArray.lastObject;
        
        // lastDictionary存储的是同一天的截屏图片
        // 如果不是同一天,则将旧的lastDictionary清除,再创建新的lastDictionary,self.screenshotsArray则有多个元素,开启下一行日期显示
        if (lastDictionary && !isSameDay)// index = 3,此时不是同一天,进入
        {
            lastDictionary = nil;
        }
        
        // 更新截屏图片数据
        [self updateScreenShotsWithAsset:asset exactImage:exactImage originImageData:originImageData lastDictionary:lastDictionary];
    }
c、更新截屏图片数据
// 更新截屏图片数据
- (void)updateScreenShotsWithAsset:(PHAsset *)asset exactImage:(UIImage *)exactImage originImageData:(NSData *)originImageData lastDictionary:(NSDictionary *)lastDictionary
{
    NSDictionary *itemDictionary = @{ @"asset" : asset, @"exactImage" : exactImage, @"originImageData" : originImageData, @"originImageDataLength" : @(originImageData.length) };
    
    // 不存在则创建
    if (!lastDictionary)
    {
        NSString *keyString = [self stringWithDate:asset.creationDate]; 
        lastDictionary = @{keyString : @[itemDictionary].mutableCopy};// value是只有一个字典元素的可变数组
        [self.screenshotsArray addObject:lastDictionary];// 将该字典加入到截屏图片数据源中
    }
    // 存在则添加
    else
    {
        // lastDictionary的value是个可变的数组,数组里的元素是字典,最后一个元素是上次新添加进去的字典
        // 将上一次的字典元素放入了itemArray可变数组中
        NSMutableArray *itemArray = lastDictionary.allValues.lastObject;
        // 将当前图片信息加入到itemArray
        [itemArray addObject:itemDictionary];
        
        // lastDictionary的key还是上次的,但是value却更新了,value是个可变数组,数组里的元素是字典,这次多加了一个字典到数组中
        lastDictionary = @{lastDictionary.allKeys.lastObject : itemArray};
        
        // 替换lastDictionary
        [self.screenshotsArray replaceObjectAtIndex:self.screenshotsArray.count - 1 withObject:lastDictionary];
    }
    
    // 可节省空间
    self.screenshotsSaveSpace = self.screenshotsSaveSpace + originImageData.length;// 0 + 169135 + ...
}

3、创建视图

创建视图

Model、View、ViewController和相似图片使用的是同样的文件。这里罗列下SimilarPhotoAndScreenShotsViewController中清除屏幕截图的部分。

@interface SimilarPhotoAndScreenShotsViewController : UIViewController

/// 相似图片或者屏幕截图数据源,传入similarArr或者screenshotsArr
@property (nonatomic, strong) NSArray *similarOrScreenshotsArr;

/// 是否是屏幕截图,默认为NO
@property (nonatomic, assign) BOOL isScreenshots;

@end

// 导航栏标题
    if (self.isScreenshots)
    {
        self.title = @"删除截屏图片";
    }
    else
    {
        self.title = @"清理相似照片";
    }

// 删除按钮文本
[deleteButton setTitle:self.isScreenshots ? @"删除屏幕截图" : @"删除相似照片" forState:UIControlStateNormal];

// 点击删除按钮
        // 清理相似照片:mutableArray的数量至少在2张照片以上
        // 清理屏幕截图:mutableArray有照片即可
        if ( (self.isScreenshots && mutableArray.count > 0) || (!self.isScreenshots && mutableArray.count > 1) )
        {
            // 更新删除选中后保留下来的未选中的数据源
            NSDictionary *tempDictionary = @{dictionary.allKeys.lastObject : mutableArray};
            [tempDataArray addObject:tempDictionary];
        }

三、压缩图片

这个和前面两个有些区别。

1、运行效果

a、压缩显示的第一张图
压缩显示的第一张图

获取显示在左边的原图和显示在右边的压缩图。这里显示数组中的第一张图,这张图得到了压缩。控制台输出结果为:

2020-08-04 15:26:57.411379+0800 Demo[3199:27062840] 图片压缩前 imageDataLength: 2.68MB, imageSize:{4032, 3024}
2020-08-04 15:26:57.606104+0800 Demo[3199:27062840] 图片压缩后 imageDataLength: 1.36MB, imageSize:{4032, 3024}
2020-08-04 15:26:57.862177+0800 Demo[3199:27061906] JPEG 原图大小 : 13.08MB
2020-08-04 15:26:58.674750+0800 Demo[3199:27061906] PNG  原图大小 : 27.01MB
2020-08-04 15:26:58.921834+0800 Demo[3199:27061906] JPEG 压缩后大小 : 5.18MB
b、压缩中以及完成后询问是否需要删除原图
压缩中
......
2020-08-04 15:37:36.923420+0800 Demo[3236:27069366] 正在压缩第5张图片,图片总数为:7
2020-08-04 15:37:36.924194+0800 Demo[3236:27070606] 图片压缩前 imageDataLength: 1.81MB, imageSize:{4288, 2848}
2020-08-04 15:37:37.031834+0800 Demo[3236:27070606] 图片压缩后 imageDataLength: 0.77MB, imageSize:{4288, 2848}
2020-08-04 15:37:37.189333+0800 Demo[3236:27069366] 正在压缩第6张图片,图片总数为:7
2020-08-04 15:37:37.190074+0800 Demo[3236:27070036] 图片压缩前 imageDataLength: 2.39MB, imageSize:{4288, 2848}
2020-08-04 15:37:37.298245+0800 Demo[3236:27070036] 图片压缩后 imageDataLength: 0.81MB, imageSize:{4288, 2848}
......
询问是否需要删除原图
待优化图片全部压缩完成
相册中原图被删除只留下了压缩后的图片
清零

内存变化:

2020-08-04 16:24:46.466146+0800 Demo[3443:27100214] 压缩照片可省 :7.50MB
2020-08-04 16:28:40.280634+0800 Demo[3443:27100214] 压缩照片可省 :0.00MB

2、ClearPhotoManager文件

a、使用到的属性和提供的方法接口
// 瘦身图片数组
@property (nonatomic, strong, readwrite) NSMutableArray *thinPhotoArray;
// 瘦身图片信息
@property (nonatomic, strong, readwrite) NSDictionary *thinPhotoInfo;
// 瘦身可以节省的空间大小
@property (nonatomic, assign) NSUInteger thinPhotoSaveSpace;

/// 获取原图
+ (void)getOriginImageWithAsset:(PHAsset *)asset completionHandler:(void (^)(UIImage *result, NSDictionary *info))completion;

/// 压缩照片
+ (void)compressImageWithData:(NSData *)imageData completionHandler:(void (^)(UIImage *compressImage, NSUInteger compresSize))completion;

相关属性的懒加载方法的实现:

- (NSMutableArray *)thinPhotoArray
{
    if (!_thinPhotoArray)
    {
        _thinPhotoArray = [NSMutableArray array];
    }
    return _thinPhotoArray;
}
b、dealImageWithIndex: 中清理相似图片的部分
// 图片瘦身
- (void)dealThinPhotoWithAsset:(PHAsset *)asset exactImage:(UIImage *)exactImage originImageData:(NSData *)originImageData
{
    // 原图大小已经满足大小,无需瘦身
    if (originImageData.length < 1024.0 * 1024.0 * 1.5)
    {
        return;
    }
    
    // 否则将当前需瘦身图片加入到瘦身数组
    NSDictionary *itemDictionary = @{ @"asset" : asset, @"exactImage" : exactImage, @"originImageData" : originImageData, @"originImageDataLength" : @(originImageData.length)};
    [self.thinPhotoArray addObject:itemDictionary];
    
    // 瘦身空间 = 原图大小 - 1024.0 * 1024.0
    self.thinPhotoSaveSpace = self.thinPhotoSaveSpace + (originImageData.length - 1024.0 * 1024.0);
}

这里就直接将字典作为元素加入数据源数组中就好了,不用再那么麻烦嵌套一层,简单了很多。

c、获取原图方法的实现
// 获取原图
+ (void)getOriginImageWithAsset:(PHAsset *)asset completionHandler:(void (^)(UIImage * _Nonnull, NSDictionary * _Nonnull))completion
{
    PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
    // deliveryMode 则用于控制请求的图片质量
    options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
    // resizeMode 属性控制图像的剪裁
    options.resizeMode = PHImageRequestOptionsResizeModeExact;
    
    // 原图
    PHImageManager *imageManager = [PHImageManager defaultManager];
    [imageManager requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:options resultHandler:completion];
}
d、获取压缩图方法的实现

压缩图片算法,经过图片质量压缩后还没满足要求达到的压缩大小,则再对图片宽高尺寸进行压缩。

#pragma mark - 图片压缩

// 压缩照片
+ (void)compressImageWithData:(NSData *)imageData completionHandler:(void (^)(UIImage * _Nonnull, NSUInteger))completion
{
    UIImage *image = [UIImage imageWithData:imageData];
    NSUInteger imageDataLength = imageData.length;
    [self compressImage:image imageDataLength:imageDataLength completionHandler:completion];
}

// 在子线程压缩图片后在主线程显示压缩后的图片
+ (void)compressImage:(UIImage *)image imageDataLength:(NSUInteger)imageDataLength completionHandler:(void (^)(UIImage *compressImage, NSUInteger compresSize))completion
{
    // 在子线程压缩
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 压缩照片
        NSDictionary *imageDictionary = [self compressImage:image imageDataLength:imageDataLength];
        
        // 在主线程显示压缩后的图片
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion)
            {
                completion(imageDictionary[@"image"], [imageDictionary[@"imageDataLength"] unsignedIntegerValue]);
            }
        });
    });
}

// 压缩图片算法,经过图片质量压缩后还没满足要求达到的压缩大小,则再对图片宽高尺寸进行压缩
+ (NSDictionary *)compressImage:(UIImage *)image imageDataLength:(NSUInteger)imageDataLength
{
    NSLog(@"图片压缩前 imageDataLength: %.2fMB, imageSize:%@", imageDataLength/1024.0/1024.0, NSStringFromCGSize(image.size));
    
    // 压缩率
    CGFloat rate = 1024 * 1024.0 / imageDataLength;
    // 数据压缩
    NSData *data = UIImageJPEGRepresentation(image, rate);
    // 压缩后的图片
    UIImage *compressImage = [UIImage imageWithData:data];
    
    NSLog(@"图片压缩后 imageDataLength: %.2fMB, imageSize:%@", data.length / 1024.0 / 1024.0, NSStringFromCGSize(compressImage.size));
    
    if (data.length > 1024 * 1024 * 1.5)// 经过图片质量压缩后还没满足要求达到的压缩大小,则再对图片宽高尺寸进行压缩
    {
        // 按照压缩比率缩小宽高
        CGSize size = CGSizeMake(image.size.width * rate, image.size.height * rate);
        UIImage *compressImageSecond = [self imageWithImage:compressImage scaledToSize:size];
        NSData *dataSecond =  UIImageJPEGRepresentation(compressImageSecond, 1);
        
        NSLog(@"按照压缩比率缩小宽高后 imageDataLength: %.2fMB, imageSize:%@", dataSecond.length / 1024.0 / 1024.0, NSStringFromCGSize(compressImageSecond.size));
        
        if (dataSecond.length > 1024 * 1024 * 1.5)// 还没有达到要求则递归调用自己
        {
            return [self compressImage:compressImageSecond imageDataLength:dataSecond.length];
        }
        else
        {
            // 压缩后的图片
            return @{@"image":compressImageSecond, @"imageDataLength":@(dataSecond.length)};
        }
    }
    else
    {
        // 压缩后的图片
        return @{@"image":compressImage, @"imageDataLength":@(data.length)};
    }
}

3、创建视图

创建视图

Model和前面两个一样,View比较简单,图省事放到了ThinPhotoViewController中。

a、View部分
#pragma mark - Life Circle

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.title = @"照片瘦身";
    self.view.backgroundColor = [UIColor whiteColor];

    // 创建视图
    [self createSubviews];
    [self configData];
}

// 创建视图
- (void)createSubviews
{
    CGFloat distance = 15;
    CGFloat singleViewWidth = (self.view.frame.size.width - 3 * distance) / 2;

    // 左边视图
    UILabel *leftLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 100, singleViewWidth, 50)];
    leftLabel.textAlignment = NSTextAlignmentCenter;
    leftLabel.text = @"优化前";
    [self.view addSubview:leftLabel];
    
    UIImageView *leftImageViewView = [[UIImageView alloc] initWithFrame:CGRectMake(distance, 150, singleViewWidth, singleViewWidth * 2)];
    leftImageViewView.contentMode = UIViewContentModeScaleAspectFit;
    leftImageViewView.backgroundColor = [UIColor lightGrayColor];
    self.leftImageViewView = leftImageViewView;
    [self.view addSubview:self.leftImageViewView];

    // 右边对比视图
    UILabel *rightLabel = [[UILabel alloc] initWithFrame:CGRectMake(distance * 2 + singleViewWidth, 100, singleViewWidth, 50)];
    rightLabel.textAlignment = NSTextAlignmentCenter;
    rightLabel.text = @"优化后";
    [self.view addSubview:rightLabel];
    
    UIImageView *rightImageViewView = [[UIImageView alloc] initWithFrame:CGRectMake(distance * 2 + singleViewWidth, 150, singleViewWidth, singleViewWidth * 2)];
    rightImageViewView.contentMode = UIViewContentModeScaleAspectFit;
    rightImageViewView.backgroundColor = [UIColor lightGrayColor];
    self.rightImageViewView = rightImageViewView;
    [self.view addSubview:self.rightImageViewView];
    
    // 提示文本
    UILabel *tipLab = [[UILabel alloc] initWithFrame:CGRectMake(0, singleViewWidth * 2 + 150, self.view.frame.size.width, 100)];
    tipLab.text = [NSString stringWithFormat:@"%@ 可省 %@", self.thinPhotoItem.detail, self.thinPhotoItem.saveString];
    tipLab.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:tipLab];
    
    // 底部优化按钮
    UIButton *optmizeButton = [[UIButton alloc] initWithFrame:CGRectMake(distance, self.view.frame.size.height - 100, self.view.frame.size.width - 2 * distance, 50)];
    [optmizeButton setTitle:@"立即优化" forState:UIControlStateNormal];
    optmizeButton.backgroundColor = [UIColor orangeColor];
    [optmizeButton addTarget:self action:@selector(clickOptmizeButton) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:optmizeButton];
}
b、Data部分

获取显示在左边的原图和显示在右边的压缩图。这里显示数组中的第一张图。

#pragma mark - Data

// 配置数据
- (void)configData
{
    // 存放将传入的info进行格式化后的model
    self.dataArray = [NSMutableArray arrayWithCapacity:self.thinPhotoArray.count];
    for (NSDictionary *dict in self.thinPhotoArray)
    {
        // 初始化Model,传入info
        PhotoInfoItem *item = [[PhotoInfoItem alloc] initWithDict:dict];
        [self.dataArray addObject:item];
    }
    
    // 获取显示在左边的原图,这里显示数组中的第一张图
    PhotoInfoItem *item = self.dataArray.firstObject;
    [ClearPhotoManager getOriginImageWithAsset:item.asset completionHandler:^(UIImage * _Nonnull result, NSDictionary * _Nonnull info) {
        
        // 原图大小
        NSData *data = UIImageJPEGRepresentation(result, 1);
        NSLog(@"JPEG 原图大小 : %.2fMB", data.length/1024.0/1024.0);
        
        // 测试用,可删除。和JPEG 原图大小进行比较
        NSData *PNGData = UIImagePNGRepresentation(result);
        NSLog(@"PNG  原图大小 : %.2fMB", PNGData.length/1024.0/1024.0);
        
        // 左侧显示原图
        self.leftImageViewView.image = result;
    }];
    
    // 获取显示在右边的压缩图
    [ClearPhotoManager compressImageWithData:item.originImageData completionHandler:^(UIImage * _Nonnull compressImage, NSUInteger compresSize) {
        // 压缩后大小
        NSData *compressData = UIImageJPEGRepresentation(compressImage, 1);
        NSLog(@"JPEG 压缩后大小 : %.2fMB", compressData.length/1024.0/1024.0);
        
        // 右侧显示压缩图
        self.rightImageViewView.image = compressImage;
    }];
}

c、点击优化按钮,对全部可以优化的图片进行压缩

点击优化按钮,从第一张图片开始进行压缩优化。

// 点击优化按钮
- (void)clickOptmizeButton {
    // 相册变更不处理
    [ClearPhotoManager shareManager].notificationStatus = PhotoNotificationStatusClose;
    // 当前位置
    self.currentIndex = 0;
    // 用来存储待删除资源
    self.assetArrary = [NSMutableArray array];
    
    // 提示框
    MBProgressHUD *hud = [[MBProgressHUD alloc] initWithView:self.view];
    hud.label.text = @"压缩中,请稍后";
    hud.mode = MBProgressHUDModeDeterminate;
    hud.removeFromSuperViewOnHide = YES;
    [hud showAnimated:YES];
    self.hud = hud;
    [self.view addSubview:self.hud];
    
    // 从第一张图片进行压缩优化
    [self optmizeImageWithIndex:0];
}

optmizeImageWithIndex:方法通过Index获取对应图片并对其进行优化,存储压缩后的图片到"相册",并且从相册资源库中删除原图,index +1 后递归调用优化图片方法,直到所以可以优化的图片全部压缩完成。

// 通过Index获取对应图片并对其进行优化
- (void)optmizeImageWithIndex:(NSInteger)index
{
    // 更新当前位置
    self.currentIndex = index;
    
    // 显示进度:正在压缩第几张图片 / 图片总数
    self.hud.progress = (CGFloat)index + 1 / self.dataArray.count;
    NSLog(@"正在压缩第%ld张图片,图片总数为:%ld", index+1, self.dataArray.count);
    
    // 压缩结束条件
    if (index >= self.dataArray.count)
    {
        // 压缩完成隐藏提示框
        NSLog(@"恭喜,压缩图片全部顺利完成");
        self.hud.hidden = YES;
        
        // 通知相册变更主动处理
        [ClearPhotoManager shareManager].notificationStatus = PhotoNotificationStatusNeed;
        
        // 从相册资源库中删除原图
        [ClearPhotoManager deleteAssets:self.assetArrary completionHandler:^(BOOL success, NSError * _Nonnull error) {
            
            if (success)
            {
                [ClearPhotoManager tipWithMessage:@"恭喜,压缩图片全部顺利完成,并且已经删除了原图"];
            }
            else
            {
                 NSLog(@"未删除原图");
            }
        }];
        return;
    }

    // 取出当前model
    PhotoInfoItem *item = self.dataArray[index];
    [ClearPhotoManager compressImageWithData:item.originImageData completionHandler:^(UIImage * _Nonnull compressImage, NSUInteger compresSize) {
        
        // 存储压缩后的图片到"相册"
        UIImageWriteToSavedPhotosAlbum(compressImage, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
    }];
}


// 成功保存图片到相册中, 必须调用此方法, 否则会报参数越界错误
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
    if (error)
    {
        [ClearPhotoManager tipWithMessage:@"当前图片保存到相册失败"];
        NSLog(@"当前图片保存到相册失败");
    }
    else
    {
        // 将当前优化的item的asset加入到待删除资源数组中
        PhotoInfoItem *item = self.dataArray[self.currentIndex];
        [self.assetArrary addObject:item.asset];
    }
    
    // +1 后递归调用优化图片方法
    [self optmizeImageWithIndex:self.currentIndex + 1];
}


四、内存泄漏问题的修复

1、内存泄漏问题

IOS 相册里图片很多,比如2000多张,会使内存飙升到1g多,导致APP崩溃了。

The app “ClearPhotoDemo” on 不才 quit unexpectedly.
Domain: IDEDebugSessionErrorDomain
Code: 4
Failure Reason: Message from debugger: Terminated due to memory issue

2、内存泄漏问题的原因

// 获取原图(原图大小)
[imageManager requestImageDataAndOrientationForAsset:self.assetArray[index] options:self.imageSizeRequestOptions resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, CGImagePropertyOrientation orientation, NSDictionary * _Nullable info) {
    
    // 处理图片,分别传入缩略图和原图
    [weakSelf dealImageWithIndex:index exactImage:result originImageData:imageData];
}];

3、暂时的解决方案

将相册进行了分类,使加载的相片数量减少,避免奔溃问题。

// 如果已经授权, 获取相簿中的所有PHAsset对象
- (void)getClassificationAsset
{
    // 获取所有资源的集合,并按资源的创建时间排序,这样就可以通过和上一张图片判断日期来分组了
    PHFetchOptions *option = [[PHFetchOptions alloc] init];
    option.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
    option.predicate = [NSPredicate predicateWithFormat:@"mediaType == %ld", PHAssetMediaTypeImage];
    
    // 获取所有智能相册
    PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
    PHFetchResult *streamAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumMyPhotoStream options:nil];
    PHFetchResult *userAlbums = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
    PHFetchResult *syncedAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumSyncedAlbum options:nil];
    PHFetchResult *sharedAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumCloudShared options:nil];
    NSArray *arrAllAlbums = @[smartAlbums, streamAlbums, userAlbums, syncedAlbums, sharedAlbums];
    
    NSMutableArray *resultArray = [[NSMutableArray alloc] init];
    for (PHFetchResult *album in arrAllAlbums) {
        [album enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull collection, NSUInteger idx, BOOL *stop) {
            // 获取相册内asset result
            PHFetchResult *result = [PHAsset fetchAssetsInAssetCollection:collection options:option];
            if (!result.count) return;
            
            [resultArray addObject:result];
        }];
    }
    
    self.assetArray = resultArray[1];
    
    // 最初从第一张图片,数组中位置0的图片开始获取
    [self requestImageWithIndex:0];
}

4、期待前辈们完善修复方案

1、思路一
__block NSData *data;
@autoreleasepool {
    [imageManager requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
        NSLog(@"requestImageDataForAsset returned info(%@)", info);
        theData = [imageData copy];
    }];
}

该方案的问题是theData取值为空,需要考虑resultHandler的调用顺序问题。

2、其他思路

......


Demo在我的Github上,欢迎下载。
SolveProblemsDemo

参考文献

iOS照片清理功能,包括相似照片清理、截屏照片清理、图片压缩

你可能感兴趣的:(IOS 解决问题:相似、截屏照片清理和图片压缩)