安卓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层返回值为空方法的apiCallVoidMethod的返回值一定也为空,所以返回值省略。即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方法名必须遵守规则且长度无法控制
  • 过程较多
  • 运行效率不高,不安全。

动态注册优点

  • 流程清晰,过程可控
  • 运行效率高