人间观察
当你喜欢一个人的时候,总是小心翼翼的,笨笨的,傻傻的,生怕做错了什么,又怕不做什么~
到此,Android中基本的JNI基础知识以及常见的基本操作差不多就基本讲完了。我们来实践一下,本文实现的是对Android Bitmap的处理: 对一张图片进行处理,照片底片效果,黑白化,灰度化,左右翻转,暖色,冷色,高斯模糊等等,市场上有很多这种处理图片的app,就看谁的算法足够厉害强大。效果图如下
效果图
我们先贴一下我们对一张图片处理后的各种效果图。
在Android中JNI层操作bitmap的需要链接系统的动态库nigraphics
图像库,怎么动态链接呢? 就是通过上一篇文章的CMake
中的方法target_link_libraries
来链接,即:
target_link_libraries( # Specifies the target library.
native-lib
jnigraphics #JNI层,添加bitmap支持
# Links the target library to the log library
# included in the NDK.
${log-lib})
在JNI层操作bitmap的函数都定义在bitmap.h
的头文件里,主要就三个函数。AndroidBitmap_getInfo
,AndroidBitmap_lockPixels
,AndroidBitmap_unlockPixels
返回值
3个方法的返回值都是如下情况。成功是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,
};
获取bitmap的信息
通过AndroidBitmap_getInfo
可以获取图片的基本信息,比如宽高,图像的格式
/**
* 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);
参数env JNI 接口指针
参数jbitmap Bitmap 对象的引用
参数info AndroidBitmapInfo 结构体的指针
返回值 0 成功
传入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 就是图片的宽,height就是图片的高,stride 就是每一行的字节数,format是图像的格式。格式有如下:
/** 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,
};
这个格式熟悉吧和Android中bitmap一样。
获取bitmap的每个像素信息
/**
* 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);
这个方法是我们最重要的一个方法,拿到图片的每个像素之后就可以对每个像素值进行操作,从而更改 Bitmap。
调用该方法后,会锁定像素确保像素的内存不会被移动,只有再次调用unlockPixels
会再次释放。 传入addrPtr
,它会指向的图片的那块内存。addrPtr的类型是void**
,给了我们足够的操作像素方式,你可以随意操作这块内存。AndroidBitmap_lockPixels
同样 执行成功的话返回 0 ,否则返回一个负数,错误码列表就是上面提到的。
特别注意
如果直接操作addrPtr
指针所指向的内容,相当于它会直接更改对应java
层的bitmap
对象。你如果不想这样,可以直接在jni
层中构造一个新的java
层的bitmap
对象然后返回,不影响原来的。
解锁像素缓存
对 Bitmap
调用完 AndroidBitmap_lockPixels
之后都应该对应调用一次 AndroidBitmap_unlockPixels
用来解锁/释放原生像素缓存。
/**
* Call this to balance a successful call to AndroidBitmap_lockPixels.
*/
int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);
每个像素的ARGB的获取,JAVA&JNI的转换注意点
讲这个前我们顺带提一下,Android 中 Bitmap 的占用内存大小,跟设备dpi和该图片所放的资源目录有关,与ImageView无关。比如一张像素为 300 * 300 的图片放在xxdpi(480dpi)目录中,设备屏幕密度为 440 dpi,每个读取参数为ARGB_8888(4个字节)。则内存占用为:
(440 / 480 * 300 ) * (440 / 480 * 300 )*4=302500(byte)
如果是放在assets 目录下的图片则不压缩计算。
在Android 中以 ARGB_8888 为例,A/R/G/B 各占 8 位,各由两个十六进制数表示,依次排列,比如常见的色值 #FF534F33,即各通道值为:透明度 alpha 0xFF,红色 red 0x53,绿色 green 0x4F,蓝色 blue 0x33。
如何才能从一个 int 值中获取各个通道(RGB)的颜色呢?只有获取了才能对RGB进行算法处理。
还以 #FF234567 为例,转换为二进制为
1111 1111 | 0101 0011 | 0100 1111 | 0011 0011 (| 符号是方便划分)
通过位运算,舍弃位数,只有自己关心的即可。比如将二进制右移 24位得到1111 1111 ,然后 & 0xFF得到alpha。即int alpha = (color >> 24) & 0xFF。
再比如得到红色二进制右移 16位得0101 0011 然后 & 0xFF得到red,即 int red=(color >> 16) & 0xFF。
0xFF的二进制的低8位是1111 1111前面24为都是0.
但是在jni的C层中不是的,在C层中,Bitmap像素点的值是ABGR,而不是ARGB,也就是说,B和R交换了,高端到低端:A,B,G,R。这个很重要,网上的文章大部分都是错误的,在下面的代码中我们也会验证一下这个结论。
图片底片效果
我们以实现图片的底片效果为例,其它效果都一样,都是AndroidBitmap_lockPixels
后操作每个像素,只是对像素操作的算法不一样。
底片的算法原理:将当前像素点的RGB值分别与255之差后的值作为当前点的RGB值,即 R = 255 – R;G = 255 – G;B = 255 – B;
int BitmapUtil::negative(JNIEnv *env, jobject bitmap) {
AndroidBitmapInfo bitmapInfo;
// 获取bitmap的属性信息
int ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
if (ret != ANDROID_BITMAP_RESULT_SUCCESS) {
LOG_D("AndroidBitmap_getInfo %d", ret);
return JNI_FALSE;
}
void *bitmapPixels;
int pixRet = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels);
if (pixRet != ANDROID_BITMAP_RESULT_SUCCESS) {
LOG_D("AndroidBitmap_lockPixels %d", pixRet);
return JNI_FALSE;
}
int w = bitmapInfo.width;
int h = bitmapInfo.height;
uint32_t *srcPix = (uint32_t *) bitmapPixels;
// 在C层中,Bitmap像素点的值是ABGR,而不是ARGB,也就是说,高端到低端:A,B,G,R
// 底片效果算法原理:将当前像素点的RGB值分别与255之差后的值作为当前点的RGB值,即
// R = 255 – R;G = 255 – G;B = 255 – B;
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
uint32_t color = srcPix[w * i + j];
uint32_t blue = (color >> 16) & 0xFF;
uint32_t green = (color >> 8) & 0xFF;
uint32_t red = color & 0xFF;
uint32_t alpha = (color >> 24) & 0xFF;
if (i == 0 && j == 0) {
LOG_D("jni color %d=%x", color, color);
LOG_D("jni red %d=%x", red, red);
LOG_D("jni green %d=%x", green, green);
LOG_D("jni blue %d=%x", blue, blue);
LOG_D("jni alpha %d=%x", alpha, alpha);
}
red = 255 - red;
green = 255 - green;
blue = 255 - blue;
uint32_t newColor =
(alpha << 24) | ((blue << 16)) | ((green << 8)) | red;
if (i == 0 & j == 0) {
LOG_D("newColor %d=%x", newColor, newColor);
}
srcPix[w * i + j] = newColor;
}
}
AndroidBitmap_unlockPixels(env, bitmap);
return JNI_TRUE;
}
上面对像素的处理我们是按照二维数组的方式进行处理的,拿到abgr每个像素的值后进行处理后,然后再把每个abgr的值通过位移放到int的各自位上去即可。最后别忘了AndroidBitmap_unlockPixels
来释放解锁缓存。
我们测试一下并验证刚才的结论,在C层中Bitmap像素点的值是ABGR,我们在java和jni中各取第一行第一列的像素值并打印观察。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.normal);
int color = bitmap.getPixel(0, 0);
Log.e(TAG, "java getPixel[0][0] " + color + "=" + Integer.toHexString(color));
JNIBitmap jniBitmap = new JNIBitmap();
long start = System.currentTimeMillis();
if (jniBitmap.negative(bitmap) == 1) {
Log.e(TAG, "negative cost:" + (System.currentTimeMillis() - start));
imageView.setImageBitmap(bitmap);
int color2 = bitmap.getPixel(0, 0);
Log.e(TAG, "java getPixel[0][0] " + color2 + "=" + Integer.toHexString(color2));
}
日志打印:
可以看到java层的像素传到jni层确实是B和R交换了吧。
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp E/JNI: java getPixel[0][0] -5000782=ffb3b1b2
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni color -5066317=ffb2b1b3
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni red 179=b3
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni green 177=b1
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni blue 178=b2
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni alpha 255=ff
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni newColor -11710900=ff4d4e4c
2020-11-07 17:30:48.853 16236-16236/com.bj.gxz.jniapp E/JNI: negative cost:86
2020-11-07 17:30:48.854 16236-16236/com.bj.gxz.jniapp E/JNI: java getPixel[0][0] -11776435=ff4c4e4d
其它底片效果
比如黑白色的算法原理:
求RGB平均值Avg = (R + G + B) / 3,如果Avg >= 100,则新的颜色值为R=G=B=255;如果Avg < 100,则新的颜色值为R=G=B=0;255就是白色,0就是黑色;至于为什么用100作比较,这是一个经验值可以根据效果来调整。
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
uint32_t color = srcPix[w * i + j];
uint32_t blue = (color >> 16) & 0xFF;
uint32_t green = (color >> 8) & 0xFF;
uint32_t red = color & 0xFF;
uint32_t alpha = (color >> 24) & 0xFF;
uint32_t gray = (int) (red * 0.3f + green * 0.59f + blue * 0.11f);
gray = gray >= 100 ? 255 : 0;
uint32_t newColor = (alpha << 24) | (gray << 16) | (gray << 8) | gray;
srcPix[w * i + j] = newColor;
}
}
其它具体参考文章末尾的源代码。
返回新的bitmap不影响原始的bitmap
上面的操作都是基于原始的bitmap处理的,在jni侧改完后的bitmap随之对应的java侧的bitmap对象的像素也会改变,在有些情况下我们希望不想改变原来的。那这样就需要我们在jni层中创建一个java层的bitmap对象newbitmap,将处理好的数据保存到一个数组中。通过AndroidBitmap_lockPixels
获取一个指向像素内存的指针,然后把处理完后的数据memcpy
到该内存即可。部分代码为:
// 省略对原始bitmap处理的过程...
// resultBitmapPixels 为处理后的
jobject newBitmap = createBitmap(env, w, h);
void *resultBitmapPixels;
pixRet = AndroidBitmap_lockPixels(env, newBitmap, &resultBitmapPixels);
if (pixRet != ANDROID_BITMAP_RESULT_SUCCESS) {
LOG_D("AndroidBitmap_lockPixels %d", pixRet);
return nullptr;
}
memcpy(resultBitmapPixels, newBitmapPixels, sizeof(uint32_t) * w * h);
delete[] newBitmapPixels;
AndroidBitmap_unlockPixels(env, newBitmap);
jni层创建bitmap的代码如下,这个就是之前文章所讲的,如何在jni中创建java对象。
jobject createBitmap(JNIEnv *env, uint32_t w, uint32_t h) {
jclass clsBp = env->FindClass("android/graphics/Bitmap");
jmethodID createBitmapMid = env->GetStaticMethodID(clsBp, "createBitmap",
"(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
if (createBitmapMid == nullptr) {
LOG_E("createBitmapMid nullptr");
return nullptr;
}
jclass clsConfig = env->FindClass("android/graphics/Bitmap$Config");
if (clsConfig == nullptr) {
LOG_E("clsConfig nullptr");
return nullptr;
}
jmethodID valueOfMid = env->GetStaticMethodID(clsConfig, "valueOf",
"(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
if (valueOfMid == nullptr) {
LOG_E("valueOfMid nullptr");
return nullptr;
}
jstring configName = env->NewStringUTF("ARGB_8888");
jobject bitmapConfig = env->CallStaticObjectMethod(clsConfig, valueOfMid, configName);
jobject newBitmap = env->CallStaticObjectMethod(clsBp, createBitmapMid, w, h, bitmapConfig);
return newBitmap;
}
到此结束。
最后源代码:https://github.com/ta893115871/JNIAPP