zxing 源代码分析 - 代码扫描过程分析
前言
zxing用于Java、Android的条形码扫描库,我们日常使用的APP中的二维码扫码功能绝大对数都是基于zxing项目做二次开发的,本文就此对zxing源码进行深入分析。
zxing项目代码较多,我们只对其中相对重要的部分进行分析,旨在理清二维码扫码识别的流程,了解zxing项目的工作原理。至于二维码的原理,请参考 二维码的生成细节和原理。
zxing github:https://github.com/zxing/zxing
zxing 目录结构
zxing github目录文件较多,但其实我们只需要查看android-core、android、core三个文件夹即可。
- android-core:这里面只有一个类,包含配置Android摄像头的一些实用方法。
- android:Android扫码二维码的Demo,也就是需要我们重点分析的源码。
- core:纯java源码,可以理解为二维码生成和识别的核心包。
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,包括打开,关闭,配置预览参数,闪光灯等。
- 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扫码流程:
- 初始化相机,设置一些相机参数;
- 绑定SurfaceView,在SurfaceView上显示预览图像;
- 获取相机的一帧图像,将图像数据传递到异步的handler中解析;
- 对图像数据进行解析,解析成功进入下一步,不成功回到第3步;
- 返回解析结果并退出。
上一篇: Java 生成、解析 QR 代码
下一篇: 用 python 生成和解析 Qr 代码
推荐阅读
-
Rancher 1024 Decoding Challenge 解密解题全过程分析(附代码)
-
使用Pyecharts进行苏州旅游攻略的可视化分析——源代码公开
-
【摩尔线程+Colossal-AI强强联手】MusaBert登上CLUE榜单TOP10:技术细节揭秘 - 技术实力:摩尔线程凭借"软硬兼备"的技术底蕴,让MusaBert得以从底层优化到顶层。其内置多功能GPU配备AI加速和并行计算模块,提供了全面的AI与科学计算支持,为AI推理和低资源条件下的大模型训练等场景带来了高效、经济且环保的算力。 - 算法层面亮点:依托Colossal-AI AI大模型开发系统,MusaBert在训练过程中展现出了卓越的并行性能与易用性,特别在预处理阶段对DataLoader进行了优化,适应低资源环境高效处理海量数据。同时,通过精细的建模优化、领域内数据增强以及Adan优化器等手段,挖掘和展示了预训练语言模型出色的语义理解潜力。基于MusaBert,摩尔线程自主研发的MusaSim通过对比学习方法微调,结合百万对标注数据,MusaSim在多个任务如语义相似度、意图识别和情绪分析中均表现出色。 - 数据资源丰富:MusaBert除了自家高质量语义相似数据外,还融合了悟道开源200GB数据、CLUE社区80GB数据,以及浪潮公司提供的1TB高质量数据,保证模型即便在较小规模下仍具备良好性能。 当前,MusaBert已成功应用于摩尔线程的智能客服与数字人项目,并广泛服务于语义相似度、情绪识别、阅读理解与声韵识别等领域。为了降低大模型开发和应用难度,MusaBert及其相关高质量模型代码已在Colossal-AI仓库开源,可快速训练优质中文BERT模型。同时,通过摩尔线程与潞晨科技的深度合作,仅需一张多功能GPU单卡便能高效训练MusaBert或更大规模的GPT2模型,显著降低预训练成本,进一步推动双方在低资源大模型训练领域的共享目标。 MusaBert荣登CLUE榜单TOP10,象征着摩尔线程与潞晨科技联合研发团队在中文预训练研究领域的领先地位。展望未来,双方将携手探索更大规模的自然语言模型研究,充分运用上游数据资源,产出更为强大的模型并开源。持续强化在摩尔线程多功能GPU上的大模型训练能力,特别是在消费级显卡等低资源环境下,致力于降低使用大模型训练的门槛与成本,推动人工智能更加普惠。而潞晨科技作为重要合作伙伴,将继续发挥关键作用。
-
jQuery 源代码位置分析
-
elasticsearch 源代码分析-05 片分配
-
Java 基础 ----- 基类 对象源代码分析
-
ffmpeg 源代码分析:av_register_all 和 avcodec_register_all - II. avcodec_register_all
-
ffmpeg 源代码分析(三)av_malloc、AVIOContext、AVFrame、avio_open2 等。- av_realloc
-
FFmpeg 源代码分析:av_parser_init
-
NuPlayer 播放框架的 GenericSource 源代码分析