NDK 开发实战 - 封装 java 层 sdk 模型

关于 Ndk 开发,网上的资料比较少,这方面的书籍也不多。因为其涉及的知识非常广,时常有哥们问我,东西那么多到底要学到什么程度呢?到底应该怎么学?这期我给大家来做一个简单回答,首先单纯站在 Android 系统的角度来说,我们可以细分为 Java 层和 Native(c/c++) 层。站在 Android 开发的角度来说,我们又可以细分为精通 Android 开发和精通 c/c++ 开发。当然笔者之前在长沙从事 Android 开发,公司是不存在 c/c++ 工程师的,也就是说所有的开发工作调用 Android Framework 层的 Api 就都能实现。

来到深圳做音视频项目,公司有专门的引擎部门,也就是说有专门的 c/c++ 音视频工程师。为了能够让 Android 和 c/c++ 打通,因此就多出来了第三类开发者,熟悉 Android 开发和熟悉 c/c++ 开发,也就是我们通常所说的 Ndk 开发,因此一个合格的 Android 开发者必须要熟悉 c/c++,我们开发个三年五载想要提升,也需要尝试着去熟悉 c/c++。

我们可能又会想,精通一门开发语言至少需要个三年五载,Java 都够我们折腾的了,哪还有时间去学习 c/c++ ,但有些招聘需求上又明确要求开发者需要熟悉 c/c++,比如 Android 音视频开发和 Android 智能识别开发等。那如果我们想要从事 Ndk 开发,得怎么去学又得学哪些东西?这里我简单的罗列一个小清单:

  • 熟悉 c/c++ 基础语法
  • 熟悉 jni 基础知识
  • 熟悉 c/c++ 进阶知识
  • 熟悉 linux 内核
  • 熟悉 shell 脚本
  • 熟悉 cmake 语法

上面的内容看似有点多,但当我们真正下定决心去学时,其实并不难也比较简单。注意上面写的是熟悉但并不是精通,我们得先熟悉然后再去精通,怎么才是算熟悉呢?首先是读,我们能够看懂 Android Native 层的源码,读 native 层源码有助于我们日常的开发和性能优化。其次是我们还要能够写,那怎么写如何写?其实套路也就那么多,这篇文章我们主要来学习如何封装 sdk 给 Java 调用者,这里我以之前所学的 OpenCv 为例来写。

1.封装 Java 层 Mat

《图形图像处理 - 手写 QQ 说说图片处理效果》 一文中处理油画效果是这么写的:

    /**
     * 实现图像油画效果
     *
     * @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 库,再联调再测试,再改再联调再测试。

相信大家都能听明白我想表达的意思,因此我们在提供 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 ,代码可能会如下:

    /**
     * 模糊处理
     *
     * @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 异常。

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 肯定需要时常拿出来溜溜。我们也可以对其做一些优化,比如支持写入对象,写入共享内存等等。

视频地址:https://pan.baidu.com/s/17v_gCfNtuhjd6LzXkhvHnw
视频密码:vj19

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342