安卓NDK开发
2023-05-05
静态注册开发流程
以java层获取C层返回字符串为例的安卓应用开发流程
在java中定义native方法
参考以下例子编写java层的类,在类中定义需要通过c语言实现的native方法
package com.example.testndk;
import android.R.string;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 弹窗显示出native函数调用结果
// 第一个参数是上下文,这里直接传入this
// 第二个参数是显示内容,这里直接调用jni函数,打印函数返回值
// 第三个参数是弹窗时间
Toast.makeText(this, getstring(), Toast.LENGTH_LONG).show();
}
// native方法的定义 - 从c层得到返回字段
public native CharSequence getstring();
}
编写c语言源码 C向Java返回字符串
- 在安卓项目目录的 src 文件夹下,使用命令
javah -jni com.example.testndk.MainActivity
生成 JNI 样式的标头文件 (默认值) 。其中,说明文字是javah
中的说明,com.example.testndk.MainActivity
是native函数所在类的路径。此时在 src 下生成了名为 com.example.testndk.h 的头文件。 - 在项目目录下创建文件夹 jni (与 src 文件夹同级)。将生成的 com.example.testndk.h 改名为 JNI_study.h ,并移动到 jni 文件夹内。
- 在 jni 文件夹内创建同名的 JNI_study.c 文件并编写ndk函数
#include "JNI_study.h" // 调用生成的库文件
// 返回指定字段
// 函数名直接从生成的JNI_study.h中复制,并按规定补全固定的形参
JNIEXPORT jobject JNICALL Java_example_testndk_MainActivity_getstring(JNIEnv *env, jobject obj) {
// 在jni.h中搜索并复制出NewStringUTF,参考定义传入指定参数,从c层将数据返回java层
// 使用搜索第一次出现的结果,第一次是c语言接口,后续出现的是c++接口
// jstring (*NewStringUTF)(JNIEnv*, const char*);
jstring str = (*env) -> NewStringUTF(env, "Hello world");
return str;
}
- 通过 jni.h 中定义的api将so中的值返回给java
说明:
env
为结构体指针,(*env)
为结构体实例,(*env) -> NewStringUTF
表明调用结构体的NewStringUTF
通过Cmake打包
- 在 jni 文件夹内编写android mk文件 Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := JNI_study # 模块名,即头文件'.h'前的文件名
LOCAL_SRC_FILES := JNI_study.c # 源文件 .c/.cpp
LOCAL_ARM_MODE := arm # 编译后的指令集 ARM指令集
LOCAL_LDLIBS += -llog # 依赖库
include $(BULID_SHARED_LIBRARY) # 指定编译文件的类型
# 编译可执行文件 $(BULID_EXECUTABLE)
- 在 jni 文件夹内编写android mk文件 Application.mk
APP_API := armeabi-v7a
- 在 jni 文件夹执行命令
ndk-build
,在项目目录下生成libs/arm/libJNI_study.so
在java层声明so文件
将android mk文件中的模块名称作为导入名 (so文件去头去尾:libJNI_study.so -> JNI_study) 进行导入
package com.example.testndk;
import android.R.string;
public class MainActivity extends Activity {
// 声明导入的模块
static{
System.loadLibrary("JNI_study");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, getstring(), Toast.LENGTH_LONG).show();
}
public native CharSequence getstring();
}
静态注册下C调用Java的普通字段与静态字段
在java层定义普通字段与静态字段,和native方法声明
package com.example.testndk;
import android.R.string;
public class MainActivity extends Activity {
// java层的普通字段与静态字段
public String str1 = "Normal String";
public static String str2 = "Static String";
static{
System.loadLibrary("JNI_study");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, getNormalString(), 1).show();
Toast.makeText(this, getStaticString(), 1).show();
}
// native函数声明 - 从java层得到字段
private native CharSequence getNormalString();
private native CharSequence getStaticString();
}
普通字段
生成javah
头文件并根据头文件编写c语言源码。从下往上,缺啥补啥。
#include "JNI_study.h"
// 获取java层普通字段
JNIEXPORT jobject JNICALL Java_example_testndk_MainActivity_getNormalString(JNIEnv *env, jobject obj) {
// 在jni.h中搜索并复制出getObjectField,参考定义传入指定参数,从java层获取字段
// jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
// 获取字段id需要用到接口
// jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
// GetFieldID第二个参数jclass 需要用到接口FindClass指明字段所在类
// jclass (*FindClass)(JNIEnv*, const char*); 字符串为类的相对路径,即包+类名的'.'换成'/'
jclass j_class = (*env) -> FindClass(env, "com/example/testndk/MainActivity");
// GetFieldID后两个参数,第一个为java层字段变量名;第二个为返回值的签名,即smali的类型('.'改成'/',字符'L'开头,并加上';')
jfieldID j_fieldID = (*env) -> GetFieldID(env, j_class, "str1", "Ljava/lang/String;");
jobject str = (*env) -> GetObjectField(env, obj, j_fieldID);
return str;
}
静态字段
生成javah
头文件并根据头文件编写c语言源码。从下往上,缺啥补啥。先缺jfieldID,后缺jclass
#include "JNI_study.h"
// 获取java层静态字段
JNIEXPORT jobject JNICALL Java_example_testndk_MainActivity_getStaticString(JNIEnv *env, jobject obj) {
// 在jni.h中搜索并复制出GetStaticObjectField,参考定义传入指定参数,从java层获取字段
// jobject (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);
// 需要获取java类和java静态字段id
jclass j_class = (*env) -> FindClass(env, "com/example/testndk/MainActivity");
// jfieldID (*GetStaticFieldID)(JNIEnv*, jclass, const char*, const char*);
// GetStaticFieldID 后两个参数,第一个为java层字段变量名;第二个为返回值的签名,即smali的类型('.'改成'/',字符'L'开头,并加上';')
jfieldID j_fieldID = (*env) -> GetStaticFieldID(env, j_class, "str2", "Ljava/lang/String;");
jobject str = (*env) -> GetStaticObjectField(env, j_class, j_fieldID);
return str;
}
静态注册下C调用Java的普通方法与静态方法
- 在java层定义普通字段与静态字段,和native方法声明
package com.example.testndk;
import android.R.string;
public class MainActivity extends Activity {
static{
System.loadLibrary("JNI_study");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 静态方法中的toast需要上下文,静态方法不能用this
context = this;
// 调用native函数
getJavaMethod();
}
// java层的普通方法
public void method1() {
Toast.makeText(this, "Normal Method", 1).show();
}
// java层的静态方法
public static void method2() {
// static 静态方法获取上下文不能用this
Toast.makeText(context, "Static Method", 1).show();
}
// native函数声明
private native String getJavaMethod();
}
- 生成
javah
头文件并根据头文件编写c语言源码。从下往上,缺啥补啥。先缺jfieldID,后缺jclass
#include "JNI_study.h"
// 调用java层普通方法与静态方法
JNIEXPORT jstring JNICALL Java_example_testndk_MainActivity_getNormalMethod(JNIEnv *env, jobject obj) {
// ************调用java层普通方法************
// 获取java类
jclass j_class = (*env) -> FindClass(env, "com/example/testndk/MainActivity");
// 获取方法id
//jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);缺失class参数
// 第三个参数为java方法名,第四个参数是入参出参签名(例如:"(Ljava/lang/String;)Ljava/lang/String;")
jmethodID method1_id = (*env) -> GetMethodID(env, j_class, "method1", "()V");
// **调用java层返回空的普通方法,第三个参数为方法id(需要api获取),后续参数为目标函数的入参
(*env) -> CallVoidMethod(env, obj, method1_id);
// ************java层静态方法************
// jmethodID (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
jmethodID method2_id = (*env) -> GetStaticMethodID(env, j_class, "method2", "()V");
// 调用java层静态方法
// void (*CallStaticVoidMethod)(JNIEnv*, jclass, jmethodID, ...);
(*env) -> CallStaticVoidMethod(env, j_class, method2_id);
// 函数需要jstring返回值 - java函数声明为native String,需要一个string返回值
return (*env)->NewStringUTF(env,"111");
}
说明:
- 由于调用java层返回值为空方法的api
CallVoidMethod
的返回值一定也为空,所以返回值省略。即void call1 = (*env) -> CallVoidMethod(env, obj, method1_id);
省略为(*env) -> CallVoidMethod(env, obj, method1_id);
。 - java层方法的入参若为字符串,需要通过jni接口创建java类型的字符串传入函数。
str_arg = (*env)->NewStringUTF(env,"我是java层方法的字符串参数");
动态注册开发流程
在java中定义native方法
参考以下例子编写一个计算器应用(两个操作数输入框,加减乘除四个操作按钮,toast弹出结果)。在java层的类中定义需要通过c语言实现的native方法
package com.example.calc;
import android.os.Bundle;
public class MainActivity extends Activity {
// 第一个操作数
private EditText num1;
// 第二个操作数
private EditText num2;
// 操作按钮 加、减、乘、除
private Button add;
private Button sub;
private Button mul;
private Button div;
// native函数的输入参数
private float arg1;
private float arg2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 控件初始化函数的调用
init();
// 操作
run();
}
// 控件初始化函数的定义 - 绑定变量与控件
private void init() {
// 绑定操作数据 - 从输入框获取操作数,需要强制类型转换
// R.id.*为布局xml中的id
num1 = (EditText)findViewById(R.id.editText1);
num2 = (EditText)findViewById(R.id.editText2);
// 绑定操作按钮
add = (Button)findViewById(R.id.add);
sub = (Button)findViewById(R.id.sub);
mul = (Button)findViewById(R.id.mul);
div = (Button)findViewById(R.id.div);
}
// 计算方法
private void run() {
// 监听触发了哪种计算
final OnClickListener click= new setOnClickListener() {
@Override
pubilc void onClick(View v) {
switch (v.getId()) {
case R.id.add:
// 将输入框内容解析成数字作为传入参数(需要放到监听内动态获取,不然只获取一次)
arg1 = Float.parseFloat(num1.getText().toString());
arg2 = Float.parseFloat(num2.getText().toString());
// 直接this是setOnClickListener的上下文, 弹出内容需要转换成字符串
Toast.makeText(MainActivity.this, add(arg1, arg2)+"", 1).show()
break
case R.id.sub:
arg1 = Float.parseFloat(num1.getText().toString());
arg2 = Float.parseFloat(num2.getText().toString());
Toast.makeText(MainActivity.this, sub(arg1, arg2)+"", 1).show()
break
case R.id.mul:
arg1 = Float.parseFloat(num1.getText().toString());
arg2 = Float.parseFloat(num2.getText().toString());
Toast.makeText(MainActivity.this, mul(arg1, arg2)+"", 1).show()
break
case R.id.div:
arg1 = Float.parseFloat(num1.getText().toString());
arg2 = Float.parseFloat(num2.getText().toString());
Toast.makeText(MainActivity.this, div(arg1, arg2)+"", 1).show()
break
default:
break
}
}
};
// 绑定监听事件
add .setOnClickListener(click);
sub .setOnClickListener(click);
mul .setOnClickListener(click);
div .setOnClickListener(click);
}
// native方法
public native float add(float arg1, float arg2);
public native float sub(float arg1, float arg2);
public native float mul(float arg1, float arg2);
public native float div(float arg1, float arg2);
}
编写c语言源码
- 在项目目录下创建文件夹 jni (与 src 文件夹同级)。在 jni 文件夹内创建c语言源文件 calcnative.c 文件并编写ndk函数(文件名自定义即可)
#include <jni.h> // 导入jni接口头文件
// 加法计算参数, 前两个参数固定, 后续为java层方法参数。函数名无需与java层一样
jfloat add_c(JNIEnv* env, jobject obj, jfloat a, jfloat b) {
return a+b;
}
jfloat sub_c(JNIEnv* env, jobject obj, jfloat a, jfloat b) {
return a-b;
}
jfloat mul_c(JNIEnv* env, jobject obj, jfloat a, jfloat b) {
return a*b;
}
jfloat div_c(JNIEnv* env, jobject obj, jfloat a, jfloat b) {
return a/b;
}
// 动态注册JNINativeMethod结构体 - jni.h中的定义
// typedef struct {
// const char* name; // java方法名
// const char* signature; // 参数签名
// void* fnPtr; // 函数指针fnPtr,指向c层的jni函数
// } JNINativeMethod;
// 绑定c层的jni函数与java层的native函数的数组,数组名自定义即可
// 通过jni.h中的结构体定义数组。
JNINativeMethod nativeMethod[] = {
// java方法名,(参数)签名,void函数指针
{"add", "(FF)F", (void*)add_c},
{"sub", "(FF)F", (void*)sub_c},
{"mul", "(FF)F", (void*)mul_c},
{"div", "(FF)F", (void*)div_c}
};
// 注册函数,此函数将由JNI_Onload调用 - 自定义名称 - 函数内使用RegisterNatives接口对函数进行注册
// jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*, jint);
jint registerNative(JNIEnv* env) {
// 方法所在类的相对路径
jclass j_class = (*env) -> FindClass(env, "com/example/calc/MainActivity");
// 需要获取类、数组指针、元素个数, 返回值为注册结果
if((*env) -> RegisterNatives(env, j_class, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0])) != JNI_OK) {
return JNI_ERR;
}
return JNI_OK;
}
// 编写JNI_Onload进行动态注册,第二个参数为保留参数
// **系统自动调用**, 需要返回版本号
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
// 获取env用于注册 - vm,env的地址,jni版本号
// jint (*GetEnv)(JavaVM*, void**, jint);
if((*vm) -> GetEnv(vm, (void**)&env, JNI_VERSION) != JNI_OK) {
// 获取env失败
return JNI_ERR;
}
if (registerNative(env) != JNI_OK) {
// 注册失败
return JNI_ERR;
}
// 全部成功,返回版本号
return JNI_VERSION;
}
通过Cmake打包
- 在 jni 文件夹内编写android mk文件 Android.mk
LOCAL_PATH := $(call my-dir) # 返回jni文件路径,便于java导入
include $(CLEAR_VARS) # 清理 LOCAL_PATH 以外,LOCAL 开头的变量,防止全局变量相互影响
LOCAL_MODULE := calcnative # 模块名 - 必须唯一,编译成so,会生成 lib(module).so
LOCAL_SRC_FILES := calcnative.c # 源文件 .c/.cpp
LOCAL_ARM_MODE := arm # 编译后的指令集 ARM指令集
LOCAL_LDLIBS += -llog # 依赖库
include $(BULID_SHARED_LIBRARY) # 指定编译文件的类型 - 此处为动态链接库
# 编译可执行文件 $(BULID_EXECUTABLE)
# 编译静态链接库 $(BUILD_STATIC_LIBRARY)
- 在 jni 文件夹内编写android mk文件 Application.mk
APP_API := armeabi-v7a
# APP_API := x86 armeabi-v7a # 包含x86
- 在 jni 文件夹执行命令
ndk-build
,在项目目录下生成libs/arm/libcalcnative.so
在java层声明so文件
将android mk文件中的模块名称作为导入名 (so文件去头去尾:libJNI_study.so -> JNI_study) 进行导入
package com.example.calc;
import android.os.Bundle;
public class MainActivity extends Activity {
// 声明导入的模块
static{
System.loadLibrary("calcnative");
}
}
静态注册与动态注册的优缺点
静态注册缺点
- 编写不方便,jni方法名必须遵守规则且长度无法控制
- 过程较多
- 运行效率不高,不安全。
动态注册优点
- 流程清晰,过程可控
- 运行效率高