一、前言
注册JNI函数有两种方式:
- 静态注册
这种方法比较常见,用的是javah -jni xxxx
命令生成一组签名函数,并去实现这些函数。
静态注册方式的弊端:
(a)需要编译所有声明了native函数的Java类,每个所生成的class文件都得用javah命令生成一个头文件。
(b)javah生成的JNI层函数名特别长,书写起来很不方便。
(c)初次调用native函数时要根据函数名字搜索对应的JNI层函数来建立关联关系,这样会影响运行效率。
静态注册在前面已经提到过,详情请见:Android – JNI开发(静态注册) - 动态注册
Java的Native方法和JNI函数
是一一对应的,JNI中有一个JNINativeMethod
结构体来保存这个对应关系。我们可以通过重写JNI_OnLoad
函数来实现动态注册。
下面通过一个演示Demo来分析一下动态注册的步骤。
演示Demo下载:Gitee
二、方法解析
Android Studio目录结构图如下:
-
实现如下布局文件,Activity界面有两个按键,分别用来演示Java调用C层和C调用Java层过程,同时有两个TextView显示调用结果。
activity_main.xml
如下:<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorAntiqueWhite" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="10dp" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:layout_marginBottom="10dp" android:orientation="vertical" android:background="@color/colorHoneydew3"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPlum1" android:orientation="vertical" android:layout_weight="1"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1"> <TextView android:id="@+id/textview_java_to_c" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" android:gravity="center" android:text="@string/java_to_c" android:textSize="24sp" android:textStyle="bold" /> </RelativeLayout> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1"> <TextView android:id="@+id/textview_c_to_java" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" android:gravity="center" android:text="@string/c_to_java" android:textSize="24sp" android:textStyle="bold" /> </RelativeLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="10dp" android:background="@color/colorLightSkyBlue" android:layout_weight="9"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="5dp" android:layout_marginLeft="5dp" android:layout_marginRight="5dp" android:layout_marginBottom="5dp" android:background="@color/colorMediumPurple1" android:orientation="horizontal"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1"> <Button android:id="@+id/button_java_to_c" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPaleGreen" android:text="@string/java_to_c" android:textAllCaps="false" android:onClick="onClick"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1"> <Button android:id="@+id/button_c_to_java" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorMediumPurple1" android:text="@string/c_to_java" android:textAllCaps="false" android:onClick="onClick"/> </LinearLayout> </LinearLayout> </LinearLayout> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
-
文件定义
我们将C层文件定义为jni_test.c
,编译出来的库文件命名为jni_test
,app目录下的build.gradle
做如下配置,在
android{}
路径下添加一下配置:sourceSets.main{ jniLibs.srcDir 'src/main/libs' // 库文件输出目录 jni.srcDirs = [] } externalNativeBuild { ndkBuild { path file('src/main/jni/Android.mk') // Android.mk编译目录 } }
建立
src/main/jni/Android.mk
文件,主要是为了引入.c文件编译对应的库,里面内容为:LOCAL_PATH := $(call my-dir) LOCAL_MODULE := jni_test LOCAL_SRC_FILES := jni_test.c include $(BUILD_SHARED_LIBRARY)
建立
src/main/jni/Application.mk
文件,主要是为了生成所有架构的库文件:APP_ABI := all
编写
JNITest.java
文件,主要用于加载jni_test
库,编写native
方法及C
层回调方法,里面有详细的注释:package com.example.jnitestdemo; import android.util.Log; public class JNITest { // Log tag private static final String LOG_TAG = "JNITest"; private static MainView mView = null; static { System.loadLibrary("jni_test"); // 加载jni_test库 } public JNITest(MainView mainView) { this.mView = mainView; } /* C层调用Java层 */ public int CtoJavaReturn(int type) { debug("CtoJavaReturn: " + type); MainView view = JNITest.mView; mView.updateTextViewCtoJava(type); return 0; } /* Java调用C层 */ public native String javaToC(); public native String CtoJava(); /* debug */ private void debug(String info) { Log.d(LOG_TAG, info); } }
编写
jni_test.c
文件,重写JNI_OnLoad
函数,并结合registerNatives
函数实现动态注册关联各个方法与函数,代码详情见注释:// // Created by jerry on 2021/3/31. // #include <jni.h> #include <string.h> #include <sys/system_properties.h> #include <android/log.h> #include <stdlib.h> #include <string.h> #include <stdio.h> #include <assert.h> #include <time.h> #ifdef __cplusplus extern "C" { #endif /* 生成字符串 */ char* getString(int length) { int flag, i; char *string; srand((unsigned) time(NULL)); if ((string = (char *) malloc(length)) == NULL) { return NULL; } for (i = 0; i < length - 1; i++) { flag = rand() % 3; switch (flag) { case 0: string[i] = 'A' + rand() % 26; break; case 1: string[i] = 'a' + rand() % 26; break; case 2: string[i] = '0' + rand() % 10; break; default: string[i] = 'x'; break; } } string[length - 1] = '\0'; return string; } // #define JNIREG_CLASS "com.example.jnitestdemo.JNITest" // 指定要注册的类 Android 5.0之前用这种方式(.)隔开 #define JNIREG_CLASS "com/example/jnitestdemo/JNITest" // 指定要注册的类 Android 5.0之后用这种方式(/)隔开 void c_to_java_test(JNIEnv * env) { srand(time(0)); int type = rand() % 1000; jclass jClass = (*env)->FindClass(env, JNIREG_CLASS); if (jClass == 0) { return; } jobject jObject = (*env)->AllocObject(env,jClass); if (jObject == NULL) { return; } jmethodID jMethodId = (*env)->GetMethodID(env,jClass,"CtoJavaReturn", "(I)I"); if (jMethodId == NULL) { return; } (**env).CallIntMethod(env,jObject,jMethodId,type); } JNIEXPORT jstring JNICALL jni_call_java_to_c(JNIEnv *env, jclass clazz) { return (*env)->NewStringUTF(env, getString(5)); } JNIEXPORT jstring JNICALL jni_call_c_to_java(JNIEnv *env, jclass clazz) { c_to_java_test(env); return (*env)->NewStringUTF(env, "JNI Call C To Java.");; } /** * Table of methods associated with a single class. */ static JNINativeMethod gMethods[] = { { "javaToC", "()Ljava/lang/String;", (void*)jni_call_java_to_c }, { "CtoJava", "()Ljava/lang/String;", (void*)jni_call_c_to_java }, }; /* * Register several native methods for one class. */ static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods) { jclass clazz; clazz = (*env)->FindClass(env, className); if (clazz == NULL) { return JNI_FALSE; } if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { return JNI_FALSE; } return JNI_TRUE; } /* * Register native methods for all classes we know about. */ static int registerNatives(JNIEnv* env) { if (!registerNativeMethods(env, JNIREG_CLASS, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) return JNI_FALSE; return JNI_TRUE; } /* * Set some test stuff up. * * Returns the JNI version on success, -1 on failure. */ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; jint result = -1; if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { return -1; } assert(env != NULL); if (!registerNatives(env)) {//注册 return -1; } /* success -- return valid version number */ result = JNI_VERSION_1_4; return result; } #ifdef __cplusplus } #endif
建立一个接口文件,用于回调
MainActivity
,代码如下:package com.example.jnitestdemo; public interface MainView { void updateTextViewCtoJava(int info); }
最后完善
MainActivity.java
文件:package com.example.jnitestdemo; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.View; import android.widget.TextView; public class MainActivity extends AppCompatActivity implements MainView{ // Log tag private static final String LOG_TAG = "JNITestDemo"; // TextView private TextView mTextViewJavatoC = null; private TextView mTextViewCtoJava = null; // msg.what private static final int UPDATE_UI_JAVA_TO_C = 0x01; private static final int UPDATE_UI_C_TO_JAVA = 0x02; // UIHandler private UIHandler mUIHandler = null; // JNITest private static JNITest mJniTest = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); debug("onCreate"); initView(); initDataSource(); } /* initView */ private void initView() { mTextViewJavatoC = findViewById(R.id.textview_java_to_c); mTextViewCtoJava = findViewById(R.id.textview_c_to_java); } /* initDataSource */ private void initDataSource() { mUIHandler = new UIHandler(); mJniTest = new JNITest(this); } /* jniCallJavaToC */ private void jniCallJavaToC() { String text = mJniTest.javaToC(); debug("jniCallJavaToC: " + text); Message message = new Message(); message.what = UPDATE_UI_JAVA_TO_C; message.obj = text; mUIHandler.sendMessage(message); } /* jniCallCToJava */ private void jniCallCToJava() { mJniTest.CtoJava(); } /* Button onClick */ public void onClick(View view) { switch (view.getId()) { case R.id.button_java_to_c: debug("click java to c"); jniCallJavaToC(); break; case R.id.button_c_to_java: debug("click c to java"); jniCallCToJava(); break; default: break; } } @Override public void updateTextViewCtoJava(int info) { Message message = new Message(); message.what = UPDATE_UI_C_TO_JAVA; message.arg1 = info; mUIHandler.sendMessage(message); } /* UIHandler */ @SuppressLint("HandlerLeak") private class UIHandler extends Handler { @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); switch (msg.what) { case UPDATE_UI_JAVA_TO_C: mTextViewJavatoC.setText(String.valueOf(msg.obj)); break; case UPDATE_UI_C_TO_JAVA: mTextViewCtoJava.setText(String.valueOf(msg.arg1)); break; default: break; } } } /* debug */ private void debug(String info) { Log.d(LOG_TAG, info); } }
三、注意事项
-
指定要注册的类在不同
Android
版本下的区别#define JNIREG_CLASS "com.example.jnitestdemo.JNITest" // 指定要注册的类 Android 5.0之前用这种方式(.)隔开 #define JNIREG_CLASS "com/example/jnitestdemo/JNITest" // 指定要注册的类 Android 5.0之后用这种方式(/)隔开
-
数值类型要一一对应,否则编译会报错。
-
我们上面虽然给库命名的为
jni_test
,但实际编译出来的库名称为libjni_test.so
。这个我们不用去理会,AS会自动将库安装到对应目录下。
-