關(guān)于 Ndk 開(kāi)發(fā),網(wǎng)上的資料比較少,這方面的書(shū)籍也不多。因?yàn)槠渖婕暗闹R(shí)非常廣,時(shí)常有哥們問(wèn)我,東西那么多到底要學(xué)到什么程度呢?到底應(yīng)該怎么學(xué)?這期我給大家來(lái)做一個(gè)簡(jiǎn)單回答,首先單純站在 Android 系統(tǒng)的角度來(lái)說(shuō),我們可以細(xì)分為 Java 層和 Native(c/c++) 層。站在 Android 開(kāi)發(fā)的角度來(lái)說(shuō),我們又可以細(xì)分為精通 Android 開(kāi)發(fā)和精通 c/c++ 開(kāi)發(fā)。當(dāng)然筆者之前在長(zhǎng)沙從事 Android 開(kāi)發(fā),公司是不存在 c/c++ 工程師的,也就是說(shuō)所有的開(kāi)發(fā)工作調(diào)用 Android Framework 層的 Api 就都能實(shí)現(xiàn)。
來(lái)到深圳做音視頻項(xiàng)目,公司有專(zhuān)門(mén)的引擎部門(mén),也就是說(shuō)有專(zhuān)門(mén)的 c/c++ 音視頻工程師。為了能夠讓 Android 和 c/c++ 打通,因此就多出來(lái)了第三類(lèi)開(kāi)發(fā)者,熟悉 Android 開(kāi)發(fā)和熟悉 c/c++ 開(kāi)發(fā),也就是我們通常所說(shuō)的 Ndk 開(kāi)發(fā),因此一個(gè)合格的 Android 開(kāi)發(fā)者必須要熟悉 c/c++,我們開(kāi)發(fā)個(gè)三年五載想要提升,也需要嘗試著去熟悉 c/c++。
我們可能又會(huì)想,精通一門(mén)開(kāi)發(fā)語(yǔ)言至少需要個(gè)三年五載,Java 都?jí)蛭覀冋垓v的了,哪還有時(shí)間去學(xué)習(xí) c/c++ ,但有些招聘需求上又明確要求開(kāi)發(fā)者需要熟悉 c/c++,比如 Android 音視頻開(kāi)發(fā)和 Android 智能識(shí)別開(kāi)發(fā)等。那如果我們想要從事 Ndk 開(kāi)發(fā),得怎么去學(xué)又得學(xué)哪些東西?這里我簡(jiǎn)單的羅列一個(gè)小清單:
- 熟悉 c/c++ 基礎(chǔ)語(yǔ)法
- 熟悉 jni 基礎(chǔ)知識(shí)
- 熟悉 c/c++ 進(jìn)階知識(shí)
- 熟悉 linux 內(nèi)核
- 熟悉 shell 腳本
- 熟悉 cmake 語(yǔ)法
上面的內(nèi)容看似有點(diǎn)多,但當(dāng)我們真正下定決心去學(xué)時(shí),其實(shí)并不難也比較簡(jiǎn)單。注意上面寫(xiě)的是熟悉但并不是精通,我們得先熟悉然后再去精通,怎么才是算熟悉呢?首先是讀,我們能夠看懂 Android Native 層的源碼,讀 native 層源碼有助于我們?nèi)粘5拈_(kāi)發(fā)和性能優(yōu)化。其次是我們還要能夠?qū)懀窃趺磳?xiě)如何寫(xiě)?其實(shí)套路也就那么多,這篇文章我們主要來(lái)學(xué)習(xí)如何封裝 sdk 給 Java 調(diào)用者,這里我以之前所學(xué)的 OpenCv 為例來(lái)寫(xiě)。
1.封裝 Java 層 Mat
在《圖形圖像處理 - 手寫(xiě) QQ 說(shuō)說(shuō)圖片處理效果》 一文中處理油畫(huà)效果是這么寫(xiě)的:
/**
* 實(shí)現(xiàn)圖像油畫(huà)效果
*
* @param bitmap 原圖片
* @return 油畫(huà)效果圖像
*/
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) {
// 油畫(huà)基于直方統(tǒng)計(jì)
// 1. 每個(gè)點(diǎn)需要分成 n*n 小塊
// 2. 統(tǒng)計(jì)灰度等級(jí)
// 3. 選擇灰度等級(jí)中最多的值
// 4. 找到最大等級(jí)的像素取平均值
// 省略代碼部分 ......
return bitmap;
}
我們不妨來(lái)思考一下,在真正開(kāi)發(fā)的過(guò)程中,我們基本都是按需定制,簡(jiǎn)單一點(diǎn)說(shuō)就是你需要什么功能,我就增加代碼封裝提供功能。這在 Java 層開(kāi)發(fā)時(shí)倒是無(wú)所謂,改改代碼直接調(diào)一下就可以了,但 Ndk 開(kāi)發(fā)所涉及的就不再只是 Java 了,改了代碼必須重新編譯 so 庫(kù)。倘若需求稍微有變動(dòng),我們需要改 Native 層代碼,然后重新編譯 so 庫(kù),再聯(lián)調(diào)再測(cè)試,再改再聯(lián)調(diào)再測(cè)試。
相信大家都能聽(tīng)明白我想表達(dá)的意思,因此我們?cè)谔峁?sdk 時(shí)一定要考慮周到,盡量不要反復(fù)的去改 c/c++ 代碼,盡量不要反復(fù)編譯聯(lián)調(diào) so 庫(kù)。接下來(lái)我們來(lái)思考一下,如何才能有效的避免我以上所說(shuō)的這些問(wèn)題,假設(shè)剛開(kāi)始需要提供一個(gè)做掩摸操作的功能,那代碼可能會(huì)是如下這樣:
/**
* 掩模操作處理
*
* @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 會(huì)報(bào)錯(cuò)
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. 卷積運(yùn)算
Mat dst;
filter2D(src, dst, src.depth(), kernel);
// 4. mat -> bitmap
cv_helper::mat2bitmap(env, dst, bitmap);
return bitmap;
}
假設(shè)現(xiàn)在又需要提供一個(gè)模糊操作,那么我們可能又得新提供 native 方法,得重新編譯調(diào)試 so ,代碼可能會(huì)如下:
/**
* 模糊處理
*
* @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 會(huì)報(bào)錯(cuò)
cvtColor(src, src, COLOR_BGRA2BGR);
// 2. 模糊卷積核
Mat kernel = Mat::ones(Size(size, size), CV_32FC1) / (size * size);
// 3. 卷積運(yùn)算
Mat dst;
filter2D(src, dst, src.depth(), kernel);
// 4. mat -> bitmap
cv_helper::mat2bitmap(env, dst, bitmap);
return bitmap;
}
倘若后面又出現(xiàn)了一個(gè)其他類(lèi)似的功能,那么我又得新提供 native 方法,重新編譯調(diào)試 so ,就出現(xiàn)了我上面所說(shuō)的,改 Native 層代碼,重新編譯 so 庫(kù),再聯(lián)調(diào)再測(cè)試,再改再聯(lián)調(diào)再測(cè)試。因此接下來(lái)我們需要將這些代碼拆分出來(lái)封裝,我們?cè)?Java 層創(chuàng)建一個(gè) Mat.java 對(duì)象用來(lái)對(duì)應(yīng) Native 層的 Mat.cpp 對(duì)象,這種思想有點(diǎn)類(lèi)似于系統(tǒng)的 Bitmap 對(duì)象。關(guān)于這部分知識(shí)大家可以參考這篇文章《JNI 基礎(chǔ) - Android 共享內(nèi)存的序列化過(guò)程》
public class Mat {
/**
* Native 創(chuàng)建 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();
}
/**
* 創(chuàng)建 Native Mat.cpp 對(duì)象
*
* @return Mat.cpp 對(duì)象頭指針
*/
private native long nMat();
/**
* 創(chuàng)建 Native Mat.cpp 對(duì)象
*
* @param rows 高
* @param cols 寬
* @param type 類(lèi)型
* @return Mat.cpp 對(duì)象頭指針
*/
private native long nMatIII(int rows, int cols, int type);
/**
* 這個(gè)方法提供給 Java 調(diào)用者
*
* @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);
}
/**
* 這個(gè)方法提供給 Java 調(diào)用者
*
* @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 回收該對(duì)象時(shí) delete Mat.cpp 對(duì)象
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 異常處理
關(guān)于 jni 的異常處理這是個(gè)技術(shù)活,之前的文章也有提到,這里還是要再做一些強(qiáng)調(diào),我們提供的 sdk 代碼盡量不要無(wú)緣無(wú)故的崩掉,適當(dāng)?shù)牡胤叫枰獟?Java 異常。因?yàn)?native 崩潰不像 Java 崩潰那樣會(huì)有 log 日志打印,如果用戶(hù)只看到閃退卻看不到崩潰信息,用戶(hù)可能根本無(wú)法進(jìn)行調(diào)試修改。因此我們要學(xué)會(huì)拋 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}");
}
}
測(cè)試代碼
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();
// 卷積運(yùn)算
Imgproc.filter2D(srcMat, dstMat, kernel);
Utils.mat2Bitmap(dstMat, dstBitmap);
最后大家可以嘗試著去了解了解騰訊的開(kāi)源框架 MMKV,可以去學(xué)學(xué)其代碼的內(nèi)部實(shí)現(xiàn),既然我們學(xué)了 NDK 肯定需要時(shí)常拿出來(lái)溜溜。我們也可以對(duì)其做一些優(yōu)化,比如支持寫(xiě)入對(duì)象,寫(xiě)入共享內(nèi)存等等。
視頻地址:https://pan.baidu.com/s/17v_gCfNtuhjd6LzXkhvHnw
視頻密碼:vj19