拥抱 C/C++ : Android JNI 的使用

编译工具 CMake

在Android Studio 2.2 之后,工具中增加了 CMake 的支持,于是我们有两种选择来编译 c/c++ 代码。一个是 ndk-build + Android.mk + Application.mk 组合,另一个是 CMake + CMakeLists.txt 组合。这2个组合与 Android 代码和 c/c++ 代码无关,只是不同的构建脚本和构建命令。

环境配置

Android Studio 的 SDK Tools 安装

  • CMake
  • LLDB
  • NDK

Hello World

先新建一个项目,记得要勾选 C++ support,看一下 Android Studio 自动生成的使用了 JNI 的项目是什么样子的。

目录结构

可以看到,与普通 Android 项目不同的是,支持 C++ 的项目在 app 目录下多了一个 .externalNativeBuild 编译目录与 CMakeLists.txt,main 目录下多了 cpp 目录。

CMakeLists 文件

关于 CMakeLists 文件的作用,我的理解是它指定了编译 c++ 库时所用到的一些配置,先来看看项目里 CMakeList.txt 文件:

// 去掉注释
cmake_minimum_required(VERSION 3.4.1)
add_library(native-lib SHARED src/main/cpp/native-lib.cpp )
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib} )
  • cmake_minimum_required(VERSION 3.4.1)
    允许构建的最低版本
  • add_library(name path)
    生成链接库,SHARED 表示生成动态库, STATIC表示生成静态库。并指定了参与编译的文件路径
  • find_library(log-lib log)
    添加在编译本地文件时依赖的库(log),并指定别名(log-lib)
  • target_link_libraries(lib1 lib2 ...)
    链接库,这里链接了我们自己的库 native-lib 与 log 库

默认的 so 库输出目录为 app/build/intermediates/cmake/debug/obj/${abi} 下,可以在 CMakeLists 中指定输出目录

#设置生成的so动态库最后输出的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})

再来看 cpp 目录下的 native-lib.cpp 文件:

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_yazhidev_cmakedemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

第一次看到 .cpp 格式的源文件肯定有点摸不着头脑,Java 与 C++ 是如何通信的呢?答案就是 JNI。

JNI 规范

JNI (Java Native Interface,Java本地接口)是一种编程框架,使得 Java 虚拟机中的 Java 程序可以调用本地应用/或库,也可以被其他程序调用。

从上面的 native-lib.cpp 文件我们可以一窥 JNI 中 C/C++ 的使用规范:

  • #include
    C 语音中使用 #include <> 直接到系统指定的目录下查找文件,我将其理解为类似 Java 中的导包。JNI 中首先头部需要引入 <jni.h> ,由于使用到了字符串,还导入了 <string>

  • JNIEXPORT
    JNIEXPORT 和 JNICALL 都是 JNI 的关键字,表示此函数是要被 JNI 调用的。

  • jstring
    是 JNI 中作为中介使 JAVA 的 String 与 C/C++ 的 String 交互的数据类型,JNI的数据类型包含两种,分别是基本类型和引用类型。

  • jobject
    指代调用该方法的对象。如果 Java 中该 native 方法是静态的,则指代该类,即 XXX.class。

  • JNIEnv
    这个env可以看做是 JNI 接口本身的一个对象,在头部引入的 jni.h 头文件中存在着大量被封装好的函数,这些函数也是 JNI 编程中经常被使用到的,要想调用这些函数就需要使用JNIEnv这个对象。除了上面使用到的传递返回值给 Java,还有获取类的 class 类型:evn->GetObjectClass(),改变 Java 中对象的某个变量的值 evn-> SetIntField(...) 等方法。

  • 命名方式
    Java_com_yazhidev_cmakedemo_MainActivity_stringFromJNI(),JNI 中对命名有规定,命名规范为:Java_包名_class_函数名,包名中的 . 也要改为 _,对应的,在 Java 中引用 Native 函数也需要声明关键字 native。

  • std::string、NewStringUTF
    这是 C++ 中字符串的一些写法,需要用时去翻一下语法,不多延伸。

以上只提到了项目里使用到的一些规范和注意点。下面通过写个 demo 实际操作一下。

实战

通过 JNI 对图片做变色处理。

jnigraphics 库

这里要使用到 NDK 里提供的 jnigraphics 库,该库
提供了基于 C/C++ 的接口,可以访问 Android 中的 Bitmap 的像素缓冲区(bitmap buffers)。

头文件中引入 android/bitmap.h,其典型用法如下(摘至 android/bitmap.h 详解):

a) 用 AndroidBitmap_getInfo() 函数从位图句柄(从JNI得到)获得信息(宽度、高度、像素格式)
b) 用 AndroidBitmap_lockPixels() 对像素缓存上锁,即获得该缓存的指针。
c) 用C/C++ 对这个缓冲区进行读写
d) 用 AndroidBitmap_unlockPixels() 解锁

我们利用该用法对 bitmap 做处理。

新建 Module

首先新建个 module,并新建类 BitmapUtil:

static {
  // 不要忘记加载库
  System.loadLibrary("bitmap-util");
}

public class BitmapUtil {
    public static native void processBitmap(Bitmap bitmap;
}

并在 main 目录下新建 cpp 目录,新建类 bitmap-util.cpp:

#include <jni.h>
#include <android/bitmap.h>

extern "C"

JNIEXPORT void JNICALL
Java_com_yazhidev_ndkdemo_BitmapUtil_processBitmap(JNIEnv *env, jobject /* this */, jobject bitmap) {
    //构造 AndroidBitmapInfo
    AndroidBitmapInfo info = {0};
    //将 bitmp 的信息填充给 info
    AndroidBitmap_getInfo(env, bitmap, &info);
    int *buf=NULL;
    //对 bitmap 解码并获取解码后的像素保存在内存中的地址指针,赋值给 srcBuf
    AndroidBitmap_lockPixels(env, bitmap, (void **) &buf);
    //处理像素
    int w = info.width;
    int h = info.height;
    int32_t *srcPixs = (int32_t *) buf;
    int alpha = 0xFF << 24;
    int i, j;
    int color;
    int red;
    int green;
    int blue;
    for (i = 0; i < h; i++) {
        for (j = 0; j < w; j++) {
            // get the color of per pixel
            color = srcPixs[w * i + j];
            red = ((color & 0x00FF0000) >> 16);
            green = ((color & 0x0000FF00) >> 8);
            blue = color & 0x000000FF;
            color = (red + green + blue) / 3;
            color = alpha | (color << 16) | (color << 8) | color;
            srcPixs[w * i + j] = color;
        }
    }
    //释放锁定,显示出被修改的像素数据
    AndroidBitmap_unlockPixels(env, bitmap);
}

module 根目录下新建 CMakeLists.txt 文件:

cmake_minimum_required(VERSION 3.4.1)
add_library(bitmap-util SHARED src/main/cpp/bitmap-util.cpp )
# 链接 jnigraphics 库
target_link_libraries(native-lib jnigraphics)

在 module 的 build.gradle 中引用 CMakeLists 文件:

android {
  ...
  externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

Java 中调用:

// Kotlin image -> ImageView(android:id="@+id/image")
val drawable = resources.getDrawable(R.mipmap.google) as BitmapDrawable
val bitmap = drawable.bitmap
BitmapUtil.processBitmap(bitmap)
image.setImageBitmap(bitmap)

遇到的问题

java.lang.UnsatisfiedLinkError: No implementation found

extern "C"

扩展

项目中导入 so 库

在使用 JNI 时有时可能只有编译好的 so 库,那么如何在项目中使用 so 库呢?

右键 app 目录,选择 new - Folder -JNI Folder,新建一个 JNI 目录用于存放 so 文件。

so 库(CPU)的兼容

使用 CMake 编译 so 库时,可通过配置 gradle 文件指定编译的 so 库架构

android {
  defaultConfig {
    externalNativeBuild {
      cmake {
        cppFlags ""
        // 生成.so库的目标平台
        abiFilters "armeabi-v7a", "armeabi", "x86"
      }
    }
  }
}

对于CPU来说,不同的架构并不意味着一定互不兼容,根据目前Android共支持七种不同类型的CPU架构,其兼容特点可总结如下:

armeabi设备只兼容armeabi;
armeabi-v7a设备兼容armeabi-v7a、armeabi;
arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi;
X86设备兼容X86、armeabi;
X86_64设备兼容X86_64、X86、armeabi;
mips64设备兼容mips64、mips;
mips只兼容mips;

根据以上的兼容总结,我们还可以得到一些规律:
armeabi的SO文件基本上可以说是万金油,它能运行在除了mips和mips64的设备上,但在非armeabi设备上运行性能还是有所损耗;
64位的CPU架构总能向下兼容其对应的32位指令集,如:x86_64兼容X86,arm64-v8a兼容armeabi-v7a,mips64兼容mips。

更多 so 文件的信息可参考:Android SO文件的兼容和适配

参考

AndroidStudio项目CMakeLists解析
JNI技术规范
Android NDK之旅——图片高斯模糊
Jni接口-深入研究参数的传递(一)
android/bitmap.h 详解

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容