zxing開源庫工作流程原始碼詳解

Royll發表於2018-11-07

程式碼獲取

作為移動客戶端開發者來說,對二維碼識別或二維碼生成相關的開發需求肯定並不陌生,Android開發二維碼相關的功能通常都會使用或參考大名鼎鼎的zxing庫。而本文則主要是通過原始碼分析一下該開源庫掃描二維碼的工作流程,對這塊能有個更深的瞭解。

首先使用git將專案程式碼clone到本地,新建專案,將zxing資料夾中的android以及core資料夾程式碼覆蓋到對應的目錄下,稍作一些修改即可執行一個簡單的二維碼掃描的示例應用。

整體流程

Demo程式碼執行起來後,會進入一個掃描的主功能介面,將掃描框對準一個二維碼即可彈出解析結果資訊的浮框。通過AndroidManifest.xml檔案中可以得知這個頁面對應的類為CaptureActivity.java,我們便從這個類開始,分析整個二維碼掃描的流程。

要分析一個Activity,當然要從它的生命週期所對應的各個方法說起。首先我們來看它的onCreate()方法:

@Override
public void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    //保持螢幕常亮
    Window window = getWindow();
    window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    setContentView(R.layout.capture);

    hasSurface = false;
    inactivityTimer = new InactivityTimer(this);
    beepManager = new BeepManager(this);
    ambientLightManager = new AmbientLightManager(this);

    PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
}
複製程式碼

這個方法程式碼不多,也很容易看懂,主要就是做一些初始化的工作。InactivityTimer主要是用來監聽當手機是使用電池而不是充電狀態時,如果5分鐘內沒有做任何操作,則主動finish掉activity。BeepManager負責掃描到結果後震動或鈴聲相關,AmbientLightManager則是負責控制閃光燈。

繼續往下走看onResume()方法:

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

    ...

    // CameraManager must be initialized here, not in onCreate(). This is necessary because we don't
	// want to open the camera driver and measure the screen size if we're going to show the help on
	// first launch. That led to bugs where the scanning rectangle was the wrong size and partially
	// off screen.
	cameraManager = new CameraManager(getApplication());

    viewfinderView = (ViewfinderView) findViewById(R.id.viewfinder_view);
    viewfinderView.setCameraManager(cameraManager);

    ...

    SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
    SurfaceHolder surfaceHolder = surfaceView.getHolder();
    if (hasSurface) {
        // The activity was paused but not stopped, so the surface still exists. Therefore
        // surfaceCreated() won't be called, so init the camera here.
        initCamera(surfaceHolder);
    } else {
        // Install the callback and wait for surfaceCreated() to init the camera.
        surfaceHolder.addCallback(this);
    }
}
複製程式碼

這個方法很重要,初始化了CameraManager,掃描二維碼毋庸置疑是需要用到相機,通過相機預覽的一幀一幀的圖片,去解析上面可能存在的二維碼資訊。而在最後面還初始化了SurfaceView,通過hasSurface來決定是走initCamera(surfaceHolder)還是surfaceHolder.addCallback(this)。在上面的onCreate()中我們可以看到hasSurface被初始化成false,所以這裡走的應該是else的程式碼塊。CaptureActivity實現了SurfaceHolder.Callback介面,因此該方法繫結了surfaceHolder的回撥。當SurfaceView新增到 activity 中時,會呼叫surfaceCreated()

@Override
public void surfaceCreated(SurfaceHolder holder) {
    if (holder == null) {
        Log.e(TAG, "*** WARNING *** surfaceCreated() gave us a null surface!");
    }
    if (!hasSurface) {
        hasSurface = true;
        initCamera(holder);
    }
}
複製程式碼

這裡我們看到會改變hasSurface的狀態,然後走initCamera(holder),和onResume()hasSurfacetrue時做的操作是一樣的:

cameraManager.openDriver(surfaceHolder);
// Creating the handler starts the preview, which can also throw a RuntimeException.
if (handler == null) {
    handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);
}
複製程式碼

cameraManager 開啟了驅動,並且把自己傳入一個CaptureActivityHandler物件中去,那這個CaptureActivityHandler看起來像是一個進行訊息通知的 Handler,它的具體作用又是什麼呢?我們來看看它的構造方法:

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();
}
複製程式碼

通過進入CaptureActivityHandler.java可以看到該類確實繼承了Handler,並且在它的構造方法中開啟了一個DecodeThread的執行緒,並且呼叫了cameraManagerstartPreview()方法:

Asks the camera hardware to begin drawing preview frames to the screen.

開啟相機預覽後,再看下面的restartPreviewAndDecode()

private void restartPreviewAndDecode() {
    if (state == State.SUCCESS) {
        state = State.PREVIEW;
        cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
        activity.drawViewfinder();
    }
}

public synchronized void requestPreviewFrame(Handler handler, int message) {
    OpenCamera theCamera = camera;
    if (theCamera != null && previewing) {
        previewCallback.setHandler(handler, message);
        theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
    }
}
複製程式碼

可以看到這個 handler 會一直傳遞到一個previewCallback物件中去,而PreviewCallbacksetOneShotPreviewCallback()方法的一個回撥,setOneShotPreviewCallback方法上的註釋說明:

Installs a callback to be invoked for the next preview frame in addition to displaying it on the screen. After one invocation, the callback is cleared. This method can be called any time, even when preview is live. Any other preview callbacks are overridden.

使用此方法註冊預覽回撥介面時,會將下一幀資料回撥給onPreviewFrame()方法,呼叫完成後這個回撥介面將被銷燬,也就是隻會回撥一次預覽幀資料。繼續順著這個方法走下去,看回撥方法onPreviewFrame()

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    Point cameraResolution = configManager.getCameraResolution();
    Handler thePreviewHandler = previewHandler;
    if (cameraResolution != null && thePreviewHandler != null) {
        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");
    }
}
複製程式碼

這裡將返回的 byte 陣列資料和預覽幀的寬高資訊通過 handler 進行通知,這個 handler 就是上文中傳過來的decodeThread.getHandler()previewMessageR.id.decode,目的就是把圖片資料拿到該執行緒中進行解析。我們跟進到DecodeHandler.java中檢視handleMessage()方法:

@Override
public void handleMessage(Message message) {
    if (message == null || !running) {
        return;
    }
    switch (message.what) {
        case R.id.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 = activity.getHandler();//CaptureActivityHandler
    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();
        }
    }
}
複製程式碼

上面程式碼很清晰,DecodeHandler 接收到R.id.decode的訊息後,會呼叫decode()方法去解析傳過來的圖片資料。經過一系列解析操作,得到結果。如果結果為不為空,則通過CaptureActivityHandler將解析成功的訊息傳到CaptureActivity中進行後續解析結果展示。而如果解析結果為空呢,說明二維碼資訊解析失敗了,傳了一個R.id.decode_failedCaptureActivityHandler中:

@Override
public void handleMessage(Message message) {
    switch (message.what) {

    	...

        case R.id.decode_failed:
            // We're decoding as fast as possible, so when one decode fails, start another.
            state = State.PREVIEW;
            cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
            break;
            
        ...
    }
}

複製程式碼

可以看到,解析失敗時,重新呼叫requestPreviewFrame獲取下一幀預覽照片,再拿去解析,知道返回正確結果或者手動退出。

整個過程的時序圖如下:

zxing開源庫工作流程原始碼詳解

相關文章