ZXing原始碼解析二:掌握解碼步驟

wizardev發表於2019-06-16

前言:上篇文章已經讓原始碼執行起來了,但是還存在很多與掃描二維碼無關的程式碼,本篇將刪除無用的程式碼只保留與掃碼有關的程式碼,同時分析解碼的步驟。

精簡程式碼

  本篇文章的目標是分析出解碼的步驟,為了不被無關的程式碼干擾,將會對原始碼進行精簡,只保留與解碼有關的程式碼。

  主要刪減的程式碼就是識別出二維碼的內容後,一些其他的操作,如分享,記錄掃描的歷史,搜尋解析結果等。刪除之後的android模組的結構如下

Android模組結構
當然,這不是最終刪減的版本,可能在分析原始碼的時候,發現無用的程式碼,會繼續刪除,最終的程式碼,我會在文末給出Github連結。

原始碼分析

  為了方便理解及記憶ZXing解碼的步驟,我會邊分析邊畫UML的序列圖,最後,分析完解碼的步驟,會有一個完整的序列圖。現在,從主程式的入口開始分析,就是CaptureActivityonCreate方法。

onCreate原始碼分析

程式碼如下

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;
    //控制activity在一段時間無操作自動finish
    inactivityTimer = new InactivityTimer(this);
    //管理掃碼後是否有聲音和震動
    beepManager = new BeepManager(this);
    //用來根據環境的明暗,自動開啟關閉閃光燈
    ambientLightManager = new AmbientLightManager(this);
    //載入一些預設的配置
    PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
  }
複製程式碼

這個方法,主要是用來例項化一些物件和獲取配置資訊,上面的程式碼中已經有註釋,就不再細說。

onResume原始碼分析

下面繼續看Activity生命週期的第二個方法,程式碼如下

  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());//1

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

    resultView = findViewById(R.id.result_view);
    statusView = (TextView) findViewById(R.id.status_view);
    handler = null;
    //省略不重要程式碼
    //.....
    SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
    SurfaceHolder surfaceHolder = surfaceView.getHolder();//2
    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);//3
    }
  }
複製程式碼

上面程式碼中的一些語句標記了序號,現在來看序號“1”處的程式碼都做了什麼,進入CameraManager類的構造方法中,程式碼如下

public CameraManager(Context context) {
    this.context = context;
    this.configManager = new CameraConfigurationManager(context);//1.1
    previewCallback = new PreviewCallback(configManager);//1.2
  }
複製程式碼

繼續跟進程式碼,看下“1.1”處的程式碼,CameraConfigurationManager構造方法中做了什麼,程式碼如下

CameraConfigurationManager(Context context) {
    this.context = context;
  }
複製程式碼

上面的程式碼就是注入了context。現在看“1.2”處的程式碼,PreviewCallback構造方法中做了什麼,程式碼如下

PreviewCallback(CameraConfigurationManager configManager) {
    this.configManager = configManager;
  }
複製程式碼

上面的這段程式碼可以看出,在PreviewCallback構造方法中,將CameraConfigurationManager類的例項,注入到了PreviewCallback類中。跟完了“1”處的程式碼,繼續往下看onResume方法中的程式碼,這裡介紹一下“2”處的程式碼,SurfaceHolder的作用,介紹如下

SurfaceHolder是一個介面,其作用就像一個Surface的監聽器。提供訪問和控制SurfaceView背後的Surface 相關的方法 (providingaccess and control over this SurfaceView's underlying surface),它通過三個回撥方法,讓我們可以感知到Surface的建立、銷燬或者改變。

繼續往下看程式碼,因為在onCreate方法中,hasSurface值為false,所以,會進入else語句,也就是“3”處的程式碼,這句程式碼的作用就是繫結Surface的監聽器,就是在當前的Activity中繫結Surface生命週期的回撥方法。 SurfaceHolder.Callback 中定義了三個介面方法:

  • public void surfaceChanged(SurfaceHolder holder, int format, int width, int height); 當surface發生任何結構性的變化時(格式或者大小),該方法就會被立即呼叫。

  • public void surfaceCreated(SurfaceHolder holder); 當surface物件建立後,該方法就會被立即呼叫。

  • public void surfaceDestroyed(SurfaceHolder holder); 當surface物件在將要銷燬前,該方法會被立即呼叫。

知道了這三個方法在什麼時候會呼叫,所以,這裡繫結回撥之後,會首先呼叫surfaceCreated這個回撥方法,看下這個方法中的程式碼,如下

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

繼續跟進程式碼,看下initCamera(holder);方法都做了什麼,程式碼如下

private void initCamera(SurfaceHolder surfaceHolder) {
    if (surfaceHolder == null) {
      throw new IllegalStateException("No SurfaceHolder provided");
    }
    //相機已經開啟
    if (cameraManager.isOpen()) {
      Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?");
      return;
    }
    try {
    //開啟相機並初始化硬體引數
      cameraManager.openDriver(surfaceHolder);
      // 例項化一個handler並開始預覽.
      if (handler == null) {
        //3.1
        handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);
      }
      decodeOrStoreSavedBitmap(null, null);
    } catch (IOException ioe) {
      Log.w(TAG, ioe);
      displayFrameworkBugMessageAndExit();
    } catch (RuntimeException e) {
      // Barcode Scanner has seen crashes in the wild of this variety:
      // java.?lang.?RuntimeException: Fail to connect to camera service
      Log.w(TAG, "Unexpected error initializing camera", e);
      displayFrameworkBugMessageAndExit();
    }
  }
複製程式碼

“3.1”處的程式碼例項化了一個CaptureActivityHandler,看下CaptureActivityHandler的構造方法,程式碼如下

  CaptureActivityHandler(CaptureActivity activity,
                         Collection<BarcodeFormat> decodeFormats,
                         Map<DecodeHintType,?> baseHints,
                         String characterSet,
                         CameraManager cameraManager) {
    this.activity = activity;//注入activity
    //新建一個執行緒並啟動
    //3.1.1
    decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet,
        new ViewfinderResultPointCallback(activity.getViewfinderView()));
    decodeThread.start();
    state = State.SUCCESS;

    // 注入cameraManager
    this.cameraManager = cameraManager;
    //要求相機硬體開始將預覽幀繪製到螢幕上
    cameraManager.startPreview();
    //開始預覽,並且解碼
    //3.1.2
    restartPreviewAndDecode();
  }
複製程式碼

現在來看“3.1.1”處新建執行緒都做了什麼,DecodeThread構造方法的程式碼如下

 DecodeThread(CaptureActivity activity,
               Collection<BarcodeFormat> decodeFormats,
               Map<DecodeHintType,?> baseHints,
               String characterSet,
               ResultPointCallback resultPointCallback) {

    this.activity = activity;
    handlerInitLatch = new CountDownLatch(1);

    hints = new EnumMap<>(DecodeHintType.class);
    if (baseHints != null) {
      hints.putAll(baseHints);
    }

    // The prefs can't change while the thread is running, so pick them up once here.
    if (decodeFormats == null || decodeFormats.isEmpty()) {
      SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
      decodeFormats = EnumSet.noneOf(BarcodeFormat.class);
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_PRODUCT, true)) {
        decodeFormats.addAll(DecodeFormatManager.PRODUCT_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_INDUSTRIAL, true)) {
        decodeFormats.addAll(DecodeFormatManager.INDUSTRIAL_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_QR, true)) {
        decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_DATA_MATRIX, true)) {
        decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_AZTEC, false)) {
        decodeFormats.addAll(DecodeFormatManager.AZTEC_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_PDF417, false)) {
        decodeFormats.addAll(DecodeFormatManager.PDF417_FORMATS);
      }
    }
    hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);

    if (characterSet != null) {
      hints.put(DecodeHintType.CHARACTER_SET, characterSet);
    }
    hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, resultPointCallback);
  }
複製程式碼

上面的程式碼可以發現,線上程的構造方法中主要是設定解碼的格式。

如果想提升掃碼速度,這裡是一個可以優化的點,可以不用設定這麼多格式,只設定與自己業務有關的解碼格式。

都知道執行緒執行,會呼叫run方法,看下run方法中的程式碼,如下

public void run() {
    Looper.prepare();
    handler = new DecodeHandler(activity, hints);
    handlerInitLatch.countDown();
    Looper.loop();
  }
複製程式碼

這段程式碼的作用是在子執行緒中例項化了一個Handler與當前執行緒繫結。繼續跟進程式碼,看下DecodeHandler的構造方法都做了什麼,程式碼如下

DecodeHandler(CaptureActivity activity, Map<DecodeHintType,Object> hints) {
    multiFormatReader = new MultiFormatReader();
    multiFormatReader.setHints(hints);
    this.activity = activity;
  }
複製程式碼

這段程式碼的作用就是將執行緒構造方法中設定的hints設定給例項化的MultiFormatReader,同時注入CaptureActivity的例項。

MultiFormatReader類的作用是一個便利類,是大多數用途的庫的主要入口點。

分析到這裡,可以畫出如下的序列圖

第一部分序列圖

接著來分析“3.1.2”處的程式碼,呼叫的方法程式碼如下

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

重點看下cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);這句程式碼,看下CameraManager中的requestPreviewFrame方法做了什麼,程式碼如下

/**
   * A single preview frame will be returned to the handler supplied. The data will arrive as byte[]
   * in the message.obj field, with width and height encoded as message.arg1 and message.arg2,
   * respectively.
   *
   * @param handler The handler to send the message to.
   * @param message The what field of the message to be sent.
   */
  public synchronized void requestPreviewFrame(Handler handler, int message) {
    OpenCamera theCamera = camera;
    if (theCamera != null && previewing) {
      previewCallback.setHandler(handler, message);
      theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
    }
  }
複製程式碼

看下這個方法的介紹,意思是解析一個預覽幀,解析的資料是一個位元組陣列,放進了message.obj中,寬和高放到了message.arg1message.arg2中,然後將message返回給傳進來的handler,由前文可只,這個handlerDecodeHandler的例項。 好了,跟到這個方法,就不繼續網下跟了,這裡可以猜測一下,一幀影像解析後會回撥PreviewCallback類中的onPreviewFrame方法,這個方法的程式碼如下

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

不難看出,這裡是把解析後的資料傳送給了DecodeHandler,最終會呼叫DecodeHandler類中的handleMessage方法,程式碼如下

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

而上面程式碼中的message.what的值剛好是R.id.decode,自然就進入了decode方法。 分析到這裡,在來看下現在的時序圖,如下

ZXing原始碼解析二:掌握解碼步驟
根據這個序列圖和前文,可以知道以下內容

  • 進入掃碼介面會先例項化CameraManagerPreviewCallback類。
  • surface回撥方法中,初始化相機設定相機的配置引數。
  • 新建一個DecodeThread執行緒並啟動。為此執行緒繫結一個DecodeHandler
  • 獲取相機幀資料轉換程byte陣列傳回DecodeHandler進行解碼。

  上文已經完成相機獲取影像到進行解碼的原始碼分析,從前面的分析可以知道,解碼的方法是在子執行緒中執行的,那麼子執行緒解碼成功,怎麼通知主執行緒能,其實非常簡單,可以從DecodeHandler中的decode方法中知道答案,decode方法的程式碼如下

private void decode(byte[] data, int width, int height) {
    long start = System.nanoTime();
    Result rawResult = null;
    PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
    if (source != null) {
      BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
      try {
      //獲取解碼的結果
        rawResult = multiFormatReader.decodeWithState(bitmap);
      } catch (ReaderException re) {
        // continue
      } finally {
        multiFormatReader.reset();
      }
    }
    //獲取了CaptureActivity中的handler
    Handler handler = activity.getHandler();
    if (rawResult != null) {
      // Don't log the barcode contents for security.
      long end = System.nanoTime();
      Log.d(TAG, "Found barcode in " + TimeUnit.NANOSECONDS.toMillis(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傳送給CaptureActivity中的handler
        message.sendToTarget();
      }
    } else {
      if (handler != null) {
        Message message = Message.obtain(handler, R.id.decode_failed);
        message.sendToTarget();
      }
    }
  }
複製程式碼

從上面的程式碼中可以發現將解碼的結果傳送給主執行緒是利用Android的Handler機制。

結束語

  因為本文的目標是掌握解碼的步驟,所以一些細節性的程式碼並沒有進行分析,如配置相機的引數,掃碼後的影像是豎屏還是橫屏,怎樣獲取最佳的影像資料進行解析等。細節性的東西將會放到後面的文章進行講解,後面的文章還會分析具體是怎麼獲取影像上的二維碼並進行解碼的。

  點選這裡獲取原始碼

本文已由公眾號“AndroidShared”首發

歡迎關注我的公眾號
掃碼關注公眾號,回覆“獲取資料”有驚喜

相關文章