对于Android程序员来说把java代码封装成sdk比把c/c++库包封装成java层的sdk还是要容易,要将c/c++库二次封装成java层sdk需要掌握的知识还是比较多的,既要熟悉java,c/c++还要熟悉ndk的开发,而有这种需求的公司还是比较多的,笔者也曾面试过几家做智能硬件公司,他们有专门的c/c++底层驱动包括java开发的工程师,项目已经有完整的c/c++库,需要找一个能将已有的c/c++ 库封装成java层的sdk提供给合作商使用。
在开发中我们sdk和封装底层框架思想也是大致相同的,要考虑的东西还是很多的,要做到的是尽量的少修改ndk层的代码(每修改一次都要进行重新编译so库),我们要就要细分功能,做到有些共用可扩展,尽量的在java层去做扩展。比如我们有两个功能都对应了写了两个navtive方法去调用,而这两个功能中有调用了相同的c/c++方法。若是以后还要扩展一个功能 里面也会调用到这个共用的c/c++函数,难到我们又要继续写一个对应native方法去完成功能扩展吗?No,如果是这样的话又要从新编译c/c++代码生成新的so库。这里我们要尽量的做到功能的拆分,想办法把功能扩展放到java层。怎样才能做到这样呢,可以照我们Android系统的源码,做到java层对象映射到c/c++层的对象,比如系统Bitmap源码 Parcel(对象序列化)源码,下面我们看看他们是怎么套路的。
//Bitmap.java 部分代码
public final class Bitmap implements Parcelable {
// Convenience for JNI access
//方便JNI访问
private final long mNativePtr;
..........
/**
* Private constructor that must received an already allocated native bitmap
* int (pointer).
*/
// called from JNI
//jni 调用
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
if (nativeBitmap == 0) {
throw new RuntimeException("internal error: native bitmap is 0");
}
mWidth = width;
mHeight = height;
mIsMutable = isMutable;
mRequestPremultiplied = requestPremultiplied;
mNinePatchChunk = ninePatchChunk;
mNinePatchInsets = ninePatchInsets;
if (density >= 0) {
mDensity = density;
}
mNativePtr = nativeBitmap;
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) {
sPreloadTracingNumInstantiatedBitmaps++;
sPreloadTracingTotalBitmapsSize += nativeSize;
}
}
..........
private static native void nativeReconfigure(long nativeBitmap, int width, int height,
int config, boolean isPremultiplied);
private static native int nativeGetPixel(long nativeBitmap, int x, int y);
private static native void nativeGetPixels(long nativeBitmap, int[] pixels,
int offset, int stride, int x, int y,
int width, int height);
..........
}
这里就不带大家具体去看Bitmap的创建了,他的创建是在native层的,有兴趣的自己去看看具体的创建过程。可以看到Bitmap的构造方法注释 called from JNI 说明该方法是给jni层调用的,成员变量mNativePtr 注释 Convenience for JNI access(方便JNI访问)mNativePtr 是在构造方法中赋值的,可以看出这个long类型的值是从jni层传过来的,在仔细的看看Bitmap中的native方法几乎都要传mNativePtr 这个值过去,这是为什么呢?在Btmap Java对象的时候相应的也创建了native Bitmap 对象 然后把nativeBitmap 首地址传递给Java层Btmap ,这样我们要操作java Btmap时其实我们是通过它的native方法将首地址带入方便我们找到创建的nativeBitmap,这样我们实际操作的是nativeBitmap。
在前几篇文章我们提到了opencv图像处理库,之前一直使用到它的c/c++部分,并没有用到它的java层的sdk。现在我们自己简单的写个例子然后的整理梳理一下sdk
之前写的代码都是这样的,一个模糊图像功能,一个是给图像做掩膜,写了两个对应native的方法
extern "C"
JNIEXPORT jobject JNICALL
Java_com_youyangbo_sdk_BitmapUtils_blur(JNIEnv *env, jclass type, jobject bitmap) {
Mat src;
bitmap2Mat(env, src, bitmap);
// bgr
Mat bgr;
cvtColor(src, bgr, COLOR_BGRA2BGR);
Mat kernel = Mat::ones(Size(15, 15), CV_32FC1) / (15 * 15);
Mat dst;
filter2D(bgr, dst, src.depth(), kernel);
mat2Bitmap(env, dst, bitmap);
return bitmap;
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_youyangbo_sdk_BitmapUtils_mask(JNIEnv *env, jclass type, jobject bitmap) {
Mat src;
bitmap2Mat(env, src, bitmap);
Mat kernel = Mat::zeros(Size(3, 3), CV_32FC1);
kernel.at<float>(0, 1) = -1;
kernel.at<float>(1, 0) = -1;
kernel.at<float>(1, 1) = 5;
kernel.at<float>(1, 2) = -1;
kernel.at<float>(2, 1) = -1;;
Mat res;
filter2D(src, res, src.depth(), kernel);
jobject new_bitmap = BitmapUtils::createBitmap(env, res.cols, res.rows, res.type());
mat2Bitmap(env, res, new_bitmap);
return new_bitmap;
}
在上面的两个功能中我们都调用到了bitmap2Mat() filter2D() mat2Bitmap()这个函数这里我们就可以做到功能的拆分,提供一个java层的bitmap与Mat相互转化的工具类,再提供一个java层filter2D()的工具类,那么我们就差了nativeMat转化成java层的Mat对象。 我们做到nativeMat转化成java层的Mat 那么功能代码就会转移到java层去做了,之后又类似功能扩展不需要动c/c++层在java就可以完成。
接下里看看nativeMat-->javaMat
/**
* 对应opencv Mat.cpp 对象
*/
public class Mat {
public final long mNativePtr; //保存native 对象地址
int rows;
int cols;
Type type;
public Mat() {
mNativePtr = nMat();
}
public Mat(int rows, int cols, Type type) {
this.rows = rows;
this.cols = cols;
this.type = type;
mNativePtr = nMatIII(rows, cols, type.value);
}
private native long nMatIII(int rows, int cols, int value);
private native long nMat();
public void put(int row, int col, int value) {
nput(mNativePtr,row,col,value);
}
private native void nput(long nativePtr, int row, int col, int value);
@Override
protected void finalize() throws Throwable {
super.finalize();
nDelte(mNativePtr);
}
private native void nDelte(long mNativePtr);
}
jni层构建c层Mat对象 并操作Mat
#include "bitmap/native_map.h"
#include <opencv2/opencv.hpp>
using namespace cv;
/**
* 创建Mat
*/
extern "C"
JNIEXPORT jlong JNICALL
Java_com_youyangbo_sdk_opencv_Mat_nMat(JNIEnv *env, jobject instance) {
Mat *mat = new Mat();
return reinterpret_cast<jlong> (mat);
}
extern "C"
JNIEXPORT jlong JNICALL
Java_com_youyangbo_sdk_opencv_Mat_nMatIII(JNIEnv *env, jobject instance, jint rows, jint cols,
jint value) {
Mat *mat = new Mat(rows, cols, value);
return reinterpret_cast<jlong> (mat);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_youyangbo_sdk_opencv_Mat_nput(JNIEnv *env, jobject instance, jlong nativePtr, jint row,
jint col, jint value) {
Mat *mat_ptr = reinterpret_cast<Mat *>(nativePtr);
mat_ptr->at<float>(row, col) = value;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_youyangbo_sdk_opencv_Mat_nDelte(JNIEnv *env, jobject instance, jlong nativePtr) {
Mat *mat_ptr = reinterpret_cast<Mat *>(nativePtr);
if (mat_ptr != NULL) {
delete mat_ptr;
mat_ptr = NULL;
}
}
Mat与Bitmap转化java层工具类的
public class OpencvUtils {
public static void mat2Bitmap(Mat mat, Bitmap bitmap) {
nmat2Bitmap(mat.mNativePtr, bitmap);
}
private static native void nmat2Bitmap(long mnativePtr, Bitmap bitmap);
public static void bitmap2Mat(Bitmap bitmap, Mat mat) {
nbitmap2Mat(bitmap, mat.mNativePtr);
}
private static native void nbitmap2Mat(Bitmap bitmap, long nativePtr);
}
#include "bitmap/NatvieUtils.h"
#include "bitmap/opencvHelp.h"
#include <android/log.h>
using namespace cv;
extern "C"
JNIEXPORT void JNICALL
Java_com_youyangbo_sdk_opencv_OpencvUtils_nmat2Bitmap(JNIEnv *env, jclass type, jlong mnativePtr,
jobject bitmap) {
Mat *mat = reinterpret_cast<Mat *>(mnativePtr);
mat2Bitmap(env, *mat, bitmap);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_youyangbo_sdk_opencv_OpencvUtils_nbitmap2Mat(JNIEnv *env, jclass type, jobject bitmap,
jlong nativePtr) {
Mat *mat = reinterpret_cast<Mat *>(nativePtr);
bitmap2Mat(env, *mat, bitmap);
}
java层filter2D工具类
public class Imgproc {
public static void filter2D(Mat src, Mat dst, Mat kernel) {
nfilter2D(src.mNativePtr, dst.mNativePtr, kernel.mNativePtr);
}
private static native void nfilter2D(long mNativePtr, long mNativePtr1, long mNativePtr2);
}
jni层filter2D工具类
#include "bitmap/NImgproc.h"
#include <opencv2/opencv.hpp>
using namespace cv;
extern "C"
JNIEXPORT void JNICALL
Java_com_youyangbo_sdk_opencv_Imgproc_nfilter2D(JNIEnv *env, jclass type, jlong srcPtr,
jlong resPtr1, jlong kenrlPtr2) {
Mat *src_ptr = reinterpret_cast<Mat *>(srcPtr);
Mat *res_ptr = reinterpret_cast<Mat *>(resPtr1);
Mat *knerl_ptr = reinterpret_cast<Mat *>(kenrlPtr2);
filter2D(*src_ptr, *res_ptr, src_ptr->depth(), *knerl_ptr);
}
基本的封装架子已经有了,但是还存在细节的处理,比如异常处理,内存的释放,答题思路就是,大体思路就是将异常尽量在java层抛出,jni层的异常要抛给java层。
最后我们继续用一个门面模式将调用代码封装起来,这样调用者就会简单
public class Utils {
public static Bitmap mask(Bitmap bitmap) {
if (bitmap == null) {
throw new NullPointerException("bitmap is null");
}
Mat mat = new Mat();
OpencvUtils.bitmap2Mat(bitmap, mat);
Mat resMat = new Mat();
Mat knerl = new Mat(3, 3, Type.CV_32FC1);
knerl.put(0, 0, 0);
knerl.put(0, 1, -1);
knerl.put(0, 2, 0);
knerl.put(1, 0, -1);
knerl.put(1, 1, 5);
knerl.put(1, 2, -1);
knerl.put(2, 0, 0);
knerl.put(2, 1, -1);
knerl.put(2, 2, 0);
Imgproc.filter2D(mat, resMat, knerl);
Bitmap newBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
OpencvUtils.mat2Bitmap(resMat, newBitmap);
return newBitmap;
}
}
最后给张Demo的效果图:
总体来说要做到C/C++库封装成Java层sdk,除了封装的思想之外还是要熟悉c/c++,java,ndk知识,