最近在做一个关于二维码的组件,已发布,现总结下。
开发的APP所需支持的最低版本为7.0,最初的方案为扫描使用苹果自带的API实现扫一扫的功能、使用ZXing识别从相册或别人转发的二维码图片。但发现ZXing识别从相册中来的图片性能很差,很多图片识别不了,且耗时较长,遂使用ZBar来实现识别从相册或别人转发的二维码图片。
这个组件重要有三个功能,扫一扫识别二维码图片、长按图片识别二维码图片和生成二维码图片。
扫一扫识别二维码图片
- (void)initCapture {
AVCaptureDevice* inputDevice =
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
[inputDevice lockForConfiguration:nil];
if ([inputDevice hasTorch]) {
inputDevice.torchMode = AVCaptureTorchModeAuto;
}
[inputDevice unlockForConfiguration];
AVCaptureDeviceInput *captureInput =
[AVCaptureDeviceInput deviceInputWithDevice:inputDevice error:nil];
if (!captureInput) {
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) {
UIAlertController *alterVC = [UIAlertController alertControllerWithTitle:@"系统提示" message:@"您已关闭相机使用权限,请至手机“设置->隐私->相机”中打开" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
[alterVC addAction:confirmAction];
[self presentViewController:alterVC animated:YES completion:nil];
} else {
UIAlertController *alterVC = [UIAlertController alertControllerWithTitle:@"系统提示" message:@"未能找到相机设备" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
[alterVC addAction:confirmAction];
[self presentViewController:alterVC animated:YES completion:nil];
}
return;
}
AVCaptureMetadataOutput *captureOutput = [[AVCaptureMetadataOutput alloc] init];
[captureOutput setMetadataObjectsDelegate:self queue:_queue];
self.captureOutput = captureOutput;
self.captureSession = [[AVCaptureSession alloc] init];
[self.captureSession addInput:captureInput];
[self.captureSession addOutput:captureOutput];
CGFloat w = 1920.f;
CGFloat h = 1080.f;
if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) {
self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
} else if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
w = 1280.f;
h = 720.f;
} else if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
self.captureSession.sessionPreset = AVCaptureSessionPreset640x480;
w = 960.f;
h = 540.f;
}
captureOutput.metadataObjectTypes = [captureOutput availableMetadataObjectTypes];
CGRect bounds = [[UIScreen mainScreen] bounds];
if (!self.prevLayer) {
self.prevLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
}
self.prevLayer.frame = bounds;
self.prevLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[self.view.layer insertSublayer:self.prevLayer atIndex:0];
// 计算rectOfInterest
CGFloat p1 = bounds.size.height/bounds.size.width;
CGFloat p2 = w/h;
CGRect cropRect = CGRectMake(CGRectGetMinX(_cropRect) - kQRReaderScanExpandWidth, CGRectGetMinY(_cropRect) - kQRReaderScanExpandHeight, CGRectGetWidth(_cropRect) + 2*kQRReaderScanExpandWidth, CGRectGetHeight(_cropRect) + 2*kQRReaderScanExpandHeight);
if (fabs(p1 - p2) < 0.00001) {
captureOutput.rectOfInterest = CGRectMake(cropRect.origin.y /bounds.size.height,
cropRect.origin.x/bounds.size.width,
cropRect.size.height/bounds.size.height,
cropRect.size.width/bounds.size.width);
} else if (p1 < p2) {
//实际图像被截取一段高
CGFloat fixHeight = bounds.size.width * w / h;
CGFloat fixPadding = (fixHeight - bounds.size.height)/2;
captureOutput.rectOfInterest = CGRectMake((cropRect.origin.y + fixPadding)/fixHeight,
cropRect.origin.x/bounds.size.width,
cropRect.size.height/fixHeight,
cropRect.size.width/bounds.size.width);
} else {
CGFloat fixWidth = bounds.size.height * h / w;
CGFloat fixPadding = (fixWidth - bounds.size.width)/2;
captureOutput.rectOfInterest = CGRectMake(cropRect.origin.y/bounds.size.height,
(cropRect.origin.x + fixPadding)/fixWidth,
cropRect.size.height/bounds.size.height,
cropRect.size.width/fixWidth);
}
}
长按图片识别二维码图片
识别图片使用的是ZBar,最初的方案为ZXing,因为ZXing有人在维护,但ZXing识别相册中的二维码图片或本地的图片,有些图片根本就识别不出来,且耗时较长,所以改为使用ZBar。在网上找到一篇文章再见ZXing 使用系统原生代码处理QRCode,实测发现在iOS9,iphone4s上传回来的数组为空。代码如下:
//decode
- (NSString *)decodeQRImageWith:(UIImage*)aImage {
NSString *qrResult = nil;
//iOS8及以上可以使用系统自带的识别二维码图片接口,但此api有问题,在一些机型上detector为nil。
// if (iOS8_OR_LATER) {
// CIContext *context = [CIContext contextWithOptions:nil];
// CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
// CIImage *image = [CIImage imageWithCGImage:aImage.CGImage];
// NSArray *features = [detector featuresInImage:image];
// CIQRCodeFeature *feature = [features firstObject];
//
// qrResult = feature.messageString;
// } else {
ZBarReaderController* read = [ZBarReaderController new];
CGImageRef cgImageRef = aImage.CGImage;
ZBarSymbol* symbol = nil;
for(symbol in [read scanImage:cgImageRef]) break;
qrResult = symbol.data ;
return qrResult;
}
无图无真相:
detector的值为nil,也就是说
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
CIDetector的初始化方法无效。推测是苹果API的问题。
生成二维码图片
在iOS8及以上版本使用苹果的API生成二维码图片,代码如下:
- (UIImage *)encodeQRImageWithContent:(NSString *)content size:(CGSize)size {
UIImage *codeImage = nil;
if (iOS8_OR_LATER) {
NSData *stringData = [content dataUsingEncoding: NSUTF8StringEncoding];
//生成
CIFilter *qrFilter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
[qrFilter setValue:stringData forKey:@"inputMessage"];
[qrFilter setValue:@"M" forKey:@"inputCorrectionLevel"];
UIColor *onColor = [UIColor blackColor];
UIColor *offColor = [UIColor whiteColor];
//上色
CIFilter *colorFilter = [CIFilter filterWithName:@"CIFalseColor"
keysAndValues:
@"inputImage",qrFilter.outputImage,
@"inputColor0",[CIColor colorWithCGColor:onColor.CGColor],
@"inputColor1",[CIColor colorWithCGColor:offColor.CGColor],
nil];
CIImage *qrImage = colorFilter.outputImage;
CGImageRef cgImage = [[CIContext contextWithOptions:nil] createCGImage:qrImage fromRect:qrImage.extent];
UIGraphicsBeginImageContext(size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, CGContextGetClipBoundingBox(context), cgImage);
codeImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRelease(cgImage);
} else {
codeImage = [QRCodeGenerator qrImageForString:content imageSize:size.width];
}
return codeImage;
}
iOS8以下使用libqrencode库来生成二维码图片。
代码完善
2015年12月11日
QA测试发现,服务端生成的二维码,使用ZBar识别不出来,但将这张图片保存到相册,然后发送就可以识别出来。最初的想法是要服务端修改生成的二维码,但安卓能够识别出来,此路不通,那只有看ZBar的源码了。
- (id <NSFastEnumeration>) scanImage: (CGImageRef) image
{
timer_start;
int nsyms = [self scanImage: image
withScaling: 0];
//没有识别出来,判断CGImageRef对象的宽和高是否大于640,大于或等于的话进行缩放再进行扫描
if(!nsyms &&
CGImageGetWidth(image) >= 640 &&
CGImageGetHeight(image) >= 640)
// make one more attempt for close up, grainy images
nsyms = [self scanImage: image
withScaling: .5];
NSMutableArray *syms = nil;
if(nsyms) {
// quality/type filtering
int max_quality = MIN_QUALITY;
for(ZBarSymbol *sym in scanner.results) {
zbar_symbol_type_t type = sym.type;
int quality;
if(type == ZBAR_QRCODE)
quality = INT_MAX;
else
quality = sym.quality;
if(quality < max_quality) {
zlog(@" type=%d quality=%d < %d\n",
type, quality, max_quality);
continue;
}
if(max_quality < quality) {
max_quality = quality;
if(syms)
[syms removeAllObjects];
}
zlog(@" type=%d quality=%d\n", type, quality);
if(!syms)
syms = [NSMutableArray arrayWithCapacity: 1];
[syms addObject: sym];
}
}
zlog(@"read %d filtered symbols in %gs total\n",
(!syms) ? 0 : [syms count], timer_elapsed(t_start, timer_now()));
return(syms);
}
在这里就产生了一个解决有些二维码图片识别不出来的解决思路:将传过来的UIImage
的宽和高设置为640,识别不出来再进行缩放识别。修改UIImage
的代码如下:
-(UIImage *)TransformtoSize:(CGSize)Newsize
{
// 创建一个bitmap的context
UIGraphicsBeginImageContext(Newsize);
// 绘制改变大小的图片
[self drawInRect:CGRectMake(0, 0, Newsize.width, Newsize.height)];
// 从当前context中创建一个改变大小后的图片
UIImage *TransformedImg=UIGraphicsGetImageFromCurrentImageContext();
// 使当前的context出堆栈
UIGraphicsEndImageContext();
// 返回新的改变大小后的图片
return TransformedImg;
}
这样类似于ZXing中的tryHard设置为YES。识别不出来的二维码图片就可以识别了。
2016年5月20日
bug: 点击进入扫一扫界面,退出,再进入,这样重复5次左右,扫一扫之前的界面的会出现卡顿。
原因:多次进入扫一扫界面,再退出,因此界面未被系统回收,captureSession对象一直在运行,会造成内存泄露,引起上一个界面卡顿。
解决方案:在视图将要消失的时候,确保captureSession对象停止运行。
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if ([self.captureSession isRunning]) {
[self.captureSession stopRunning];
}
}
2018年4月28日
识别二维码图片优化
近期通过bugly
收集卡顿问题发现,二维码组件在识别二维码图片时,会出现卡顿问题。为优化识别速度,采用了三种方案,并分别进行测试,并对测试数据进行分析,最终挑选出最优的方案。
任务A:使用系统提供的CoreImage的CIDetector接口去识别二维码图片,返回对应的字符串;
任务B:使用zbar中的方法去识别二维码图片,返回对应的字符串。
//任务A
+ (NSString *)useSystemMethodDecodeImage:(UIImage *)image {
NSString *resultString = nil;
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode
context:nil
options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
if (detector) {
CIImage *ciImage = [CIImage imageWithCGImage:image.CGImage];
NSArray *features = [detector featuresInImage:ciImage];
CIQRCodeFeature *feature = [features firstObject];
resultString = feature.messageString;
}
return resultString;
}
//任务B
+ (NSString *)useZbarMethodDecodeImage:(UIImage *)image {
UIImage *decodeImage = image;
if (decodeImage.size.width < 641) {
decodeImage = [decodeImage TransformtoSize:CGSizeMake(640, 640)];
}
QRCodeZBarReaderController* read = [QRCodeZBarReaderController new];
CGImageRef cgImageRef = decodeImage.CGImage;
QRCodeZBarSymbol *symbol = nil;
for(symbol in [read scanImage:cgImageRef]) break;
return symbol.data;
}
- 方案A:先执行任务A,如果获取到的字符串为空,再执行任务B。
+ (NSString *)planOneDecodeWithImage:(UIImage *)image index:(NSInteger)index{
NSMutableString *costTimeInfo = [NSMutableString stringWithFormat:@"%ld\r\n",index];
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
NSString *detectorString = [MUIQRCodeDecoder useSystemMethodDecodeImage:image];
CFAbsoluteTime detectorCostTime = (CFAbsoluteTimeGetCurrent() - startTime);
[costTimeInfo appendString:[NSString stringWithFormat:@"detector : %f ms\r\n",detectorCostTime *1000.0]];
NSAssert(detectorString.length > 0, @"detector fail!");
CFAbsoluteTime zbarStartTime = CFAbsoluteTimeGetCurrent();
NSString *zbarSymbolString = [MUIQRCodeDecoder useZbarMethodDecodeImage:image];
NSAssert(zbarSymbolString.length > 0, @"zbar fail!");
CFAbsoluteTime zbarCostTime = (CFAbsoluteTimeGetCurrent() - zbarStartTime);
[costTimeInfo appendString:[NSString stringWithFormat:@"zbar : %f ms\r\n",zbarCostTime *1000.0]];
CFAbsoluteTime totalCostTime = (CFAbsoluteTimeGetCurrent() - startTime);
[costTimeInfo appendString:[NSString stringWithFormat:@"total cost : %f ms\r\n",totalCostTime *1000.0]];
[costTimeInfo appendString:[NSString stringWithFormat:@"detectorString : %@ ms\r\n",detectorString]];
[costTimeInfo appendString:[NSString stringWithFormat:@"zbarSymbolString : %@ ms\r\n",zbarSymbolString]];
return [costTimeInfo copy];
}
- 方案B:同时执行任务A和任务B,两者均执行完后,返回识别的结果;
+ (NSString *)planTwoDecodeWithImage:(UIImage *)image index:(NSInteger)index {
__block NSMutableString *costTimeInfo = [NSMutableString stringWithFormat:@"%ld\r\n",index];
__block NSString *detectorString = nil;
__block NSString *zbarSymbolString = nil;
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
detectorString = [MUIQRCodeDecoder useSystemMethodDecodeImage:image];
NSAssert(detectorString.length > 0, @"detector fail!");
CFAbsoluteTime costTime = (CFAbsoluteTimeGetCurrent() - startTime);
[costTimeInfo appendString:[NSString stringWithFormat:@"detector : %f ms\r\n",costTime *1000.0]];
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
zbarSymbolString = [MUIQRCodeDecoder useZbarMethodDecodeImage:image];
NSAssert(zbarSymbolString.length > 0, @"zbar fail!");
CFAbsoluteTime costTime = (CFAbsoluteTimeGetCurrent() - startTime);
[costTimeInfo appendString:[NSString stringWithFormat:@"zbar : %f ms\r\n",costTime *1000.0]];
});
dispatch_group_notify(group, dispatch_get_global_queue(0,0), ^{
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
CFAbsoluteTime totalCostTime = (CFAbsoluteTimeGetCurrent() - startTime);
[costTimeInfo appendString:[NSString stringWithFormat:@"total cost : %f ms\r\n",totalCostTime *1000.0]];
[costTimeInfo appendString:[NSString stringWithFormat:@"detectorString : %@ ms\r\n",detectorString]];
[costTimeInfo appendString:[NSString stringWithFormat:@"zbarSymbolString : %@ ms\r\n",zbarSymbolString]];
return [costTimeInfo copy];
}
- 方案C:同时执行任务A和任务B
1、任务A先执行完且识别成功,返回识别结果;
2、任务B先执行完且识别成功,返回识别结果;
3、任务A和任务B均识别失败,两者均执行完后,返回识别的结果。
+ (NSString *)planThreeDecodeWithImage:(UIImage *)image index:(NSInteger)index {
__block NSMutableString *costTimeInfo = [NSMutableString stringWithFormat:@"%ld\r\n",index];
__block NSString *detectorString = nil;
__block NSString *zbarSymbolString = nil;
__block BOOL isNeedSendSignal = YES;
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
detectorString = [MUIQRCodeDecoder useSystemMethodDecodeImage:image];
//NSAssert(detectorString.length > 0, @"detector fail!");
CFAbsoluteTime costTime = (CFAbsoluteTimeGetCurrent() - startTime);
[costTimeInfo appendString:[NSString stringWithFormat:@"detector : %f ms\r\n",costTime *1000.0]];
if (detectorString.length > 0 && isNeedSendSignal) {
isNeedSendSignal = NO;
dispatch_semaphore_signal(semaphore);
}
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
zbarSymbolString = [MUIQRCodeDecoder useZbarMethodDecodeImage:image];
//NSAssert(zbarSymbolString.length > 0, @"zbar fail!");
CFAbsoluteTime costTime = (CFAbsoluteTimeGetCurrent() - startTime);
[costTimeInfo appendString:[NSString stringWithFormat:@"zbar : %f ms\r\n",costTime *1000.0]];
if (zbarSymbolString.length > 0 && isNeedSendSignal) {
isNeedSendSignal = NO;
dispatch_semaphore_signal(semaphore);
}
});
dispatch_group_notify(group, dispatch_get_global_queue(0,0), ^{
if (isNeedSendSignal) {
dispatch_semaphore_signal(semaphore);
}
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
CFAbsoluteTime totalCostTime = (CFAbsoluteTimeGetCurrent() - startTime);
[costTimeInfo appendString:[NSString stringWithFormat:@"total cost : %f ms\r\n",totalCostTime *1000.0]];
[costTimeInfo appendString:[NSString stringWithFormat:@"detectorString : %@ ms\r\n",detectorString]];
[costTimeInfo appendString:[NSString stringWithFormat:@"zbarSymbolString : %@ ms\r\n",zbarSymbolString]];
return [costTimeInfo copy];
}
测试数据如下所示:(取了前10张图片)
分析测试数据发现:
1、在测试第一张二维码图片时,总耗时均较大,如果第一次识别使用的是系统方法,耗时超过500ms,这也是为什么会出现卡顿的原因;
2、使用系统方法去识别二维码图片时,如果不是第一次去识别,耗时较小,在65ms以内;
3、使用zbar的方法去识别二维码图片,耗时均值在200ms以内;
4、在方案C中,如果第一次使用系统方法,耗时为226ms。
总结得出,从优化卡顿问题的角度出发,使用方案C最优,同时发现,如果使用系统方法能识别出二维码图片,在初始化之后(也就是第二次使用),耗时最短。同时因为在实际的使用场景中,图片是一张一张识别的,识别过程有一个间隔时间,如果已经使用系统方法识别过二维码图片,那下次识别就能达到最优。所以使用方案C的话,最差情况均值在200ms左右,最好的情况和方案A中第二次使用系统方法耗时基本一致。综合考虑,使用方案C。
小结
在实际的项目开发过程中,设想的情况和实际情况会存在偏差,需要自己时刻使用性能调优工具,根据数据去进行优化,而不能想当然的认为某种方式是最优的。
源码和demo请点这里
参考的文章链接如下
再见ZXing 使用系统原生代码处理QRCode
IOS二维码扫描,你需要注意的两件事
Zbar算法流程介绍
本文已经同步到我的个人技术博客: 传送门 ,欢迎常来^^。