前言
之前做过一个关于二维码的组件,已发布,现总结下。
开发的APP
所需支持的最低版本为8.0
,最初的方案为扫描使用苹果自带的API
实现扫一扫的功能、使用ZXing
识别从相册或别人转发的二维码图片。但发现ZXing
识别从相册中来的图片性能很差,很多图片识别不了,且耗时较长,遂使用ZBar
来实现识别从相册或别人转发的二维码图片。
这个组件重要实现了三个功能,扫一扫识别二维码图片、长按图片识别二维码图片和生成二维码图片。
首先来看下扫一扫识别二维码图片的代码实现:
功能实现
扫一扫识别二维码图片
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
| - (void)initCapture { AVCaptureDevice* inputDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; [inputDevice lockForConfiguration:nil]; if ([inputDevice hasTorch]){ inputDevice.torchMode = AVCaptureTorchModeAuto; } AVCaptureFocusMode foucusMode = AVCaptureFocusModeContinuousAutoFocus; if ([inputDevice isFocusModeSupported:foucusMode]) { inputDevice.focusMode = foucusMode; } [inputDevice unlockForConfiguration]; AVCaptureDeviceInput *captureInput = [AVCaptureDeviceInput deviceInputWithDevice:inputDevice error:nil]; if (!captureInput) { UIAlertController *alterVC = [UIAlertController alertControllerWithTitle:MUIQRCodeLocalizedString(@"ScanViewController_system_tip") message:MUIQRCodeLocalizedString(@"ScanViewController_camera_permission") preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:MUIQRCodeLocalizedString(@"ScanViewController_yes") style:UIAlertActionStyleDefault handler:nil]; [alterVC addAction:confirmAction]; [self presentViewController:alterVC animated:YES completion:nil]; [self.activityView stopAnimating]; [self onVideoStart: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]; CGFloat p1 = bounds.size.height/bounds.size.width; CGFloat p2 = w/h; CGRect cropRect = CGRectMake(CGRectGetMinX(_cropRect) - kSNReaderScanExpandWidth, CGRectGetMinY(_cropRect) - kSNReaderScanExpandHeight, CGRectGetWidth(_cropRect) + 2*kSNReaderScanExpandWidth, CGRectGetHeight(_cropRect) + 2*kSNReaderScanExpandHeight);
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); } }
|
识别二维码图片
识别二维码图片的功能,最初的方案是使用三方库ZXing
来实现,因为ZXing
有人在维护,但ZXing
识别相册中的二维码图片或本地的图片时,有些图片根本就识别不出来,且耗时较长,所以改为使用ZBar
。在网上找到一篇文章再见ZXing 使用系统原生代码处理QRCode,实测发现使用系统原生代码来识别二维码图片时,在,iphone4s,系统为iOS9的手机发现传回来的数组为空。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| - (NSString *)decodeQRImageWith:(UIImage*)aImage { NSString *qrResult = 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,也就是说
1
| CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
|
CIDetector的初始化方法无效。推测是苹果API的问题。
生成二维码图片
在iOS8
及以上版本使用苹果的API
生成二维码图片,代码如下:
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
| - (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的源码了。
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
| - (id <NSFastEnumeration>) scanImage: (CGImageRef) image { timer_start; int nsyms = [self scanImage: image withScaling: 0]; if(!nsyms && CGImageGetWidth(image) >= 640 && CGImageGetHeight(image) >= 640) nsyms = [self scanImage: image withScaling: .5];
NSMutableArray *syms = nil; if(nsyms) { 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); } 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
的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| -(UIImage *)TransformtoSize:(CGSize)Newsize { UIGraphicsBeginImageContext(Newsize); [self drawInRect:CGRectMake(0, 0, Newsize.width, Newsize.height)]; UIImage *TransformedImg=UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return TransformedImg; }
|
这样类似于将ZXing
中的tryHard
设置为YES
。识别不出来的二维码图片就可以识别了。
2016年5月20日
遗留的bug
: 点击进入扫一扫界面,退出,再进入,这样重复5次左右,扫一扫之前的界面的会出现卡顿。
原因:多次进入扫一扫界面,再退出,因此界面未被系统回收,captureSession对象一直在运行,会造成内存泄露,引起上一个界面卡顿。
解决方案:在视图将要消失的时候,确保captureSession对象停止运行。
1 2 3 4 5 6
| - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if ([self.captureSession isRunning]) { [self.captureSession stopRunning]; } }
|
2018年4月28日
识别二维码图片优化
近期通过bugly
收集卡顿问题发现,二维码组件在识别二维码图片时,会出现卡顿问题。为优化识别速度,采用了三种方案,并分别进行测试,并对测试数据进行分析,最终挑选出最优的方案。
任务A:使用系统提供的CoreImage的CIDetector接口去识别二维码图片,返回对应的字符串;
任务B:使用zbar中的方法去识别二维码图片,返回对应的字符串。
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
| + (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; }
+ (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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| + (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,两者均执行完后,返回识别的结果;
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
| + (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均识别失败,两者均执行完后,返回识别的结果。
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
| + (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]; 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]; 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算法流程介绍