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 肯定需要時常拿出來溜溜。我們也可以對其做一些優化,比如支援寫入物件,寫入共享記憶體等等。
視訊地址:週六晚八點