Android NDK-0.使用Android.mk构建Jni实现JAVA和C++互调

官方参考:
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html

Android.mk和Application.mk参看 https://blog.csdn.net/hgy413/article/details/86471848

示例演示lib.soapp之间的c++java互调。

示例

首先创建一个项目NDKDemo,不需勾选 include c++ support,勾选是使用cmake编译的,这里演示使用.mk编译。
再创建一个名为lib的库(aar),New Module处选择Android Library即可,不要选错了。
如下图:

并让app依赖于lib.sobuild.gradle(app) 中加入implementation project(':lib')

JAVA调C++

生成so

在App中新建一个JAVA2C的类:

public class JAVA2C {
    public native String BrawlStars_Modify_SkinsCSV(String[] oldSkinNames, String[] newSkinNames, String heroName);
}

参看 https://blog.csdn.net/hgy413/article/details/82842768
利用javah 生成.h 文件,如下:

extern "C" {
JNIEXPORT jstring JNICALL Java_com_hgy413_ndkdemo_JAVA2C_BrawlStars_1Modify_1SkinsCSV
  (JNIEnv *, jobject, jobjectArray, jobjectArray, jstring);

这一步的目的为了获得jni的函数原型,在lib中任意找个目录(一般会放在jni文件夹中)建一个cpp文件,我是放在myjni目录下。
名字就叫JAVA2C.cpp,内容如下:

#include <jni.h>
extern "C"
JNIEXPORT jstring JNICALL Java_com_hgy413_ndkdemo_JAVA2C_BrawlStars_1Modify_1SkinsCSV
  (JNIEnv *, jobject, jobjectArray, jobjectArray, jstring){
  // 等下填充
  }

这时.h文件可以删除了,我们其实只需要cpp就够了,同时,会有如下提示:
在这里插入图片描述

myjni目录下创建Android.mk文件,内容如下:

LOCAL_PATH := $(call my-dir)
MAIN_LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := va++

LOCAL_C_INCLUDES += $(MAIN_LOCAL_PATH)

LOCAL_CFLAGS := -Wno-error=format-security -fpermissive -DLOG_TAG=\"VA++\"
LOCAL_CFLAGS += -fno-rtti -fno-exceptions

LOCAL_SRC_FILES := JAVA2C.cpp

LOCAL_LDLIBS := -llog -latomic
include $(BUILD_SHARED_LIBRARY)

myjni目录下创建Application.mk文件,内容如下:

APP_ABI := all
APP_PLATFORM := android-14
APP_STL := gnustl_static
APP_OPTIM := release

在build.gradle(lib)中加入指定:

android {
    .....
    externalNativeBuild {
        ndkBuild {
            path file("src/main/myjni/Android.mk")
        }
    }
}

Android.mkApplication.mk解释可以参看:https://blog.csdn.net/hgy413/article/details/86471848

到此,编译文件,就可以在apk包的lib目录内发现libva++.so

调用接口

在JAVA2C.java文件中加载libva++.so

public class JAVA2C {

    static {
        try {
            System.loadLibrary("va++");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public native String BrawlStars_Modify_SkinsCSV(String[] oldSkinNames, String[] newSkinNames, String heroName);
}

MainActivity.java中调用测试:

public class MainActivity extends Activity {

    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        JAVA2C java2c = new JAVA2C();
        String[] oldSkinNames = {"BanditGirlDefault","BanditGirlDefault1"};
        String[] newSkinNames = {"BanditGirlBandita","BanditGirlBandita1"};
        String strRet = java2c.BrawlStars_Modify_SkinsCSV(oldSkinNames,newSkinNames, "hgy");
        Log.e(TAG, "onCreate:" + strRet);
    }
}

将JAVA2C.cpp修改为如下:

#include <jni.h>
#include <string>
#include <android/log.h>

#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

extern "C"
JNIEXPORT jstring JNICALL Java_com_hgy413_ndkdemo_JAVA2C_BrawlStars_1Modify_1SkinsCSV
  (JNIEnv *env, jobject /* this */, jobjectArray oldSkinNames, jobjectArray newSkinNames, jstring heroName)
{

    jsize size = env->GetArrayLength(oldSkinNames);
    for(int i=0;i<size;i++)
    {
        jstring obj = (jstring)env->GetObjectArrayElement(oldSkinNames,i);
        std::string sstr = (std::string)env->GetStringUTFChars(obj,NULL);//得到字符串
        LOGE("[oldSkinNames]:%s", sstr.c_str());
    }

    size = env->GetArrayLength(newSkinNames);
    for(int i=0;i<size;i++)
    {
        jstring obj = (jstring)env->GetObjectArrayElement(newSkinNames,i);
        std::string sstr = (std::string)env->GetStringUTFChars(obj,NULL);//得到字符串
        LOGE("[newSkinNames]:%s", sstr.c_str());
    }

    std::string sstr = (std::string)env->GetStringUTFChars(heroName,NULL);//得到字符串
    LOGE("[heroName]:%s", sstr.c_str());

    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
  }

调用即可得到打印的结果,其中LOG_TAGAndroid.mk 定义了-DLOG_TAG=\"VA++\"
Application.mk 设置为debugAPP_OPTIM := debug),就可以在JAVA2C.cpp下断点直接单步调试了,和VC++没什么区别。

C++ 反调JAVA

参考:https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html
 

C/C++访问Java实例方法和静态方法

调用一个JAVA类的静态方法,直接通过类名.方法就可以调用,在这个调用过程中,JVM 是帮我们做了很多工作的。当我们在运行一个 Java 程序时,JVM 会先将程序运行时所要用到所有相关的 class 文件加载到 JVM 中,并采用按需加载的方式加载,也就是说某个类只有在被用到的时候才会被加载,这样设计的目的也是为了提高程序的性能和节约内存。

所以我们在用类名调用一个静态方法之前,JVM 首先会判断该类是否已经加载,如果没有被 ClassLoader 加载到 JVM 中,JVM 会从classpath 路径下查找该类,如果找到了,会将其加载到 JVM 中,然后才是调用该类的静态方法。如果没有找到,JVM 会抛出 java.lang.ClassNotFoundException 异常,提示找不到这个类。

ClassLoader是JVM加载class字节码文件的一种机制,在JNI开发当中,本地代码也是按照上面的流程来访问类的静态方法或实例方法的,下面通过一个例子,详细介绍本地代码调用Java方法流程当中的每个步聚:

App中增加C2JAVA.java

public class C2JAVA {
    private static void callStaticMethod(String str, int i) {
        System.out.format("ClassMethod::callStaticMethod called!-->str=%s," + " i=%d\n", str, i);
    }

    private void callInstanceMethod(String str, int i) {
        System.out.format("ClassMethod::callInstanceMethod called!-->str=%s, " + "i=%d\n", str, i);
    }
}

我们直接在上面的Java_com_hgy413_ndkdemo_JAVA2C_BrawlStars_1Modify_1SkinsCSV函数中做反调测试:

extern "C"
JNIEXPORT jstring JNICALL Java_com_hgy413_ndkdemo_JAVA2C_BrawlStars_1Modify_1SkinsCSV
  (JNIEnv *env, jobject /* this */, jobjectArray oldSkinNames, jobjectArray newSkinNames, jstring heroName)
{
	 // 在内部调下JAVA试试.
    C2jtest_static(env);
    c2jtest_method(env);
}
jni方法签名

下面代码中使用到的签名参数,规则如下图:

签名规则:(参数1类型签名参数2类型签名……参数n类型签名)返回值类型签名
其中类的签名规则是:”L+全限定类名+;”三部分组成。例如String,它位于java.lang.String类中,对应:Ljava/lang/String;

注意:返回值类型签名在括号后面,void对应V签名

所以下面用到的两个函数,void xx (String str, int i)对应为"(Ljava/lang/String;I)V"
又如long fun(int n, String str, int[] arr)对应为(ILjava/lang/String;[I)J

静态函数调用分析

静态调用函数C2jtest_static如下:

void C2jtest_static(JNIEnv *env)
{
    // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
    jclass clazz =env->FindClass("com/hgy413/ndkdemo/C2JAVA");
    if (clazz == NULL) {
        return;
    }

    // 2、从clazz类中查找callStaticMethod方法
    jmethodID mid_static_method = env->GetStaticMethodID(clazz,"callStaticMethod","(Ljava/lang/String;I)V");
    if (mid_static_method == NULL) {
        printf("找不到callStaticMethod这个静态方法。");
        return;
    }

    // 3、调用clazz类的callStaticMethod静态方法
    jstring str_arg = env->NewStringUTF("我是静态方法");
    env->CallStaticVoidMethod(clazz,mid_static_method, str_arg, 100);

    //4. 删除局部引用
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(str_arg);
}

1.调用FindClass函数,传入一个Class描述符,JVM会从classpath路径下搜索该类,并返回jclass类型。

2.调用GetStaticMethodID函数,从ClassMethod类中获取callStaticMethod方法ID,返回jmethodID类型(用于存储方法的引用),最后一个参数参看上面的jni方法签名介绍。

3.调用CallStaticVoidMethod函数,执行ClassMethod.callStaticMethod方法调用。str_arg和100是callStaticMethod方法的实参。

注意:JVM针对所有数据类型的返回值都定义了相关的函数。
返回类型为Void对应CallStaticVoidMethod
返回类型为int对应CallStaticIntMethod
返回类型为float对应CallStaticFloatMethod
返回类型为short对应CallStaticShortMethod
返回类型为Object对应CallStaticObjectMethod
另外,每种返回值类型的函数都提供了接收3种实参类型的实现:CallStaticXXXMethod(env, clazz, methodID, ...)CallStaticXXXMethodV(env, clazz, methodID, va_list args)CallStaticXXXMethodA(env, clazz, methodID, const jvalue *args) ,分别表示:接收可变参数列表、接收va_list作为实参和接收const jvalue*为实参。

4.释放局部变量
虽然函数结束后,JVM会自动释放所有局部引用变量所占的内存空间。但还是手动释放一下比较安全,因为在JVM中维护着一个引用表,用于存储局部和全局引用变量,经测试在Android NDK环境下,这个表的最大存储空间是512个引用,如果超过这个数就会造成引用表溢出,JVM崩溃。在PC环境下测试,不管申请多少局部引用也不释放都不会崩,我猜可能与JVM和Android Dalvik虚拟机实现方式不一样的原因。所以有申请就及时释放是一个好的习惯!

成员函数调用分析

调用成员函数c2jtest_method如下:

void c2jtest_method(JNIEnv *env) {
    // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
    jclass clazz = env->FindClass("com/hgy413/ndkdemo/C2JAVA");
    if (clazz == NULL) {
        return;
    }

    // 2、获取类的默认构造方法ID
    jmethodID mid_construct = env->GetMethodID(clazz, "<init>", "()V");
    if (mid_construct == NULL) {
        printf("找不到默认的构造方法");
        return;
    }

    // 3、查找实例方法的ID
    jmethodID mid_instance = env->GetMethodID(clazz, "callInstanceMethod",
                                              "(Ljava/lang/String;I)V");
    if (mid_instance == NULL) {
        return;
    }

    // 4、创建该类的实例
    jobject jobj = env->NewObject(clazz, mid_construct);
    if (jobj == NULL) {
        return;
    }

    // 5、调用对象的实例方法
    jstring str_arg = env->NewStringUTF("我是实例方法");
    env->CallVoidMethod(jobj, mid_instance, str_arg, 200);


    // 删除局部引用
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(jobj);
    env->DeleteLocalRef(str_arg);
}

1.同调用静态方法一样,首先通过FindClass函数获取类的Class对象。

2.获取类的构造方法ID,因为创建类的对象首先会调用类的构造方法。这里以默认构造方法为例

 jmethodID mid_construct = env->GetMethodID(clazz, "<init>", "()V");

<init>代表类的构造方法名称,()V代表无参无返回值的构造方法。

3.调用GetMethodID获取callInstanceMethod的方法ID,用于创建一个实例

  jmethodID mid_instance = env->GetMethodID(clazz, "callInstanceMethod",   "(Ljava/lang/String;I)V");

4.调用NewObject函数,创建类的实例对象

5.调用CallVoidMethod函数,执行ClassMethod.callInstanceMethod方法调用,str_arg和200是方法实参

6.删除局部引用(从引用表中移除)

同JNI调用Java静态方法一样,JVM针对所有数据类型的返回值都定义了相关的函数(CallXXXMethod)如:CallIntMethodCallFloatMethodCallObjectMethod等,也同样提供了支持三种类型实参的函数实现

JNIEnv跨线程问题

如果我们上面的代码不是通过java 调 c++的接口反调,是没有JNIEnv指针对象的,所以我们要想法创建它。

JNIEnv类型是一个指向全部JNI方法的指针。
JNIEnv是一个线程相关的变量。
JNIEnv 对于每个 thread 而言是唯一的。
JNIEnv *env指针不可以为多个线程共用。

解决方式:
JavaVM是虚拟机在JNI中的表示,一个JVM中只有一个JavaVM对象,这个对象是线程共享的。我们可以用JavaVM来得到当前线程的JNIEnv指针,可以使用javaAttachThread保证取得当前线程的Jni环境变量。

封装一个Environment类:

#ifndef NDK_ENVIRONMENT_H
#define NDK_ENVIRONMENT_H

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

class Environment {
public:
    static JavaVM* getVM();
    static JNIEnv* attachCurrentThread();
    static void detachCurrentThread();
};

#endif
#include <Environment.h>


JavaVM *g_jvm = NULL;
extern "C" JNIEXPORT void JNICALL Java_com_hgy413_ndkdemo_MainActivity_setJNIEnv
        (JNIEnv *env , jobject)
{
    env->GetJavaVM(&g_jvm);
}

JavaVM* Environment::getVM()
{
    return g_jvm;
}

JNIEnv* Environment::attachCurrentThread() {
    JavaVMAttachArgs args = {JNI_VERSION_1_6, NULL, NULL};
    JNIEnv* env = NULL;
    jint result = g_jvm->AttachCurrentThread(&env, &args);
    return env;
}

void Environment::detachCurrentThread() {
    // 当在一个子线程里面调用AttachCurrentThread后,如果不需要用的时候一定要DetachCurrentThread
    g_jvm->DetachCurrentThread();
}

我们在App的MainActivity初始化:

public class MainActivity extends Activity {

    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
		.....
        JAVA2C java2c = new JAVA2C();
        setJNIEnv();//初始化jinenv
        ......
    }

    public native void setJNIEnv();
}

如此,我们可以这样在C++中调用:

 JNIEnv* NewEnv = Environment::attachCurrentThread();
    C2jtest_static(NewEnv);
    c2jtest_method(NewEnv);
    //Environment::detachCurrentThread();// 这里是主线程,一直是需要的,不用退出
JNI_OnLoad

Dalvik虚拟机加载C库时,第一件事是调用JNI_OnLoad()函数
1.如果你的*.so没有提供JNI_OnLoad()函数,VM会默认该*.so是使用最老的JNI 1.1版本。
2.由于VM执行到System.loadLibrary()函数时,就会立即先调用JNI_OnLoad(),所以可以用它来做so初始化。

当然我们也可以用它来初始化我们的g_jvm对象了, 将Environment.cpp 增加一个方法:

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    Environment::initialize(vm);
    return JNI_VERSION_1_6;
}

void Environment::initialize(JavaVM* vm)
{
    g_jvm = vm;
}

这时就可以不在MainActivity 中调用setJNIEnv了。

JNI_OnUnload

当虚拟机释放该C库时,则会调用JNI_OnUnload()函数来进行善后清除动作。

参考:
https://blog.csdn.net/xyang81/article/details/42582213
https://www.jianshu.com/p/b1af53fefbd1

猜你喜欢

转载自blog.csdn.net/hgy413/article/details/86475739