手把手教你如何在Android下进行JNI开发(入门)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 手把手教你如何在Android下进行JNI开发(入门)

在进行Android开发的过程中,我们必定会遇到视频图像处理、高强度密集运算、特殊算法等场景,这时我们就不得不需要去接触一些C/C++代码,进行JNI开发。下面我将从Android.mk和CMake这两种方式教大家如何进行开发。文章结尾将给出演示的项目代码,如果你能耐心地仔细看完,相信你一定能掌握如何在Android下进行JNI开发。


使用Android.mk进行JNI开发


1.编写native接口和C/C++代码


定义native接口


package com.xuexiang.jnidemo;
public class JNIApi {
    public native String stringFromJNI();
}


编写C/C++代码


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


2.编写Android.mk


模版如下:


LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := native-lib
LOCAL_SRC_FILES := native-lib.cpp
## 导入logcat日志库
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
include $(BUILD_SHARED_LIBRARY)


说明:


  • LOCAL_PATH := $(call my-dir) :指向当前目录的地址,包含该.mk


  • include $(CLEAR_VARS):清理掉所有以LOCAL_开头的内容,这句话是必须的,因为如果所有的变量都是全局的,所有的可控的编译文件都需要在一个单独的GNU中被解析并执行。


  • LOCAL_MODULE:调用的库名,用来区分android.mk中的每一个模块。文件名必须是唯一的,不能有空格。注意,这里编译器会为你自动加上一些前缀lib和后缀.so,来保证文件是一致的。


  • LOCAL_SRC_FILES:变量必须包含一个C、C++或者java源文件的列表,这些会被编译并聚合到一个模块中,文件之间可以用空格或Tab键进行分割,换行请用"\"


  • LOCAL_LDLIBS:定义需要链接的库。一般用于链接那些存在于系统目录下本模块需要链接的库(比如这里的logcat库)。


  • include $(BUILD_SHARED_LIBRARY):来生成一个动态库libnative-lib.so


3.编写Application.mk


# APP_ABI := armeabi armeabi-v7a arm64-v8a x86
APP_ABI := all
APP_OPTIM := release
## 引用静态库
APP_STL := stlport_static
#NDK_TOOLCHAIN_VERSION=4.8
#APP_PLATFORM := android-14


说明:


  • APP_ABI:定义编译so文件的CPU型号,all为所有类型。也可以指定特定类型的CPU型号,直接使用空格隔开。


  • APP_OPTIM:优化选项,非必填。其值可以为'release'或'debug'.此变量用来修改优先等级.默认情况下为release.在release模式下,将编译生成被优化了的二进制的机器码,而debug模块用来生成便于调试的未被优化的二进制机器码。


  • APP_STL:选择支持的C++标准库。在默认情况下,NDK通过Androoid自带的最小化的C++运行库(system/lib/libstdc++.so)来提供标准C++头文件.然而,NDK提供了可供选择的C++实现,你可以通过此变量来选择使用哪个或链接到你的程序。


APP_STL := stlport_static    --> static STLport library
APP_STL := stlport_shared    --> shared STLport library
APP_STL := system            --> default C++ runtime library


比如,这里我们使用到了#include <string>,就需要设置stlport_static


4.设置项目根目录的local.properties文件


因为Android Studio 2.2以后推荐使用CMake进行JNI开发,因此需要修改一下参数进行兼容。


android.useDeprecatedNdk=true


5.编译C/C++代码生成so文件


cd 到jni(存放Android.mk的目录)下,执行ndk-build即可。


执行成功后,将会在jni的同级目录下生成libsobj文件夹,存放的是编译好的so文件。


6.在模块的build.gradle中设置so文件路径


sourceSets {
    main {
        jni.srcDirs = []
        jniLibs.srcDirs = ['src/main/libs']
    }
}


至此完成了Android.mk的设置,下面我们就可以愉快地进行jni开发了!


上面介绍的Android.mk都可以在Eclispe和Android Studio下进行编译开发,可以说是一种比较传统的做法。下面我将介绍Android Studio着重推荐的CMake方式进行JNI开发。


使用CMake进行JNI开发


开发环境


JNI:Java Native Interface(Java 本地编程接口),一套编程规范,它提供了若干的 API 实现了 Java 和其他语言的通信(主要是 C/C++)。Java 可以通过 JNI 调用本地的 C/C++ 代码,本地的 C/C++ 代码也可以调用 java 代码。Java 通过 C/C++ 使用本地的代码的一个关键性原因在于 C/C++ 代码的高效性。


在 Android Studio 下,进行JNI的开发,需要准备以下内容:


  • Android Studio 2.2以上。


  • NDK:这套工具集允许为 Android 使用 C 和 C++ 代码。


  • CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果只计划使用 ndk-build,则不需要此组件。


  • LLDB:一种调试程序,Android Studio 使用它来调试原生代码。


微信截图_20220515210101.png


创建支持C++的项目


新建支持C++的项目


在新建项目时,勾上Include C++ support就行了:


微信截图_20220515210140.png


在向导的 Customize C++ Support 部分,有下列自定义项目可供选择:


  • C++ Standard:使用下拉列表选择使用哪种 C++ 标准。选择 Toolchain Default 会使用默认的 CMake 设置。


  • Exceptions Support:如果希望启用对 C++ 异常处理的支持,请选中此复选框。如果启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle文件的 cppFlags中,Gradle 会将其传递到 CMake。


  • Runtime Type Information Support:如果希望支持 RTTI,请选中此复选框。如果启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle文件的 cppFlags中,Gradle 会将其传递到 CMake。


微信截图_20220515210228.png


支持C++的项目目录


微信截图_20220515210300.png


  • src/main/cpp下存放的我们编写供JNI调用的C++源码。


  • CMakeLists.txt文件是CMake的配置文件,通常他包含的内容如下:


# TODO 设置构建本机库文件所需的 CMake的最小版本
cmake_minimum_required(VERSION 3.4.1)
# TODO 添加自己写的 C/C++源文件
add_library( native-lib
             SHARED
             src/main/cpp/native-lib.cpp )
# TODO 依赖 NDK中的库
find_library( log-lib
              log )
# TODO 将目标库与 NDK中的库进行连接
target_link_libraries( native-lib
                       ${log-lib} )


build.gradle的配置


android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                // 默认是 “ cppFlags "" ”
                // 如果要修改 Customize C++ Support 部分,可在这里加入
                cppFlags "-frtti -fexceptions"
            }
        }
        ndk {
            // abiFiliter: ABI 过滤器(application binary interface,应用二进制接口)
            // Android 支持的 CPU 架构
            abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'//, 'armeabi' 不支持了
        }
    }
    buildTypes {
        ...
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}


注意事项


  • 1.在使用JNI前,需要加载so库


static {
    System.loadLibrary("native-lib");
}


  • 2.快速生成C++代码:先在java中定义native方法,然后使用Alt + Enter快捷键自动生成C++方法体。


微信截图_20220515210344.png


  • 3.CPP 资源文件夹下面的文件和文件夹不能重名,不然 System.loadLibrary() 时找不到,会报错:java.lang.UnsatisfiedLinkError: Native method not found.


  • 4.在定义库的名字时,不要加前缀 lib 和后缀 .so,不然会报错:java.lang.UnsatisfiedLinkError: Couldn’t load xxx : findLibrary【findLibrary returned null错误.


  • 5.新建 C/C++ 源代码文件,要添加到 CMakeLists.txt 文件中。


# 增加c++源代码
add_library( # library的名称.
             native-lib
             # 标志库共享.
             SHARED
             # C++源码文件的相对路径.
             src/main/cpp/native-lib.cpp )
# 将目标库与 NDK中的库进行连接
target_link_libraries( # 目标library的名称.
                    native-lib
                    ${log-lib} )


  • 6.引入第三方 .so文件,要添加到 CMakeLists.txt 文件中。


# TODO 添加第三方库
# TODO add_library(libavcodec-57
# TODO 原先生成的.so文件在编译后会自动添加上前缀lib和后缀.so,
# TODO       在定义库的名字时,不要加前缀lib和后缀 .so,
# TODO       不然会报错:java.lang.UnsatisfiedLinkError: Couldn't load xxx : findLibrary returned null
add_library(avcodec-57
            # TODO STATIC表示静态的.a的库,SHARED表示.so的库
            SHARED
            IMPORTED)
set_target_properties(avcodec-57
                      PROPERTIES IMPORTED_LOCATION
                      # TODO ${CMAKE_SOURCE_DIR}:表示 CMakeLists.txt的当前文件夹路径
                      # TODO ${ANDROID_ABI}:编译时会自动根据 CPU架构去选择相应的库
                      # TODO ABI文件夹上面不要再分层,直接就 jniLibs/${ANDROID_ABI}/
                      # TODO ${CMAKE_SOURCE_DIR}/src/main/jniLibs/ffmpeg/${ANDROID_ABI}/libavcodec-57.so
                      ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libavcodec-57.so)


  • 7.引入第三方 .h 文件夹,也要添加到 CMakeLists.txt 文件中


# TODO include_directories( src/main/jniLibs/${ANDROID_ABI}/include )
# TODO 路径指向上面会编译出错(无法在jniLibs中引入),指向下面的路径就没问题
include_directories( src/main/cpp/ffmpeg/include )


  • 8.C++ library编译生成的so文件,在 build/intermediates/cmake


微信截图_20220515210433.png


至此完成了CMake的设置,下面我们就可以愉快地进行jni开发了!


讲完了两种进行JNI开发的姿势后,下面我们来简单讲讲JNI的基础语法。


JNI基础语法


基础类型


Java类型 native类型 描述
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A


引用类型


JNI为不同的java对象提供了不同的引用类型,JNI引用类型如下:



微信截图_20220515210513.png


在c里面,所有JNI引用类型其实都是jobject。


Native方法参数


  • JNI接口指针是native方法的第一个参数,JNI接口指针的类型是JNIEnv。


  • 第二个参数取决于native method是否静态方法,如果是非静态方法,那么第二个参数是对对象的引用,如果是静态方法,则第二个参数是对它的class类的引用


  • 剩下的参数跟Java方法参数一一对应


extern "C" /* specify the C calling convention */
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
     JNIEnv *env,        /* interface pointer */
     jobject obj,        /* "this" pointer */
     jint i,             /* argument #1 */
     jstring s)          /* argument #2 */
{
     const char *str = env->GetStringUTFChars(s, 0);
     ...
     env->ReleaseStringUTFChars(s, str);
     return ...
}


点击查看JNI接口


签名描述


基础数据类型


Java类型 签名描述
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void


引用数据类型


(以L开头,以;结束,中间对应的是该类型的完整路径)


String : Ljava/lang/String;
Object : Ljava/lang/Object;
自定义类型 Area : Lcom/xuexiang/jnidemo/Area;


数组


(在类型前面添加[,几维数组就在前面添加几个[)


int [] :[I
Long[][]  : [[J
Object[][][] : [[[Ljava/lang/Object


使用命令查看


javap -s <java类的class文件路径>


class文件存在于 build->intermediates->classes下。


微信截图_20220515210551.png


JNI常见用法


1、jni访问java非静态成员变量


  • 1.使用GetObjectClassFindClass获取调用对象的类


  • 2.使用GetFieldID获取字段的ID。这里需要传入字段类型的签名描述。


  • 3.使用GetIntFieldGetObjectField等方法,获取字段的值。使用SetIntFieldSetObjectField等方法,设置字段的值。


注意:即使字段是private也照样可以正常访问。


extern "C"
JNIEXPORT void JNICALL
Java_com_xuexiang_jnidemo_JNIApi_testCallNoStaticField(JNIEnv *env, jobject instance) {
    //获取jclass
    jclass j_class = env->GetObjectClass(instance);
    //获取jfieldID
    jfieldID j_fid = env->GetFieldID(j_class, "noStaticField", "I");
    //获取java成员变量int值
    jint j_int = env->GetIntField(instance, j_fid);
    LOGI("noStaticField==%d", j_int);//noStaticField==0
    //Set<Type>Field    修改noStaticKeyValue的值改为666
    env->SetIntField(instance, j_fid, 666);
}


2、jni访问java静态成员变量


  • 1.使用GetObjectClassFindClass获取调用对象的类


  • 2.使用GetStaticFieldID获取字段的ID。这里需要传入字段类型的签名描述。


  • 3.使用GetStaticIntFieldGetStaticObjectField等方法,获取字段的值。使用SetStaticIntFieldSetStaticObjectField等方法,设置字段的值。


3、jni调用java非静态成员方法


  • 1.使用GetObjectClassFindClass获取调用对象的类


  • 2.使用GetMethodID获取方法的ID。这里需要传入方法的签名描述。


  • 3.使用CallVoidMethod执行无返回值的方法,使用CallIntMethodCallBooleanMethod等执行有返回值的方法。


extern "C"
JNIEXPORT void JNICALL
Java_com_xuexiang_jnidemo_JNIApi_testCallParamMethod(JNIEnv *env, jobject instance) {
    //回调JNIApi中的noParamMethod
    jclass clazz = env->FindClass("com/xuexiang/jnidemo/JNIApi");
    if (clazz == NULL) {
        printf("find class Error");
        return;
    }
    jmethodID id = env->GetMethodID(clazz, "paramMethod", "(I)V");
    if (id == NULL) {
        printf("find method Error");
        return;
    }
    env->CallVoidMethod(instance, id, ++number);
}


4、jni调用java静态成员方法


  • 1.使用GetObjectClassFindClass获取调用对象的类


  • 2.使用GetStaticMethodID获取方法的ID。这里需要传入方法的签名描述。


  • 3.使用CallStaticVoidMethod执行无返回值的方法,使用CallStaticIntMethodCallStaticBooleanMethod等执行有返回值的方法。


5、jni调用java构造方法


  • 1.使用FindClass获取需要构造的类


  • 2.使用GetMethodID获取构造方法的ID。方法名为<init>, 这里需要传入方法的签名描述。


  • 3.使用NewObject执行创建对象。


extern "C"
JNIEXPORT jint JNICALL
Java_com_xuexiang_jnidemo_JNIApi_testCallConstructorMethod(JNIEnv *env, jobject instance) {
    //获取jclass
    jclass j_class = env->FindClass("com/xuexiang/jnidemo/Area");
    //找到构造方法jmethodID   public Area(int width, int height)
    jmethodID j_constructor_methoid = env->GetMethodID(j_class, "<init>", "(II)V");
    //初始化java类构造方法  public Area(int width, int height)
    jobject j_Area_obj = env->NewObject(j_class, j_constructor_methoid, 2, 10);
    //找到getArea()  jmethodID
    jmethodID j_getArea_methoid = env->GetMethodID(j_class, "getArea", "()I");
    //调用java中的   public int getArea() 获取面积
    jint j_area = env->CallIntMethod(j_Area_obj, j_getArea_methoid);
    LOGI("面积==%d", j_area);//面积==20
    return j_area;
}


6、jni引用全局变量


  • 使用NewGlobalRef创建全局引用,使用NewLocalRef创建局部引用。


  • 局部引用,通过DeleteLocalRef手动释放对象;全局引用,通过DeleteGlobalRef手动释放对象。


  • 引用不主动释放会导致内存泄漏。


7、jni异常处理


  • 使用ExceptionOccurred进行异常的检测。注意,这里只能检测java异常。


  • 使用ExceptionClear进行异常的清除。


  • 使用ThrowNew来上抛异常。


注意,ExceptionOccurredExceptionClear一般是成对出现的,类似于java的try-catch。


//上抛java异常
void throwException(JNIEnv *env, const char *message) {
    jclass newExcCls = env->FindClass("java/lang/Exception");
    env->ThrowNew(newExcCls, message);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_xuexiang_jnidemo_JNIApi_jniTryCatchException(JNIEnv *env, jobject instance) {
    //获取jclass
    jclass j_class = env->GetObjectClass(instance);
    //获取jfieldID
    jfieldID j_fid = env->GetFieldID(j_class, "method", "Ljava/lang/String666;");
    //检测是否发生Java异常
    jthrowable exception = env->ExceptionOccurred();
    if (exception != NULL) {
        LOGE("jni发生异常");
        //jni清空异常信息
        env->ExceptionClear(); //需要和ExceptionOccurred方法成对出现
        throwException(env, "native出错!");
    }
}


8、日志打印


#include <android/log.h> //引用android log
//定义日志打印的方法
#define TAG "CMake-JNI" // 这个是自定义的LOG的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型
LOGE("jni发生异常"); //日志打印




相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
6天前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
18天前
|
Android开发 开发者 Kotlin
探索安卓开发中的新特性
【9月更文挑战第14天】本文将引导你深入理解安卓开发领域的一些最新特性,并为你提供实用的代码示例。无论你是初学者还是经验丰富的开发者,这篇文章都会给你带来新的启示和灵感。让我们一起探索吧!
|
2天前
|
开发框架 移动开发 Android开发
安卓与iOS开发中的跨平台解决方案:Flutter入门
【9月更文挑战第30天】在移动应用开发的广阔舞台上,安卓和iOS两大操作系统各自占据半壁江山。开发者们常常面临着选择:是专注于单一平台深耕细作,还是寻找一种能够横跨两大系统的开发方案?Flutter,作为一种新兴的跨平台UI工具包,正以其现代、响应式的特点赢得开发者的青睐。本文将带你一探究竟,从Flutter的基础概念到实战应用,深入浅出地介绍这一技术的魅力所在。
18 7
|
6天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台解决方案
【9月更文挑战第27天】在移动应用开发的广阔天地中,安卓和iOS两大操作系统如同双子星座般耀眼。开发者们在这两大平台上追逐着创新的梦想,却也面临着选择的难题。如何在保持高效的同时,实现跨平台的开发?本文将带你探索跨平台开发的魅力所在,揭示其背后的技术原理,并通过实际案例展示其应用场景。无论你是安卓的忠实拥趸,还是iOS的狂热粉丝,这篇文章都将为你打开一扇通往跨平台开发新世界的大门。
|
3天前
|
缓存 Java Linux
探索安卓开发:从新手到专家的旅程
【9月更文挑战第30天】在这篇文章中,我们将一起踏上一段激动人心的旅程,探索安卓开发的广阔世界。无论你是刚入门的新手,还是希望提升技能的开发者,本文都将为你提供宝贵的知识和指导。我们将深入探讨安卓开发的基础知识、关键概念、实用工具和最佳实践,帮助你在安卓开发领域取得更大的成功。让我们一起开启这段精彩的旅程吧!
|
3天前
|
监控 安全 Java
Kotlin 在公司上网监控中的安卓开发应用
在数字化办公环境中,公司对员工上网行为的监控日益重要。Kotlin 作为一种基于 JVM 的编程语言,具备简洁、安全、高效的特性,已成为安卓开发的首选语言之一。通过网络请求拦截,Kotlin 可实现网址监控、访问时间记录等功能,满足公司上网监控需求。其简洁性有助于快速构建强大的监控应用,并便于后续维护与扩展。因此,Kotlin 在安卓上网监控应用开发中展现出广阔前景。
7 1
|
14天前
|
Android开发 开发者
安卓开发中的自定义视图:从入门到精通
【9月更文挑战第19天】在安卓开发的广阔天地中,自定义视图是一块充满魔力的土地。它不仅仅是代码的堆砌,更是艺术与科技的完美结合。通过掌握自定义视图,开发者能够打破常规,创造出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战应用,一步步展示如何用代码绘出心中的蓝图。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往创意和效率的大门。让我们一起探索自定义视图的秘密,将你的应用打造成一件艺术品吧!
38 10
|
7天前
|
存储 开发工具 Android开发
使用.NET MAUI开发第一个安卓APP
【9月更文挑战第24天】使用.NET MAUI开发首个安卓APP需完成以下步骤:首先,安装Visual Studio 2022并勾选“.NET Multi-platform App UI development”工作负载;接着,安装Android SDK。然后,创建新项目时选择“.NET Multi-platform App (MAUI)”模板,并仅针对Android平台进行配置。了解项目结构,包括`.csproj`配置文件、`Properties`配置文件夹、平台特定代码及共享代码等。
|
15天前
|
Java Linux Android开发
深入理解Android开发:从基础到高级
【9月更文挑战第17天】本文将深入探讨Android开发的各个方面,包括应用开发、操作系统等。我们将通过代码示例来展示如何创建一个简单的Android应用,并解释其背后的原理。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和启示。
|
13天前
|
存储 Java Android开发
🔥Android开发大神揭秘:从菜鸟到高手,你的代码为何总是慢人一步?💻
在Android开发中,每位开发者都渴望应用响应迅速、体验流畅。然而,代码执行缓慢却是常见问题。本文将跟随一位大神的脚步,剖析三大典型案例:主线程阻塞导致卡顿、内存泄漏引发性能下降及不合理布局引起的渲染问题,并提供优化方案。通过学习这些技巧,你将能够显著提升应用性能,从新手蜕变为高手。
16 2
下一篇
无影云桌面