CameraX:Android 相機庫開發實踐

前言

前段時間由於工做的須要對項目中的相機模塊進行了優化,咱們項目中的相機模塊是基於開源庫 CameraView 進行開發的。那次優化主要包括兩個方面,一個是相機的啓動速度,另外一個是相機的拍攝的清晰度的問題。由於時間倉促,那次只是在原來的代碼的基礎之上進行的優化,然而那份代碼自己存在一些問題,致使相機的啓動速度沒法進一步提高。因此,我準備本身開發一款功能完善,而且可拓展的相機庫,因而 CameraX 就誕生了。java

雖然去年學習了不少的 Android 的知識,可是這並無什麼驕傲的。我以爲若是一我的學習了不少的東西,可是卻沒有辦法作出屬於本身的東西,那麼即便學了也跟沒學同樣。相比於學習能力,我更看重人的創造力。因此我也將開發一個 Android 相機庫做爲我的 2019 年在 Android 上面要完成的目標之一。android

Android 相加開源庫的現狀

要使用 Android 相機實現圖片拍照功能自己並不複雜,Camera1 + SurfaceView 就能夠搞定。可是若是讓相機可以自由拓展,就須要花費不少的功夫。我所接觸的開源庫包括 Google 非官方的 CameraView,以及 CameraFragment. 兩個庫的設計有各自的優勢和缺點。git

開源庫 優勢 缺點
CameraView 1.支持基本的拍照、縮放等功能;2.支持自定義圖片的寬高比;3.支持多種預覽佈局方式; 1.每次獲取相機支持的尺寸的時候,會先將其組裝到一個有序的 Set 中,這個過程會佔用必定的啓動時間;2.不支持拍攝視頻;3.代碼堆砌,結構混亂
CameraFragment 1.支持拍攝照片和視頻;2.代碼結構清晰 1.不支持縮放;2.默認寬高比4:3,沒法運行時修改;3.必須基於 Fragment

以上是兩個開源庫的優勢和缺點,而咱們能夠結合它們的優缺點實現一個更加完善的相機庫,同時對性能的優化和用戶自定義配置,咱們也提供了更多的可用的接口。github

CameraX 總體結構設計

雖然文章的題目是相機開發實踐,可是咱們並不打算介紹太多關於如何使用 Camera API 的內容,由於本項目是開源的,讀者能夠自行 Fork 代碼進行閱讀。在這裏,咱們只對項目中的一些關鍵部分的設計思路進行說明。設計模式

相機總體架構

連接:www.processon.com/view/link/5…數組

以上是咱們相機庫的總體架構的設計圖,這裏筆者使用了 UML 建模進行基礎的架構設計(固然,並不是嚴格遵循 UML 建模的語言規則)。下面,咱們介紹下項目的關鍵部分的設計思路。緩存

Camera1 仍是 Camera2?

瞭解 Android 相機 API 的同窗可能知道,在 LoliPop 上面提出了 Camera2 API. 就筆者我的的實踐開發的效果來看,Camera2 相機的性能確實比 Camera1 要好得多,這體如今相機對焦的速率和相機啓動的速率上。固然,這和硬件也有必定的關係。Camera2 比 Camera1 使用起來確實複雜得多,但提供的能夠調用的 API 也更豐富。Camera2 的另外一個問題是國內的不少手機設備對 Camera2 的支持並很差。性能優化

對於這個問題,首先,咱們能夠根據系統的參數來判斷該設備是否支持 Camera2:bash

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static boolean hasCamera2(Context context) {
        if (context == null) return false;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return false;
        try {
            CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
            assert manager != null;
            String[] idList = manager.getCameraIdList();
            boolean notNull = true;
            if (idList.length == 0) {
                notNull = false;
            } else {
                for (final String str : idList) {
                    if (str == null || str.trim().isEmpty()) {
                        notNull = false;
                        break;
                    }
                    final CameraCharacteristics characteristics = manager.getCameraCharacteristics(str);

                    Integer iSupportLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
                    if (iSupportLevel != null && iSupportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                        notNull = false;
                        break;
                    }
                }
            }
            return notNull;
        } catch (Throwable ignore) {
            return false;
        }
    }
複製代碼

不過,即使上面方法返回的結果標明支持 Camera2,但相機仍然可能在啓動中出現異常。因此 CameraView 的解決方案是,相機啓動的方法返回一個 boolean 類型標明 Camera2 是否啓動成功,若是失敗了,就降級並使用 Camera1。可是降級的過程會浪費必定的啓動時間,所以,有人提出了使用 SharedPreferences 存儲降級的記錄,下次直接使用 Camera1 的解決方案。數據結構

上面兩種方案各自有優缺點,使用第二種方案意味着你要修改相機庫的源代碼,而咱們但願以一種更加靈活的方式提供給用戶選擇相機的權力。沒錯,就是策略設計模式

由於雖然 Camera1 和 Camera2 的 API 設計和使用不一樣,可是咱們並不須要知道內部如何實現,咱們只須要給用戶提供切換相機、打開閃光燈、拍照、縮放等的接口便可。在這種狀況下,固然使用門面設計模式是最好的選擇。

另外,對於 TextureView 仍是 SurfaceView 的選擇,咱們也使用了策略模式+門面模式的思路。

即。對於相機的選擇,咱們提供門面 CameraManager 接口,Camera1 的實現類 Camera1Manager 以及 Camera2 的實現類 Camera2Manager. Camera1Manager 和 Camera2Manager 又統一繼承自 BaseCameraManager. 這裏的 BaseCameraManager 是一個抽象類,用來封裝一些通用的相機方法。

因此問題到了是 Camera1Manager 仍是 Camera2Manager 的問題。這裏咱們提供了策略接口 CameraManagerCreator,它返回 CameraManager:

public interface CameraManagerCreator {

    CameraManager create(Context context, CameraPreview cameraPreview);
}
複製代碼

以及一個默認的實現:

public class CameraManagerCreatorImpl implements CameraManagerCreator {

    @Override
    public CameraManager create(Context context, CameraPreview cameraPreview) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && CameraHelper.hasCamera2(context)) {
            return new Camera2Manager(cameraPreview);
        }
        return new Camera1Manager(cameraPreview);
    }
}
複製代碼

所以,咱們只須要在相機的全局配置中指定本身的 CameraManager 建立策略就可使用指定的相機了。

全局配置

以前考慮指定 CameraManager 建立策略的時候,思路是直接對靜態的變量賦值的方式,不事後來考慮到對相機的支持的尺寸進行緩存的問題,因此將其設計了靜態單實例的類:

public class ConfigurationProvider {

    private static volatile ConfigurationProvider configurationProvider;

    private ConfigurationProvider() {
        if (configurationProvider != null) {
            throw new UnsupportedOperationException("U can't initialize me!");
        }
        initWithDefaultValues();
    }

    public static ConfigurationProvider get() {
        if (configurationProvider == null) {
            synchronized (ConfigurationProvider.class) {
                if (configurationProvider == null) {
                    configurationProvider = new ConfigurationProvider();
                }
            }
        }
        return configurationProvider;
    }

    // ... ...
}
複製代碼

除了指定一些全局的配置以外,咱們還能夠在 ConfigurationProvider 中緩存一些相機的信息,好比相機支持的尺寸的問題。由於相機所支持的尺寸屬於相機屬性的一部分,是不變的,咱們沒有必要獲取屢次,能夠將其緩存起來,下次直接使用。固然,咱們還提供了不使用緩存的接口:

public class ConfigurationProvider {

    // ...
    private boolean useCacheValues;
    private List<Size> pictureSizes;

    public List<Size> getPictureSizes(android.hardware.Camera camera) {
        if (useCacheValues && pictureSizes != null) {
            return pictureSizes;
        }
        List<Size> sizes = Size.fromList(camera.getParameters().getSupportedPictureSizes());
        if (useCacheValues) {
            pictureSizes = sizes;
        }
        return sizes;
    }

}
複製代碼

這樣,咱們在獲取相機支持的圖片尺寸信息的時候只須要傳入 Camera 便可使用緩存的信息。固然,緩存信息在某些極端的狀況下可能會帶來問題,好比從 Camera1 切換到 Camera2 的時候,須要清除緩存。

注:這裏緩存的時候應該使用 SoftReference,可是考慮到數據量不大,沒有這麼設計,之後會考慮修改。

輸出媒體文件的尺寸的問題

使用 Android 相機一個讓人頭疼的地方是計算尺寸的問題:由於相機支持的尺寸有三種,包括相片的支持尺寸、預覽的支持尺寸和視頻的支持尺寸。預覽的尺寸決定了用戶看到的畫面的清晰程度,可是真正拍攝出圖片的清晰度取決於相片的尺寸,同理輸出的視頻的尺寸取決於視頻的尺寸。

在 CameraView 中,它容許你指定一個圖片的尺寸,當沒有知足的要求的尺寸的時候會 Crash…這樣的處理方式是將其很差的,由於用戶根本沒法肯定相機最大的支持尺寸,而 CameraView 甚至沒有提供獲取相機支持尺寸的接口……

爲了解決這個問題,咱們首先提供了一系列用戶獲取相機支持尺寸的接口:

Size getSize(@Camera.SizeFor int sizeFor);

    SizeMap getSizes(@Camera.SizeFor int sizeFor);
複製代碼

這裏的 SizeFor 是基於註解的枚舉,咱們經過它來判斷用戶是但願獲取相片、預覽仍是視頻的尺寸信息。這裏的 SizeMap 是一個哈希表,從相機的寬高比映射到對應的尺寸列表。跟 CameraView 處理方式不一樣的是,咱們只有在調用上述方法的時候才計算圖片的寬高比信息,雖然調用下面的方法的時候會花費一丁點兒時間,可是相機的啓動速度大大提高了:

@Override
    public SizeMap getSizes(@Camera.SizeFor int sizeFor) {
        switch (sizeFor) {
            case Camera.SIZE_FOR_PREVIEW:
                if (previewSizeMap == null) {
                    previewSizeMap = CameraHelper.getSizeMapFromSizes(previewSizes);
                }
                return previewSizeMap;
            case Camera.SIZE_FOR_PICTURE:
                if (pictureSizeMap == null) {
                    pictureSizeMap = CameraHelper.getSizeMapFromSizes(pictureSizes);
                }
                return pictureSizeMap;
            case Camera.SIZE_FOR_VIDEO:
                if (videoSizeMap == null) {
                    videoSizeMap = CameraHelper.getSizeMapFromSizes(videoSizes);
                }
                return videoSizeMap;
        }
        return null;
    }
複製代碼

獲取了相機的尺寸信息的目的固然是將其設置到相機上面,因此咱們提供了兩個用來設置相機尺寸的接口:

void setExpectSize(Size expectSize);

    void setExpectAspectRatio(AspectRatio expectAspectRatio);
複製代碼

它們一個用來指按期望的輸出文件的尺寸,一個用來指按期望的圖片的寬高比。

OK,既然用戶能夠指定計算參數,那麼怎麼計算呢?這固然仍是用戶說了算的,由於咱們同樣在全局配置中爲用戶提供了計算的策略接口:

public interface CameraSizeCalculator {

    Size getPicturePreviewSize(@NonNull List<Size> previewSizes, @NonNull Size pictureSize);

    Size getVideoPreviewSize(@NonNull List<Size> previewSizes, @NonNull Size videoSize);

    Size getPictureSize(@NonNull List<Size> pictureSizes, @NonNull AspectRatio expectAspectRatio, @Nullable Size expectSize);

    Size getVideoSize(@NonNull List<Size> videoSizes, @NonNull AspectRatio expectAspectRatio, @Nullable Size expectSize);
}
複製代碼

固然,咱們也會提供一個默認的計算策略。在 CameraManager 內部,咱們會在須要的地方調用上述接口的方法以獲取最終的相機尺寸信息:

private void adjustCameraParameters(boolean forceCalculateSizes, boolean changeFocusMode, boolean changeFlashMode) {
        Size oldPreview = previewSize;
        long start = System.currentTimeMillis();
        CameraSizeCalculator cameraSizeCalculator = ConfigurationProvider.get().getCameraSizeCalculator();
        android.hardware.Camera.Parameters parameters = camera.getParameters();
        if (mediaType == Media.TYPE_PICTURE && (pictureSize == null || forceCalculateSizes)) {
            pictureSize = cameraSizeCalculator.getPictureSize(pictureSizes, expectAspectRatio, expectSize);
            previewSize = cameraSizeCalculator.getPicturePreviewSize(previewSizes, pictureSize);
            parameters.setPictureSize(pictureSize.width, pictureSize.height);
            notifyPictureSizeUpdated(pictureSize);
        }

        // ... ...
    }
複製代碼

性能優化

爲了對相機的性能進行優化,筆者但是花了大量的精力。由於在以前進行優化的時候積累了一些經驗,因此此次開發的時候就容易得多。下面是 TraceView 進行分析的圖:

Android 相機 TraceView 分析

能夠看出從相機當中獲取支持尺寸的自己會佔用必定時間的,而這種屬於相機固有的信息,通常是不會發生變化的,因此咱們能夠經過將其緩存起來來提高下一次打開相機的速率。

總體上,該項目的優化主要體如今幾個地方:

  1. 使用註解+常量取代枚舉:由於枚舉佔用的內存空間比較大,而單純使用註解沒法約束輸入參數的範圍。這在 enums 包下面能夠看到,這也是 Android 性能優化最多見的手段之一。

  2. 延遲初始化:咱們爲了達到只在使用到某些數據的時候才初始化的目的採用了延遲初始化的解決方案,好比 Size 的寬高比的問題:

public class Size {

    // ...

    private double ratio;

    public double ratio() {
        if (ratio == 0 && width != 0) {
            ratio = (double) height / width;
        }
        return ratio;
    }

}
複製代碼
  1. 數據結構的應用和選擇:選擇合適的數據結構和自定義數據結構每每能起到化腐朽爲神奇的做用。好比 SizeMap
public class SizeMap extends HashMap<AspectRatio, List<Size>> {
}
複製代碼

好比在列表數據結構的應用上面,使用 ArrayList 可是提早指定數組大小,減少數組擴容的次數:

public static List<Size> fromList(@NonNull List<Camera.Size> cameraSizes) {
        List<Size> sizes = new ArrayList<>(cameraSizes.size());
        for (Camera.Size size : cameraSizes) {
            sizes.add(of(size.width, size.height));
        }
        return sizes;
    }
複製代碼
  1. 緩存,這個咱們以前已經提到過,除了尺寸信息咱們還緩存了一些其餘的信息,具體能夠參考源碼。

  2. 異步線程:這個固然是最能提高應用相應速度的方式。它可以讓咱們不阻塞主線程,從而提高界面相應的速度。可是在相機開發的時候存在一個問題,即一般打開的相機的時候比較耗時,因此放在異步線程中;而開啓預覽處於主線程,這很容易由於線程執行的順序的問題致使一些難以預測的異常。在以前,筆者的解決方案是使用一個私有鎖來實現線程的控制。

總結

本次相機庫開發佔用的時間其實很少,更多的時間花費在了 UML 建模圖的設計和在真正開發以前收集資料信息。不得不說,若是你開發一個小的項目,不須要作什麼設計,直接就能夠上了,可是若是你設計一個比較複雜的庫,花費更多時間在 UML 建模上面是值得的,由於它能讓你的開發思路更加清晰。另外,爲了開發 Camera2,筆者不只找遍了開源庫,還翻譯了相關的官方文檔,這在開源項目中會一併奉上。

相機目前支持的功能

編號 功能
1 拍攝照片
2 拍攝視頻
3 指定使用 Camera1 仍是 Camera2
4 指定使用 TextureView 仍是 SurfaceView
5 閃光燈打開和關閉
6 自動對焦的選擇
7 前置和後置相機
8 快門聲
9 指定縮放的大小
10 指按期望的圖片大小
11 指按期望的圖片寬高比
12 獲取支持的圖片、預覽和視頻的尺寸信息
13 相機尺寸發生變化監聽
14 輸出視頻的文件位置
15 輸出視頻的時間長度
16 手指界面滑動的監聽
17 觸摸進行縮放
18 預覽自適應和裁剪等
19 緩存相機信息,清除和不適用緩存信息

最後是關於項目的一些小問題

該項目目前全部功能已經開發完畢,不過仍有一些小的問題須要完善:

  1. Camera2 預覽放大以後拍攝出的圖片沒有放大效果的問題;
  2. Camera1 拍攝出的圖片須要旋轉 90 度;
  3. Camera2 在屏幕旋轉成橫屏以後相機預覽須要同時選擇 90 度的問題;
  4. Camera1 和 Camera2 切換存在一些問題。

另外,因爲時間限制,該相機庫目前沒有進行嚴格的測試,因此建議使用的時候進行充分測試以後再使用。

是否會繼續完善該項目?

是的,包括對相機的功能進行充分測試。只是目前的時間結點,筆者有其餘的事務須要處理,因此先把它介紹給讀者。固然也但願可以有更多感興趣的朋友對該項目貢獻代碼。

項目地址:

  1. 項目地址:github.com/Shouheng88/…
  2. UML 建模圖地址:www.processon.com/view/link/5…
  3. 筆者翻譯的Camera2 文檔:github.com/Shouheng88/…
相關文章
相關標籤/搜索