zxing源码分析——扫码流程分析

前言

zxing用于Java、Android的条形码扫描库,我们日常使用的APP中的二维码扫码功能绝大对数都是基于zxing项目做二次开发的,本文就此对zxing源码进行深入分析。

zxing项目代码较多,我们只对其中相对重要的部分进行分析,旨在理清二维码扫码识别的流程,了解zxing项目的工作原理。至于二维码的原理,请参考 二维码的生成细节和原理

zxing github:https://github.com/zxing/zxing

zxing 目录结构

zxing github目录文件较多,但其实我们只需要查看android-core、android、core三个文件夹即可。

zxing 目录结构
  • android-core:这里面只有一个类,包含配置Android摄像头的一些实用方法。
  • android:Android扫码二维码的Demo,也就是需要我们重点分析的源码。
  • core:纯java源码,可以理解为二维码生成和识别的核心包。
zxing - android 包目录:
zxing - android 包目录
  • book:搜索与展示书籍的相关类。
  • camera:操作摄像头的相关类。
  • clipboard:剪贴板。
  • encode:编码功能的各个组件集合。
  • history:扫描历史管理的相关类。
  • result:扫码结果的相关类。
  • share:将扫码结果分享出去。
  • wifi:扫码自动连接WIFI。

Demo的功能模块较多,我们只重点分析camera模块和扫码流程相关类,在实际开发过程中,也只对这几个模块和类进行二次开发,其他次要模块都会被裁减掉。

开启扫码流程

分析扫码二维码的流程,需要先找到程序入口。我们从CaptureActivity开始分析。

CaptureActivity是Demo的逻辑核心类,包含了各个模块manager的实例化、初始化动作,以及加载预览控件和处理显示扫码结果。

CaptureActivity在onCreate中只做了初始化动作,我们直接看onResume()方法:

    @Override
    protected void onResume() {
        super.onResume();

        ···
        
        //初始化CameraManager
        cameraManager = new CameraManager(getApplication());

        //ViewfinderView是扫码框控件,传入cameraManager为了获取扫码框的尺寸
        viewfinderView = (ViewfinderView) findViewById(R.id.viewfinder_view);
        viewfinderView.setCameraManager(cameraManager);

        ...

        //SurfaceView是图像预览控件
        SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
        SurfaceHolder surfaceHolder = surfaceView.getHolder();
        if (hasSurface) {
            initCamera(surfaceHolder);
        } else {
            surfaceHolder.addCallback(this);
        }
    }
    
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (holder == null) {
            Log.e(TAG, "*** WARNING *** surfaceCreated() gave us a null surface!");
        }
        if (!hasSurface) {
            hasSurface = true;
            //在SurfaceHolder加载完成以后初始化摄像头。
            initCamera(holder);
        }
    }
    
    private void initCamera(SurfaceHolder surfaceHolder) {
        
        ···
        
        try {
            //打开摄像头
            cameraManager.openDriver(surfaceHolder);
            // Creating the handler starts the preview, which can also throw a RuntimeException.
            if (handler == null) {
                //这个handler负责扫码流程的所有状态的传递
                handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);
            }
            decodeOrStoreSavedBitmap(null, null);
        } catch (IOException ioe) {
            ···
        } catch (RuntimeException e) {
            ···
        }
    }

在onResume和SurfaceHolder加载完成以后,调用了initCamera方法初始化摄像头,并且创建了一个CaptureActivityHandler,用来传递扫码流程中的所有状态。

上面代码可以看到,在CaptureActivity中调用了cameraManager.openDriver(surfaceHolder)来开启摄像头,于是我们来分析CameraManager类和摄像头模块。

启动摄像头

摄像头模块用于管理camera,包括打开,关闭,配置预览参数,闪光灯等。

camera 包目录
  • CameraFacing:枚举类,标明前置摄像头,后置摄像头。
  • OpenCamera:表示已经打开的Camera以及它的元数据。
  • OpenCameraInterface:用于打开Camera并获得数据。
  • AutoFocusManager:Camera自动对焦相关。
  • CameraConfigurationManager:用于读取,分析,设置Camera参数。
  • CameraConfigurationUtils:主要用于配置camera参数。
  • CameraManager:相机管理核心类,操作Camera的入口。
  • FrontLightMode:枚举类, 表示闪光灯的开,关,自动。
  • PreviewCallback:预览回调类。

注意,CameraConfigurationUtils原本是在android-core文件夹中的,在我的本地项目里为了方便查看,将CameraConfigurationUtils类直接copy到摄像头模块包中。CameraConfigurationUtils主要为CameraConfigurationManager服务的工具类。

CameraManager类封装了所有有关摄像头的操作,所有外部模块想操作摄像头都需要通过它。下面仅分析zxing的摄像头开启流程,关于摄像头具体操作细节请自行查看源码和API。

    public synchronized void openDriver(SurfaceHolder holder) throws IOException {
        OpenCamera theCamera = camera;
        if (theCamera == null) {
            //通过OpenCameraInterface打开摄像头
            theCamera = OpenCameraInterface.open(requestedCameraId);
            if (theCamera == null) {
                throw new IOException("Camera.open() failed to return object from driver");
            }
            camera = theCamera;
        }

        //初始化执行的操作
        if (!initialized) {
            initialized = true;
            //初始化相机的参数,选择最佳的预览分辨率,配置画面预览方向
            configManager.initFromCameraParameters(theCamera);
           
            ···
            
        }

        Camera cameraObject = theCamera.getCamera();
        Camera.Parameters parameters = cameraObject.getParameters();
        String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save these, temporarily
        try {
            //设置必要的参数,包括焦点,闪光灯等
            configManager.setDesiredCameraParameters(theCamera, false);
        } catch (RuntimeException re) {
            
            ···
            
        }
        
        //设置摄像头预览控件载体
        cameraObject.setPreviewDisplay(holder);
    }

CameraManager的openDriver方法,通过OpenCameraInterface打开摄像头,通过configManager初始化参数,并且在最后设置了摄像头预览控件载体,也就是说,openDriver方法执行完成之后,摄像头的开启和配置工作已经完成。那么在哪里启动了摄像头的预览呢?

我们还记得,在CaptureActivity的initCamera方法中,执行完openDriver方法后创建了一个CaptureActivityHandler对象,摄像头预览动作就执行在CaptureActivityHandler的构造函数里面。

获取一帧图像

CaptureActivityHandler对象负责扫码流程中的所有状态的传递,CaptureActivityHandler在构造函数中启动了摄像头的预览动作,并且执行了一个解码线程。

    CaptureActivityHandler(CaptureActivity activity,
                           Collection<BarcodeFormat> decodeFormats,
                           Map<DecodeHintType, ?> baseHints,
                           String characterSet,
                           CameraManager cameraManager) {
        this.activity = activity;
        //开启一条解码线程
        decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet,
                new ViewfinderResultPointCallback(activity.getViewfinderView()));
        decodeThread.start();
        state = State.SUCCESS;

        // Start ourselves capturing previews and decoding.
        this.cameraManager = cameraManager;
        //启动摄像头预览
        cameraManager.startPreview();
        //开始预览并解码
        restartPreviewAndDecode();
    }
    
    ···
    
    private void restartPreviewAndDecode() {
        if (state == State.SUCCESS) {
            state = State.PREVIEW;
            //请求摄像头的一帧图像数据,注意这里传入的是decodeThread的handler
            cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
            //重绘扫码框控件
            activity.drawViewfinder();
        }
    }

CaptureActivityHandler在开启摄像头预览以后,执行了restartPreviewAndDecode方法,通过cameraManager的requestPreviewFrame方法请求摄像头的一帧图像数据。那么我们再来看一下cameraManager的requestPreviewFrame方法:

    public synchronized void requestPreviewFrame(Handler handler, int message) {
        OpenCamera theCamera = camera;
        if (theCamera != null && previewing) {
            //传decodeThread的handler到previewCallback
            previewCallback.setHandler(handler, message);
            //请求camera的一帧预览画面
            theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
        }
    }

这里通过camera的setOneShotPreviewCallback方法,请求camera的一帧预览画面,并传入了previewCallback回调对象。我们看一下PreviewCallback的回调接口源码:

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        Point cameraResolution = configManager.getCameraResolution();
        Handler thePreviewHandler = previewHandler;
        if (cameraResolution != null && thePreviewHandler != null) {
            //拿到一帧预览画面数据,回传给handler处理
            Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
                    cameraResolution.y, data);
            message.sendToTarget();
            previewHandler = null;
        } else {
            Log.d(TAG, "Got preview callback, but no handler or resolution available");
        }
    }

获取一帧预览画面的流程:

  • 通过CaptureActivityHandler启动一条解码线程DecodeThread。
  • 调用cameraManager.startPreview()启动摄像头预览。
  • 调用restartPreviewAndDecode方法,通过 cameraManager的requestPreviewFrame方法设置PreviewCallback回调接口,并传入DecodeThread的DecodeHandler对象。
  • 在onPreviewFrame方法中拿到一帧预览数据,再通过之前传进来的DecodeHandler对象处理这一帧数据。

到这里我们已经拿到了一帧预览画面,并且已经交给了DecodeThread的DecodeHandler处理,那么很明显,DecodeThread就是用来将这一帧数据解码的线程。

图像解码流程

DecodeThread是负责图像解码这样耗时动作的子线程,它内部的DecodeHandler负责具体的图像解码。我们来看DecodeThread的源码:

    Handler getHandler() {
        //保证线程同步,防止handler为空。
        try {
            handlerInitLatch.await();
        } catch (InterruptedException ie) {
            // continue?
        }
        return handler;
    }

    @Override
    public void run() {
        Looper.prepare();
        //创建一个执行在子线程的handler
        handler = new DecodeHandler(activity, hints);
        handlerInitLatch.countDown();
        Looper.loop();
    }

DecodeThread在run()方法中创建了一个执行在子线程的DecodeHandler,前文我们已经知道,一帧预览画面的数据就是通过这个DecodeHandler进行处理的,那么DecodeHandler是如何处理的呢?

    @Override
    public void handleMessage(Message message) {
        if (message == null || !running) {
            return;
        }
        switch (message.what) {
            case R.id.decode:
                //一帧预览图像的数据就是先传递到这里
                //decode进行图像解码
                decode((byte[]) message.obj, message.arg1, message.arg2);
                break;
            case R.id.quit:
                running = false;
                Looper.myLooper().quit();
                break;
        }
    }

    private void decode(byte[] data, int width, int height) {
        long start = System.currentTimeMillis();
        
        //省略具体的解码过程
        ···     

        //获取主线程的handler,解码完成的结果传递到主线程处理
        Handler handler = activity.getHandler();
        if (rawResult != null) {
            // Don't log the barcode contents for security.
            long end = System.currentTimeMillis();
            Log.d(TAG, "Found barcode in " + (end - start) + " ms");
            if (handler != null) {
                //解码成功,解码完成的结果传递到主线程处理
                Message message = Message.obtain(handler, R.id.decode_succeeded, rawResult);
                Bundle bundle = new Bundle();
                bundleThumbnail(source, bundle);
                message.setData(bundle);
                message.sendToTarget();
            }
        } else {
            if (handler != null) {
                //解码失败
                Message message = Message.obtain(handler, R.id.decode_failed);
                message.sendToTarget();
            }
        }
    }

以上代码可以看到,图像数据在子线程的handler中完成了解码过程,不管成功还是失败,解码结果都会传递到主线程的handler中处理,下面来看主线程handler(CaptureActivityHandler)的处理过程:

    @Override
    public void handleMessage(Message message) {
        switch (message.what) {
            case R.id.restart_preview:
                //重新取一帧图像并执行解码
                restartPreviewAndDecode();
                break;
            case R.id.decode_succeeded:
                //解码成功
                state = State.SUCCESS;
                Bundle bundle = message.getData();
                Bitmap barcode = null;
                float scaleFactor = 1.0f;
                if (bundle != null) {
                    byte[] compressedBitmap = bundle.getByteArray(DecodeThread.BARCODE_BITMAP);
                    if (compressedBitmap != null) {
                        barcode = BitmapFactory.decodeByteArray(compressedBitmap, 0, compressedBitmap.length, null);
                        // Mutable copy:
                        barcode = barcode.copy(Bitmap.Config.ARGB_8888, true);
                    }
                    scaleFactor = bundle.getFloat(DecodeThread.BARCODE_SCALED_FACTOR);
                }
                //activity执行handleDecode方法。
                activity.handleDecode((Result) message.obj, barcode, scaleFactor);
                break;
            case R.id.decode_failed:
                //解码失败
                state = State.PREVIEW;
                //重新取一帧图像并执行解码
                cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
                break;
            
            ···
            
        }
    }

主线程handler处理执行结果,如果解码失败就再重新取一帧图像并执行解码,循环整个流程直到扫码成功;如果解码成功就在activity执行扫码成功的返回。至此,整个扫码流程就分析完成了。

总结

zxing扫码流程:

  1. 初始化相机,设置一些相机参数;
  2. 绑定SurfaceView,在SurfaceView上显示预览图像;
  3. 获取相机的一帧图像,将图像数据传递到异步的handler中解析;
  4. 对图像数据进行解析,解析成功进入下一步,不成功回到第3步;
  5. 返回解析结果并退出。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342