问题描述
最近用户反馈iOS客户端进入餐厅首页后,过几秒钟后闪退了。
分析解决
原因内存泄漏导致。
运营上架了新菜品,配置了高清图片,列表同时展示多张高清图片内存不足。
首先要知道几点:
1.RN上的列表是没有复用机制的,这就导致列表上的所有图片对象都会同时被持有。
2.运营在后台配置了高清图片,因我司的餐厅模块依赖了哗啦啦平台(为了支持线下下单,购买了双屏机,餐品由双屏机操作录入),而哗啦啦提供的服务并不支持同一个菜品配置多张图片用于展示缩略图、高清图,同时也不支持动态获取指定尺寸的图片。
3.我司开发人员无法从技术上约束运营只能配置小尺寸图片。
解决方案:
iOS Native 添加图片裁剪接口给 js 使用。
对比前后
具体实现
思路:
1.先判断本地是否已经有裁剪好的图片可以使用,如果有就直接使用。
2.依赖SDWebImage下载图片。
3.根据指定尺寸裁剪图片。
4.保存图片到本地。
5.返回本地图片路径给js。
要注意的是需要控制图片处理的并发量,如果同时处理多张图片同样有内存问题。
一下为Native的代码,只要看 -getImage:resolve:rejecter
方法即可。
#define QD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define QD_UNLOCK(lock) dispatch_semaphore_signal(lock);
@interface ImageDownloadModule()
/// 图片处理并发数
@property (strong, nonatomic, nonnull) dispatch_semaphore_t imageHandleLock;
@end
@implementation ImageDownloadModule
RCT_EXPORT_MODULE()
- (instancetype)init
{
self = [super init];
if (self) {
self.imageHandleLock = dispatch_semaphore_create(3);
}
return self;
}
RCT_EXPORT_METHOD(getImage:(NSDictionary *)params
resolve:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSString *url = params[@"url"];
CGFloat width = [params[@"width"] integerValue];
CGFloat height = [params[@"height"] integerValue];
NSString *qdResizeMode = params[@"qdResizeMode"];
// 先查找本地是否存在
NSString *fileName = [self fileNameWithUrl:url width:width height:height resizeMode:qdResizeMode];
NSString *path = [[self diskCachePath] stringByAppendingPathComponent:fileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
resolve(@{ @"path": path });
return;
}
// 控制并发数
QD_LOCK(self.imageHandleLock)
// 依赖SDWebImage下载图片
__weak typeof(self) weakSelf = self;
[[SDWebImageManager sharedManager] loadImageWithURL:[NSURL URLWithString:url]
options:0
progress:^(NSInteger receivedSize,
NSInteger expectedSize,
NSURL * _Nullable targetURL)
{
// 下载过程
} completed:^(UIImage * _Nullable image,
NSData * _Nullable data,
NSError * _Nullable error,
SDImageCacheType cacheType,
BOOL finished,
NSURL * _Nullable imageURL)
{
// 获取图片完成
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 获取指定尺寸
UIViewContentMode mode = [qdResizeMode isEqualToString:@"contain"] ? UIViewContentModeScaleAspectFit : UIViewContentModeScaleAspectFill;
CGRect rect = [weakSelf aspectRectFromSize:image.size
toSize:CGSizeMake(width, height)
contentMode:mode];
// 根据尺寸裁剪
UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0f);
[image drawInRect:rect];
UIImage *imagez = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 内存优化
NSString *cacheKey = [SDWebImageManager.sharedManager cacheKeyForURL:imageURL];
[[SDImageCache sharedImageCache] removeImageFromMemoryForKey:cacheKey];
// 保存到本地
NSData *pngData = UIImagePNGRepresentation(imagez);
BOOL result = [pngData writeToFile:path atomically:YES];
if (result) {
resolve(@{ @"path": path });
} else {
NSError *err = [NSError errorWithDomain:@"" code:1 userInfo:nil];
reject(@"1", @"", err);
}
QD_UNLOCK(weakSelf.imageHandleLock)
});
}];
}
- (NSString *)fileNameWithUrl:(NSString *)url
width:(NSInteger)width
height:(NSInteger)height
resizeMode:(NSString *)resizeMode
{
@try {
NSString *fileName = [NSString stringWithFormat:@"%@_%ld_%ld_%@", resizeMode, (long)width, (long)height, url.lastPathComponent];
return fileName;
} @catch (NSException *exception) {
return @"";
} @finally {
}
}
- (NSString *)diskCachePath {
NSString *folderName = @"tmp";
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *path = [paths[0] stringByAppendingPathComponent:folderName];
NSFileManager *manager = [NSFileManager defaultManager];
BOOL isExist = [manager fileExistsAtPath:path];
if (!isExist) {
[manager createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:nil];
}
return path;
}
- (CGRect)aspectRectFromSize:(CGSize)fromSize
toSize:(CGSize)toSize
contentMode:(UIViewContentMode)contentMode
{
CGRect rect = CGRectZero;
// tosize 200x200
CGFloat scaleW = fromSize.width / toSize.width;
CGFloat scaleH = fromSize.height / toSize.height;
CGFloat scale;
if (contentMode == UIViewContentModeScaleAspectFit) {
scale = MAX(scaleW, scaleH);
} else if (contentMode == UIViewContentModeScaleAspectFill) {
scale = MIN(scaleW, scaleH);
} else {
scale = MIN(scaleW, scaleH);
}
CGFloat width = fromSize.width / scale;
CGFloat height = fromSize.height / scale;
CGFloat x = (toSize.width - width) * 0.5;
CGFloat y = (toSize.height - height) * 0.5;
rect = CGRectMake(x, y, width, height);
return rect;
}
@end
对于js端,包装一个使用方法与Image
相似的组件即可。
// 具体使用
内部实现
export class SizeImage extends Component {
static defaultProps = {
width: 100,
height: 100,
/** 只支持 cover 、 contain */
qdResizeMode: 'cover',
};
constructor(props) {
super(props);
const { source, width, height, qdResizeMode } = props || {}
const { uri } = source || {}
this.state = {
localPath: ''
}
const { ImageDownloadModule } = NativeModules
if (Platform.OS === 'ios' && ImageDownloadModule && uri) {
// 调用Native,获取缩略图localPath
ImageDownloadModule.getImage({
url: uri,
width,
height,
qdResizeMode,
}).then(res => {
const { path } = res || {}
this.setState({ localPath: path })
})
}
}
render() {
const { localPath } = this.state
const { style, source } = this.props || {}
const { uri } = source || {}
const { ImageDownloadModule } = NativeModules
if (Platform.OS === 'ios' && ImageDownloadModule && uri) {
return
} else {
return
}
}
}
总结
这种场景很少遇到,但就是这种场景需要去思考解决成长,给平淡的工作加点料。