在 Android 中通過 JNI 去操作 Bitmap。
在 Android 通過 JNI 去調用 Bitmap,通過 CMake 去編 so 動態鏈接庫的話,需要添加 jnigraphics 圖像庫。
target_link_libraries( # Specifies the target library.
native-operation
jnigraphics
${log-lib} )
在 Android 中關於 JNI Bitmap 的操作,都定義在 bitmap.h
的頭文件里面了,主要就三個函數,明白它們的含義之后就可以去實踐體會了。
檢索 Bitmap 對象信息
AndroidBitmap_getInfo 函數允許原生代碼檢索 Bitmap 對象信息,如它的大小、像素格式等,函數簽名如下:
/**
* Given a java bitmap object, fill out the AndroidBitmapInfo struct for it.
* If the call fails, the info parameter will be ignored.
*/
int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,
AndroidBitmapInfo* info);
其中,第一個參數就是 JNI 接口指針,第二個參數就是 Bitmap 對象的引用,第三個參數是指向 AndroidBitmapInfo 結構體的指針。
AndroidBitmapInfo 結構體如下:
/** Bitmap info, see AndroidBitmap_getInfo(). */
typedef struct {
/** The bitmap width in pixels. */
uint32_t width;
/** The bitmap height in pixels. */
uint32_t height;
/** The number of byte per row. */
uint32_t stride;
/** The bitmap pixel format. See {@link AndroidBitmapFormat} */
int32_t format;
/** Unused. */
uint32_t flags; // 0 for now
} AndroidBitmapInfo;
其中,width 就是 Bitmap 的寬,height 就是高,format 就是圖像的格式,而 stride 就是每一行的字節數。
圖像的格式有如下支持:
/** Bitmap pixel format. */
enum AndroidBitmapFormat {
/** No format. */
ANDROID_BITMAP_FORMAT_NONE = 0,
/** Red: 8 bits, Green: 8 bits, Blue: 8 bits, Alpha: 8 bits. **/
ANDROID_BITMAP_FORMAT_RGBA_8888 = 1,
/** Red: 5 bits, Green: 6 bits, Blue: 5 bits. **/
ANDROID_BITMAP_FORMAT_RGB_565 = 4,
/** Deprecated in API level 13. Because of the poor quality of this configuration, it is advised to use ARGB_8888 instead. **/
ANDROID_BITMAP_FORMAT_RGBA_4444 = 7,
/** Alpha: 8 bits. */
ANDROID_BITMAP_FORMAT_A_8 = 8,
};
如果 AndroidBitmap_getInfo 執行成功的話,會返回 0 ,否則返回一個負數,代表執行的錯誤碼列表如下:
/** AndroidBitmap functions result code. */
enum {
/** Operation was successful. */
ANDROID_BITMAP_RESULT_SUCCESS = 0,
/** Bad parameter. */
ANDROID_BITMAP_RESULT_BAD_PARAMETER = -1,
/** JNI exception occured. */
ANDROID_BITMAP_RESULT_JNI_EXCEPTION = -2,
/** Allocation failed. */
ANDROID_BITMAP_RESULT_ALLOCATION_FAILED = -3,
};
訪問原生像素緩存
AndroidBitmap_lockPixels 函數鎖定了像素緩存以確保像素的內存不會被移動。
如果 Native 層想要訪問像素數據並操作它,該方法返回了像素緩存的一個原生指針,
/**
* Given a java bitmap object, attempt to lock the pixel address.
* Locking will ensure that the memory for the pixels will not move
* until the unlockPixels call, and ensure that, if the pixels had been
* previously purged, they will have been restored.
*
* If this call succeeds, it must be balanced by a call to
* AndroidBitmap_unlockPixels, after which time the address of the pixels should
* no longer be used.
*
* If this succeeds, *addrPtr will be set to the pixel address. If the call
* fails, addrPtr will be ignored.
*/
int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
其中,第一個參數就是 JNI 接口指針,第二個參數就是 Bitmap 對象的引用,第三個參數是指向像素緩存地址的指針。
AndroidBitmap_lockPixels 執行成功的話返回 0 ,否則返回一個負數,錯誤碼列表就是上面提到的。
釋放原生像素緩存
對 Bitmap 調用完 AndroidBitmap_lockPixels 之后都應該對應調用一次 AndroidBitmap_unlockPixels 用來釋放原生像素緩存。
當完成對原生像素緩存的讀寫之后,就應該釋放它,一旦釋放后,Bitmap Java 對象又可以在 Java 層使用了,函數簽名如下:
/**
* Call this to balance a successful call to AndroidBitmap_lockPixels.
*/
int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);
其中,第一個參數就是 JNI 接口指針,第二個參數就是 Bitmap 對象的引用,如果執行成功返回 0,否則返回 1。
對 Bitmap 的操作,最重要的就是 AndroidBitmap_lockPixels 函數拿到所有像素的緩存地址,然后對每個像素值進行操作,從而更改 Bitmap 。
實踐
通過對 Bitmap 進行旋轉,上下翻轉,左右鏡像來體驗 JNI 的開發。
效果如下:
具體代碼可以參考我的 Github 項目,歡迎 Star。
通過 JNI 將 Bitmap 旋轉
首先定義一個這樣的 native 函數:
// 順時針旋轉 90° 的操作
public native Bitmap rotateBitmap(Bitmap bitmap);
傳入一個 Bitmap 對象,然后返回一個 Bitmap 對象。
然后在 C++ 代碼中,首先檢索 Bitmap 的信息,看看是否成功。
AndroidBitmapInfo bitmapInfo;
int ret;
if ((ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return NULL;
}
接下來就是獲得 Bitmap 的像素緩存指針:
// 讀取 bitmap 的像素內容到 native 內存
void *bitmapPixels;
if ((ret = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
return NULL;
}
這個指針指向的就是 Bitmap 像素內容,它是一個以一維數組的形式保存所有的像素點的值,但是我們在定義 Bitmap 圖像時,都會定義寬和高,這就相對於是一個二維的了,那么就存在 Bitmap 的像素內容如何轉成指針指向的一維內容,是按照行排列還是按照列排列呢?
在這里是按照行進行排列的,而且行的排列是從左往右,列的排列是從上往下,起始點就和屏幕坐標原點一樣,位於左上角。
通過 AndroidBitmap_lockPixels 方法,bitmapPixels 指針就指向了 Bitmap 的像素內容,它的長度就是 Bitmap 的寬和高的乘積。
要將 Bitmap 進行旋轉,可以通過直接更改 bitmapPixels 指針指向的像素點的值,也可以通過創建一個新的 Bitmap 對象,然后將像素值填充到 Bitmap 對象中,這里選擇后者的實現方式。
首先創建一個新的 Bitmap 對象,參考之前文章中提到的方式:Android 通過 JNI 訪問 Java 字段和方法調用。
在 Java 代碼中,通過 createBitmap 方法可以創建一個 Bitmap,如下所示:
Bitmap.createBitmap(int width, int height, @NonNull Config config)`
所以在 JNI 中就需要調用 Bitmap 的靜態方法來創建一個 Bitmap 對象。
jobject generateBitmap(JNIEnv *env, uint32_t width, uint32_t height) {
jclass bitmapCls = env->FindClass("android/graphics/Bitmap");
jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls,
"createBitmap",
"(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jstring configName = env->NewStringUTF("ARGB_8888");
jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(
bitmapConfigClass, "valueOf",
"(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass,
valueOfBitmapConfigFunction, configName);
jobject newBitmap = env->CallStaticObjectMethod(bitmapCls,
createBitmapFunction,
width,
height, bitmapConfig);
return newBitmap;
}
首先通過 FindClass
方法找到 Config 類,得到一個 ARGB_8888 的配置,然后得到 Bitmap 類,調用它的靜態方法 createBitmap
創建一個新的 Bitmap 對象,具體可以參考之前的文章。
在這里要傳入新 Bitmap 的寬高,這個寬高也是通過 AndroidBitmap_getInfo
方法得到原來的寬高之后,根據不同的操作計算后得到的。
// 旋轉操作,新 Bitmap 的寬等於原來的高,新 Bitmap 的高等於原來的寬
uint32_t newWidth = bitmapInfo.height;
uint32_t newHeight = bitmapInfo.width;
有了新的 Bitmap 對象,又有了原有的 Bitmap 像素指針,接下來就是創建新的像素指針,並填充像素內容,然后把這個像素內容再填充到 Bitmap 上。
// 創建一個新的數組指針,把這個新的數組指針填充像素值
uint32_t *newBitmapPixels = new uint32_t[newWidth * newHeight];
int whereToGet = 0;
for (int y = 0; y < newHeight; ++y) {
for (int x = newWidth - 1; x >= 0; x--) {
uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
newBitmapPixels[newWidth * y + x] = pixel;
}
}
在這兩個 for
循環里面就是從原來的像素指針中取出像素值,然后把它按照特定的排列順序填充到新的像素指針中對應位置的值,這里也就是前面強調的像素指針是按照行進行排列的,起點是 Bitmap 的左上角。
void *resultBitmapPixels;
if ((ret = AndroidBitmap_lockPixels(env, newBitmap, &resultBitmapPixels)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
return NULL;
}
int pixelsCount = newWidth * newHeight;
memcpy((uint32_t *) resultBitmapPixels, newBitmapPixels, sizeof(uint32_t) * pixelsCount);
AndroidBitmap_unlockPixels(env, newBitmap);
再次創建一個 resultBitmapPixels 指針,並調用 AndroidBitmap_lockPixels 方法獲取新的 Bitmap 的像素指針緩存,然后調用 memcpy
方法,將待填充的像素指針填充到 resultBitmapPixels 上,這樣就完成了像素的賦值,最后調用 AndroidBitmap_unlockPixels
方法釋放像素指針緩存,完成整個賦值過程。
就這樣通過讀取原有 Bitmap 的像素內容然后進行操作后再賦值給新的 Bitmap 對象就完成了 JNI 操作 Bitmap 。
通過 JNI 將 Bitmap 上下翻轉和左右鏡像
將 Bitmap 進行上下翻轉以及左右鏡像和旋轉操作類似了,只是針對像素指針的操作方式不同。
上下翻轉的操作:
int whereToGet = 0;
for (int y = 0; y < newHeight; ++y) {
for (int x = 0; x < newWidth; x++) {
uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
newBitmapPixels[newWidth * (newHeight - 1 - y) + x] = pixel;
}
}
左右鏡像的操作:
int whereToGet = 0;
for (int y = 0; y < newHeight; ++y) {
for (int x = newWidth - 1; x >= 0; x--) {
uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
newBitmapPixels[newWidth * y + x] = pixel;
}
}
其他的操作都相同了,具體還是看項目代碼吧。
參考
- 《Android C++ 高級編程--使用 NDK》