阅读7分钟

二维码简单介绍

二维码(Quick Response Code,QRCode)是一种由水平和垂直两个方向上的线条设计而成的二维条形码,可以存储数据信息,本文主要是介绍二维码的读取(不涉及二维码的生成)

二维码读取

读取二维码就是通过扫描二维码图像来获取其中的数据信息,任何条形码的扫描都是基于视频采集,因此需要用到AVFoundation框架

以下是AVFoundation库的概述

The AVFoundation framework combines six major technology areas that together encompass a wide range of tasks for capturing, processing, synthesizing, controlling, importing and exporting audiovisual media on Apple platforms.

AVFoundation框架结合了六个主要技术领域,这些领域共同涵盖了在Apple平台上捕获,处理,合成,控制,导入和导出视听媒体的广泛任务。

对于二维码的读取,我们主要用到该库中的Capture部分,即AVCaptureSession类,以下是其概述

可以看到该类继承自NSObject,主要功能是 用于管理capture(捕获)活动并协调从输入设备到捕获设备的数据流

扫描过程概述

扫描二维码的过程即从摄像头捕获二维码图像(input)到解析出字符串内容(output)的过程,该过程主要就是通过AVCaptureSession对象来实现

AVCaptureSession 对象用于协调从输入到输出的数据流 ,在执行过程中,需要先将输入和输出添加到该对象中,然后通过发送 startRunning stopRunning 消息来启动或停止数据流,最后通过 AVCaptureVideoPreviewLayer 对象来将捕获的视频显示在屏幕上

其中,输入对象通常是 AVCaptureDeviceInput 对象,通过 AVCaptureDevice 的实例来获得,输出对象通常是 AVCaptureMetaDataOutput 对象,该对象是读取二维码的核心部分,需要结合 AVCaptureMetaDataOutputObjectsDelegate 协议结合使用,可以捕获在输入设备中找到的任何元数据(metadata就是元数据的意思),并将其转换为字符串的格式

接下来我们来结合代码详细说明每个过程

1. 导入AVFoundation框架

#import <AVFoundation/AVFoundation.h>

2. 判断权限

由于扫描二维码过程需要用到摄像头,因此我们需要设置摄像头的权限并进行判断

第一步:设置权限

有两种方式设置权限

  • 直接在info.plist文件中添加
  • 再通过source code的方式打开,可以看到自动添加了两行代码

    因此我们其实也可以直接在源代码中添加对应的代码,就是下面的方法

  • 通过source code的方式在info.plist文件中添加
  • <key>NSCameraUsageDescription</key>
    <string>获取相机权限</string>
    

    举一反三,对于其他的需要获取权限设置也可以通过如上两个方式实现,例如麦克风、地理位置等,如图

    第二步:判断权限

    代码简写了,核心的部分如下

    #pragma mark --判断权限
    -(void)judgeAuthority{
    //判断权限的方法
        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
            //要放到主线程中刷新
            dispatch_async(dispatch_get_main_queue(), ^{
                // 若已授权
                if (granted) {
                     //调用扫描二维码的方法
                } else {
                    //若未授权,提示弹窗
    

    且其中最核心的部分就是requestAccessForMediaType:(AVMediaType)mediaType completionHandler:(void (^)(BOOL granted))handler ; 这个方法,用于请求权限,包含两个参数

  • 第一个参数AVMediaType是媒体类型 有如下几种类型
  • 第二个参数是一个block块,写相关判断的代码即可 关于提示弹窗,我们用的是UIAlertController
  • 创建UIAlertController对象
  • NSString *title = @"请在iPhone的“设置-隐私-相机“选项中,允许App访问你的相机";
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:title preferredStyle:UIAlertControllerStyleAlert];
    
  • 创建UIAlertAction对象,即按钮
  • UIAlertAction *conform = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
                        NSLog(@"点击了确认按钮");
                    UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
                        NSLog(@"点击了取消按钮");
    
  • 将按钮添加到alert中
  • [alert addAction:conform];
    [alert addAction:cancel];
    
    [self presentViewController:alert animated:YES completion:nil];
    

    显示效果如下,首先会系统自动弹窗请求相机权限

    若点击了不允许,即无法拿到相机权限,显示弹窗

    3. 创建AVCaptureSession对象

    @property (nonatomic, strong) AVCaptureSession *captureSession;
    _captureSession = [[AVCaptureSession alloc]init];
    

    4. 为AVCaptureSession对象添加输入输出

    //1. 初始化设备
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    //2. 创建输入,基于device实例的输入
    AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];
    //3. 创建输出
    AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];
    //4. 添加输入输出
    [_captureSession addInput:deviceInput];
    [_captureSession addOutput:metadataOutput];
    

    5. 配置AVCaptureMetaDataOutput对象

    首先是设置代理,然后是设置元数据类型

    //1. 设置代理
    [metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    //2. 设置元数据类型,因为这里是二维码的扫描,所以数据类型是AVMetadataObjectTypeQRCode,注意是需要传入数组
    [metadataOutput setMetadataObjectTypes:@[AVMetadataObjectTypeQRCode]];
    

    6. 创建并设置AVCaptureVideoPreviewLayer对象来显示捕获到的视频

    @property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewLayer;//展示layer
    //1. 实例化预览涂层图层
    _videoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
    //2. 设置预览图层填充方式
    [_videoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
    //3. 设置图层的frame
    [_videoPreviewLayer setFrame:_viewPreview.layer.bounds];
    //4. 将图层添加到预览view的图层上
    [_viewPreview.layer addSublayer:_videoPreviewLayer];
    //5. 设置扫描范围,这里使用的是相对位置
    metadataOutput.rectOfInterest = CGRectMake(0.2f, 0.2f, 0.8f, 0.8f);
    

    这里用到了rectOfInterest这个属性,是用于设置元数据的搜索区域的,确定矩形,矩形的坐标原点位于左上角

    7. 实现代理方法

    #pragma mark --AVCaptureMetadataOutputObjectsDelegate
    - (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
        //判断是否正在读取数据
        if (!_isReading) {
            //没有读取,返回
            return;
        //若metadataObjects.count > 0,代表扫描到二维码
        if (metadataObjects.count > 0) {
            _isReading = NO;
            AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects[0];
            NSString *result = metadataObject.stringValue;
            if (self.resultBlock) {
                self.resultBlock(result);
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self.navigationController popViewControllerAnimated:YES];
    

    开发过程中遇到的问题和解决方法

    其实上面也有一些已经说了例如权限设置这些,以下还有几个点

    1. 程序运行黑屏

    AppDelegate.h中添加UIWindow属性

    @property (nonatomic, strong)UIWindow *window;
    

    2. 导航栏不显示title

    title属性是从UIViewController上面继承过来的,而不是UINavigationController上面的名字

    由于UINavigationController属于容器,所以最少需要一个RootVIewController

    然后在RootViewController的viewDidLoad设置title而不是在UINavigationController的subclass中设置

    self.navigationController.title = @"扫一扫";  //原本的代码
    self.title = @"扫一扫"; //修改为self即可
    

    3. 按钮不居中

    //原本的代码,发现按钮水平方向偏右,竖直方向”居中“
    btn.frame = CGRectMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0, 80, 40);
    //解决方法,水平方向 - 40 即可(还没有纠结原因)
    btn.frame = CGRectMake(self.view.bounds.size.width / 2.0 - 40 , self.view.bounds.size.height / 2.0, 80, 40);
    

    但其实有更好的方法就是直接设置center 即可

    //先确定frame,定长度和宽度
    btn.frame = CGRectMake(0,0, 80, 40);
    //确定中心,即坐标
    btn.center = self.view.center;
    

    4. block属性传值

    简单梳理block属性传值的大致代码

    //ViewController.m
    //点击按钮时调用jumpToScanVC方法
    [btn addTarget:self action:@selector(jumpToScanVC) forControlEvents:UIControlEventTouchUpInside];
    -(void)jumpToScanVC{
        SecondViewController *secondVC = [[SecondViewController alloc]init];
        secondVC.secondBlock = ^(NSString * _Nonnull string) {
            self.label.text = string;
        [self.navigationController pushViewController:secondVC animated:NO];
    
    // SecondViewController.h
    //定义block属性
    @interface SecondViewController : UIViewController
    @property (nonatomic, copy) void(^secondBlock)(NSString *string);
    
    // SecondViewController.m
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        if (_secondBlock) {
            _secondBlock(@"hahaha");
        [self.navigationController popViewControllerAnimated:NO];
    

    可以看到在第二个控制器中调用了block并将值传递到了第一个控制器

    5. UIAlertController的用法

    扫描结果显示并弹窗,但是控制器没有被成功pop出去,报错显示

    popViewControllerAnimated: called on <UINavigationController 0x101827e00> while an existing transition or presentation is occurring; the navigation stack will not be updated.
    

    其实就是我们UIAlertController的弹窗动画和pop控制器的动画冲突了

    但其实我们的navigation stack已经pop掉了,只是无法显示动画

    因此我们可以通过延迟执行来达到先完成弹窗动画,再完成pop动画

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self.navigationController popViewControllerAnimated:YES];
    

    6. 点击扫描后跳转了页面但是没有显示扫描框,控制器卡死

    原因是没有放到主线程中去刷新UI导致了卡死

    7. 防止NSTimer导致无法释放的问题

    因为设置了扫描线(其实就是一个装饰,显得专业一点),扫描线的移动用到了计时器,但是注意计时器的使用很可能导致内存无法释放的情况,所以我们要事先将NSTimer对象置空

    #pragma mark --结束
    -(void)stopRunning{
        //判断定时器是否正在工作,若还在工作另起暂停并置空
        if ([_timer isValid]) {
            //正在工作就使其失效
            [_timer invalidate];
            //并给定时器赋值nil
            _timer = nil;
        [self.captureSession stopRunning];
    

    运行界面演示

    二维码图片

    运行界面演示