NDK 開發實戰 - 封裝 java 層 sdk 模型

關於 Ndk 開發,網上的資料比較少,這方面的書籍也很少。由於其涉及的知識很是廣,時常有哥們問我,東西那麼多到底要學到什麼程度呢?到底應該怎麼學?這期我給你們來作一個簡單回答,首先單純站在 Android 系統的角度來講,咱們能夠細分爲 Java 層和 Native(c/c++) 層。站在 Android 開發的角度來講,咱們又能夠細分爲精通 Android 開發和精通 c/c++ 開發。固然筆者以前在長沙從事 Android 開發,公司是不存在 c/c++ 工程師的,也就是說全部的開發工做調用 Android Framework 層的 Api 就都能實現。java

來到深圳作音視頻項目,公司有專門的引擎部門,也就是說有專門的 c/c++ 音視頻工程師。爲了可以讓 Android 和 c/c++ 打通,所以就多出來了第三類開發者,熟悉 Android 開發和熟悉 c/c++ 開發,也就是咱們一般所說的 Ndk 開發,所以一個合格的 Android 開發者必需要熟悉 c/c++,咱們開發個三年五載想要提高,也須要嘗試着去熟悉 c/c++。linux

咱們可能又會想,精通一門開發語言至少須要個三年五載,Java 都夠咱們折騰的了,哪還有時間去學習 c/c++ ,但有些招聘需求上又明確要求開發者須要熟悉 c/c++,好比 Android 音視頻開發和 Android 智能識別開發等。那若是咱們想要從事 Ndk 開發,得怎麼去學又得學哪些東西?這裏我簡單的羅列一個小清單:c++

  • 熟悉 c/c++ 基礎語法
  • 熟悉 jni 基礎知識
  • 熟悉 c/c++ 進階知識
  • 熟悉 linux 內核
  • 熟悉 shell 腳本
  • 熟悉 cmake 語法

上面的內容看似有點多,但當咱們真正下定決心去學時,其實並不難也比較簡單。注意上面寫的是熟悉但並非精通,咱們得先熟悉而後再去精通,怎麼纔是算熟悉呢?首先是讀,咱們可以看懂 Android Native 層的源碼,讀 native 層源碼有助於咱們平常的開發和性能優化。其次是咱們還要可以寫,那怎麼寫如何寫?其實套路也就那麼多,這篇文章咱們主要來學習如何封裝 sdk 給 Java 調用者,這裏我以以前所學的 OpenCv 爲例來寫。git

1.封裝 Java 層 Mat

《圖形圖像處理 - 手寫 QQ 說說圖片處理效果》 一文中處理油畫效果是這麼寫的:github

/**
     * 實現圖像油畫效果
     *
     * @param bitmap 原圖片
     * @return 油畫效果圖像
     */
    public static final native Bitmap oilPainting(Bitmap bitmap);

    // Native 層代碼
    extern "C"
    JNIEXPORT jobject JNICALL
    Java_com_darren_ndk_day70_NDKBitmapUtils_oilPainting(JNIEnv *env, jclass type, jobject bitmap) {
    // 油畫基於直方統計
    // 1. 每一個點須要分紅 n*n 小塊
    // 2. 統計灰度等級
    // 3. 選擇灰度等級中最多的值
    // 4. 找到最大等級的像素取平均值
  
    // 省略代碼部分 ......
    return bitmap;
}
複製代碼

咱們不妨來思考一下,在真正開發的過程當中,咱們基本都是按需定製,簡單一點說就是你須要什麼功能,我就增長代碼封裝提供功能。這在 Java 層開發時卻是無所謂,改改代碼直接調一下就能夠了,但 Ndk 開發所涉及的就再也不只是 Java 了,改了代碼必須從新編譯 so 庫。假若需求稍微有變更,咱們須要改 Native 層代碼,而後從新編譯 so 庫,再聯調再測試,再改再聯調再測試。shell

相信你們都能聽明白我想表達的意思,所以咱們在提供 sdk 時必定要考慮周到,儘可能不要反覆的去改 c/c++ 代碼,儘可能不要反覆編譯聯調 so 庫。接下來咱們來思考一下,如何纔能有效的避免我以上所說的這些問題,假設剛開始須要提供一個作掩摸操做的功能,那代碼可能會是以下這樣:性能優化

/**
     * 掩模操做處理
     *
     * @param bitmap 原圖
     * @return 掩模效果圖
     */
    public native static Bitmap mask(Bitmap bitmap);

    // native 代碼
    extern "C"
    JNIEXPORT jobject JNICALL
    Java_com_darren_ndk_day72_MainActivity_mask(JNIEnv *env, jclass type, jobject bitmap) {
      // 1. bitmap -> mat
      Mat src;
      cv_helper::bitmap2mat(env, bitmap, src);
      // bgra -> bgr 不然 filter2D 會報錯
      cvtColor(src, src, COLOR_BGRA2BGR);

      // 2. 自定義卷積核
      Mat kernel(3, 3, CV_32FC1);
      kernel.at<float>(0, 0) = 0;
      kernel.at<float>(0, 1) = -1;
      kernel.at<float>(0, 2) = 0;
      kernel.at<float>(1, 0) = -1;
      kernel.at<float>(1, 1) = 5;
      kernel.at<float>(1, 2) = -1;
      kernel.at<float>(2, 0) = 0;
      kernel.at<float>(2, 1) = -1;
      kernel.at<float>(2, 2) = 0;

      // 3. 卷積運算
      Mat dst;
      filter2D(src, dst, src.depth(), kernel);

      // 4. mat -> bitmap
      cv_helper::mat2bitmap(env, dst, bitmap);
      return bitmap;
    }
複製代碼

假設如今又須要提供一個模糊操做,那麼咱們可能又得新提供 native 方法,得從新編譯調試 so ,代碼可能會以下:bash

/**
     * 模糊處理
     *
     * @param bitmap 原圖
     * @param size   模糊半徑,半徑越大越模糊
     * @return 模糊效果圖
     */
    public native static Bitmap blur(Bitmap bitmap, int size);

    // native 層代碼
    extern "C"
    JNIEXPORT jobject JNICALL
    Java_com_darren_ndk_day72_MainActivity_blur(JNIEnv *env, jclass type, jobject bitmap, jint size) {
        // 1. bitmap -> mat
        Mat src;
        cv_helper::bitmap2mat(env, bitmap, src);
        // bgra -> bgr 不然 filter2D 會報錯
        cvtColor(src, src, COLOR_BGRA2BGR);

        // 2. 模糊卷積核
        Mat kernel = Mat::ones(Size(size, size), CV_32FC1) / (size * size);

        // 3. 卷積運算
        Mat dst;
        filter2D(src, dst, src.depth(), kernel);

        // 4. mat -> bitmap
        cv_helper::mat2bitmap(env, dst, bitmap);
        return bitmap;
    }
複製代碼

假若後面又出現了一個其餘相似的功能,那麼我又得新提供 native 方法,從新編譯調試 so ,就出現了我上面所說的,改 Native 層代碼,從新編譯 so 庫,再聯調再測試,再改再聯調再測試。所以接下來咱們須要將這些代碼拆分出來封裝,咱們在 Java 層建立一個 Mat.java 對象用來對應 Native 層的 Mat.cpp 對象,這種思想有點相似於系統的 Bitmap 對象。關於這部分知識你們能夠參考這篇文章《JNI 基礎 - Android 共享內存的序列化過程》框架

public class Mat {
    /**
     * Native 建立 Mat 的首地址
     */
    public final long mNativePtr;

    private int rows;
    private int cols;
    private CVType type;

    public Mat(int rows, int cols, CVType type) {
        this.cols = cols;
        this.rows = rows;
        this.type = type;
        mNativePtr = nMatIII(rows, cols, type.value);
    }

    public Mat() {
        mNativePtr = nMat();
    }

    /**
     * 建立 Native Mat.cpp 對象
     *
     * @return Mat.cpp 對象頭指針
     */
    private native long nMat();

    /**
     * 建立 Native Mat.cpp 對象
     *
     * @param rows 高
     * @param cols 寬
     * @param type 類型
     * @return Mat.cpp 對象頭指針
     */
    private native long nMatIII(int rows, int cols, int type);

    /**
     * 這個方法提供給 Java 調用者
     *
     * @param row
     * @param col
     * @param value
     */
    public void put(int row, int col, int value) {
        if (type == CVType.CV_32FC1) {
            throw new UnsupportedOperationException("Provider value nonsupport and please check CVType.");
        }

        nPutI(mNativePtr, row, col, value);
    }

    /**
     * 這個方法提供給 Java 調用者
     *
     * @param row
     * @param col
     * @param value
     */
    public void put(int row, int col, float value) {
        if (type != CVType.CV_32FC1) {
            throw new UnsupportedOperationException("Provider value nonsupport and please check CVType.");
        }

        nPutF(mNativePtr, row, col, value);
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        // GC 回收該對象時 delete Mat.cpp 對象
        nDelete(mNativePtr);
    }

    public int getCols() {
        return cols;
    }

    public int getRows() {
        return rows;
    }

    public CVType getType() {
        return type;
    }

    public void release() {
        nRelease(mNativePtr);
    }

    private native void nDelete(long nativePtr);

    private native void nRelease(long nativePtr);

    private native void nPutI(long nativePtr, int row, int col, int value);

    private native void nPutF(long nativePtr, int row, int col, float value);
}
複製代碼
2. JNI 異常處理

關於 jni 的異常處理這是個技術活,以前的文章也有提到,這裏仍是要再作一些強調,咱們提供的 sdk 代碼儘可能不要平白無故的崩掉,適當的地方須要拋 Java 異常。由於 native 崩潰不像 Java 崩潰那樣會有 log 日誌打印,若是用戶只看到閃退卻看不到崩潰信息,用戶可能根本沒法進行調試修改。所以咱們要學會拋 java 異常。ide

void cv_helper::bitmap2mat(JNIEnv *env, jobject &bitmap, cv::Mat &dst) {
    try {
        AndroidBitmapInfo bitmapInfo;
        CV_Assert(AndroidBitmap_getInfo(env, bitmap, &bitmapInfo) >= 0);
        void *pixels;
        CV_Assert(AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0);
        CV_Assert(pixels);

        if (bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
            //  ANDROID_BITMAP_FORMAT_RGBA_8888 -> CV_8UC4
            dst.create(bitmapInfo.height, bitmapInfo.width, CV_8UC4);
            dst.data = reinterpret_cast<uchar *>(pixels);
        } else if (bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGB_565) {
            dst.create(bitmapInfo.height, bitmapInfo.width, CV_8UC2);
            dst.data = reinterpret_cast<uchar *>(pixels);
        } else {
            cv::Exception exception;
            exception.msg = "Bitmap only support RGBA_8888 and RGB_565";
            throw exception;
        }

        AndroidBitmap_unlockPixels(env, bitmap);
    } catch (const cv::Exception exception) {
        jclass ej = env->FindClass("java/lang/Exception");
        env->ThrowNew(ej, exception.what());
    } catch (...) {
        jclass ej = env->FindClass("java/lang/Exception");
        env->ThrowNew(ej, "Unknown exception in JNI code {mat2bitmap}");
    }
}
複製代碼

測試代碼

Bitmap srcBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.lbb);
Bitmap dstBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ALPHA_8);

// 模糊卷積核
int size = 9;
Mat kernel = new Mat(size, size, CVType.CV_32FC1);
float value = 1f / (size * size);
for (int rows = 0; rows < size; ++rows) {
  for (int cols = 0; cols < size; ++cols) {
    kernel.put(rows, cols, value);
  }
}

Mat srcMat = new Mat();
Utils.bitmap2mat(srcBitmap, srcMat);
Mat dstMat = new Mat();
// 卷積運算
Imgproc.filter2D(srcMat, dstMat, kernel);
Utils.mat2Bitmap(dstMat, dstBitmap);
複製代碼

最後你們能夠嘗試着去了解了解騰訊的開源框架 MMKV,能夠去學學其代碼的內部實現,既然咱們學了 NDK 確定須要時常拿出來溜溜。咱們也能夠對其作一些優化,好比支持寫入對象,寫入共享內存等等。

視頻地址:pan.baidu.com/s/17v_gCfNt…

視頻密碼:vj19

相關文章
相關標籤/搜索