Android NDK-0.JAVA和C++互调示例(mk构建)

官方参考:
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参看 ndk-build的Android.mk和Android.mk简介

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

1. 基础示例

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

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

 

1.1 JAVA调C++

 

1.1.1 生成so

在App中新建一个JAVA2C的类:

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

参看 Android Studio NDK快速生成.h头文件和native函数声明
利用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就够了,同时,会有如下提示:
在这里插入图片描述

特别注意,不要忘了extern "C" JNIEXPORT

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

1.1.1.1 限定只编译某种ABI的so

可能有这种需求,我们的手机基本都是arm64的,通常,so会编译多份,安装时会自动从apk中提出arm64的那一份进行安装,但我们可能想只运行 armeabi-v7ax86的so,那么可以在build.gradle(lib)中加入指定:

android {
	...
   externalNativeBuild {
            ndkBuild {
               abiFilters "armeabi-v7a", "x86"
            }
        }
 }

同样,我们也能在build.gradle(调用app)中加入指定:

android {
	...
	defaultConfig {
	 ...
        ndk {
            abiFilters "armeabi-v7a", "x86"
        }
	}
 }

也可以在Application.mk中加入指定:

APP_ABI := armeabi-v7a x86

 

1.1.2 调用接口

在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_TAG由 Android.mk 定义了-DLOG_TAG=\"VA++\"
将 Application.mk 设置为debugAPP_OPTIM := debug),就可以在JAVA2C.cpp下断点直接单步调试了,和VC++没什么区别。

1.1.3 RegisterNatives动态注册

Java_com_hgy413_ndkdemo_JAVA2C_BrawlStars_1Modify_1SkinsCSV这玩意太长了,有没有简单的方式呢,可以使用RegisterNatives动态注册。
在NDK层定义如下:

void NDK_BrawlStars_Modify_SkinsCSV_ByMap(JNIEnv *env, jclass, jobjectArray strKeyArray, jobjectArray strValueArray);
static JNINativeMethod brawlStarsMethods[] = {
		{"BrawlStars_Modify_SkinsCSV_ByMap", "([Ljava/lang/String;[Ljava/lang/String;)V",  (void *)NDK_BrawlStars_Modify_SkinsCSV_ByMap}
};

static int registerNativeMethods(JNIEnv *env, const char *className,
                                 JNINativeMethod *gMethods, int numMethods) {
    jclass clazz = env->FindClass(className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }
    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

NDK_BrawlStars_Modify_SkinsCSV_ByMap这个名字可以为任意命名,不需要像Java_com_hgy413_ndkdemo_JAVA2C_BrawlStars_1Modify_1SkinsCSV那样必须按包名类名来定义。

它的参数和BrawlStars_Modify_SkinsCSV_ByMap完全一致。一定要注意参数完全一致,因为有时你少传1个参数,编译器不会报错的。

调用方式,在JNI_OnLoad中调用:

  if (registerNativeMethods(env, "com/hgy413/ndkdemo/JAVA2C/BrawlStarsHook",brawlStarsMethods,
           sizeof(brawlStarsMethods) / sizeof(brawlStarsMethods[0])))
    {
    }

第二个参数传的是BrawlStarsHook.java的全路径。brawlStarsMethods是一个数组,所以我们可以同时动态注册多个函数。

Java_com_hgy413_ndkdemo_JAVA2C_BrawlStars_1Modify_1SkinsCSVregisterNativeMethods方法注册并存时,会优先调用registerNativeMethods方法注册的函数。

 

1.2 C++ 反调JAVA

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

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

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

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

ClassLoader是JVM加载class字节码文件的一种机制,具体可以参考: ClassLoader工作机制相关简介(热修复)

在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);
}

 

1.2.1.1 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

手工去写jni签名是非常容易出错的,所以我们利用javap工具可以方便的生成签名,比如我们定义了一个类FreeFireHook.java,它的代码如下:

package com.hgy413.reflexdemo;

public class FreeFireHook {
    private static native int nativeLoadPlugin(String SoPath, boolean dryRun, String jsonParam, String jsonCustom);
}

首先在文件上右键选择 Open in Terminal
在这里插入图片描述
然后输入以下两条命令,注意第二条命令最后一个.的前后都有空格
-s表示打印签名信息
-p表示打印所有函数和成员的签名信息,默认只会打印public的签名信息

javac FreeFireHook.java
javap -s -p -classpath . FreeFireHook

如下图所示:
在这里插入图片描述
还有一种更简单获取签名的方式,打开Android Studio->Settings->Plugins,点击下面的Browse respositories,搜索jclasslib
在这里插入图片描述
安装完后,打开一个java文件,点击右上角View->Show ByteCode with jclasslib,就可以在右边显示这个类的字节码信息了,比如要找的方法签名。

也可以配置javehexternal tools, 如下:
在这里插入图片描述
主要配置以下三条命令

$JDKPath$\bin\javap.exe
-s -p $FileClass$
$OutputPath$

注意要先生成apk,也就是有class文件后,才能使用javah这个扩展命令。

 

1.2.1.2 静态函数调用分析

静态调用函数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虚拟机实现方式不一样的原因。所以有申请就及时释放是一个好的习惯!

 

1.2.1.3 成员函数调用分析

调用成员函数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等,也同样提供了支持三种类型实参的函数实现

一个简单的示例,纯NDK层获取指定程序APK的版本号versionCode

static jobject getCurrentContext(JNIEnv *env)
{
    jclass ActivityThread_clz = env->FindClass("android/app/ActivityThread");
    jmethodID currentActivityThread = env->GetStaticMethodID(ActivityThread_clz,
                                                             "currentActivityThread",
                                                             "()Landroid/app/ActivityThread;");
    jobject currentActivityThread_obj = env->CallStaticObjectMethod(ActivityThread_clz,
                                                                    currentActivityThread);
    jmethodID getApplication = env->GetMethodID(ActivityThread_clz, "getApplication",
                                                "()Landroid/app/Application;");
    jobject context_obj = env->CallObjectMethod(currentActivityThread_obj, getApplication);
    return context_obj;
}

static int getApkVersion(JNIEnv *env, char *cPkgName)
{
    int ret = -1;
    if (cPkgName != nullptr)
    {
        // 获取Context的实例对象
        jobject context_obj = getCurrentContext(env);
        if (context_obj)
        {
            jclass Context_clz = env->GetObjectClass(context_obj);
            jmethodID getPackageManager = env->GetMethodID(Context_clz, "getPackageManager",
                                                           "()Landroid/content/pm/PackageManager;");
            jobject pm_obj = env->CallObjectMethod(context_obj, getPackageManager);
            jclass PackageManager_clz = env->GetObjectClass(pm_obj);
            jmethodID getPackageInfo = env->GetMethodID(PackageManager_clz, "getPackageInfo",
                                                        "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
            jstring packageName = env->NewStringUTF(cPkgName);
            jobject pi_obj = env->CallObjectMethod(pm_obj, getPackageInfo, packageName, 0);
            jclass PackageInfo_clz = env->GetObjectClass(pi_obj);
            jfieldID versionCode_fieldId = env->GetFieldID(PackageInfo_clz, "versionCode", "I");
            ret = (int) env->GetIntField(pi_obj, versionCode_fieldId);
        }
    }
    return ret;
}

 

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();// 这里是主线程,一直是需要的,不用退出

 

1.3 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;
}

特别注意,NDK是非常坑的,如果JNI_OnLoad中不写return,编译也是能通过的,但运行时会报JNI版本太低的错误

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

如果调试时发现进不了JNI_OnLoad(),看看工程的classpath配置,测试发现如果是
classpath 'com.android.tools.build:gradle:3.3.1',怎么都进不了JNI_OnLoad()

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

2.抽象类so调用示例

有时我们需要像windows一样提供一个so,这个so提供了一个抽象类接口供另一个so使用,也就是不需要每次dlsym去动态获取接口,dlsym太麻烦,而且容易出错。

2.1 抽象类提供方so

假定名字为interface.so, 对外提供的抽象类iPlugin.h:

class IPlugin {
public:
    virtual void on_dlopen(const char *name, void *handle) = 0;
    virtual int plugin_init(void *p) = 0;
    virtual char* plugin_invoke(const char *jsonParam) = 0;
};

__attribute__ ((visibility ("default"))) extern IPlugin* NPlugin();

NPlugin()创建一个实例类,动态调用抽象类接口,非常简洁。

某些NDK工程会在Application.mk加上APP_CPPFLAGS += -fno-rtti -fvisibility=hidden, 导致默认的接口全是非导出的。所以加上__attribute__ ((visibility ("default"))) ,来为了确保NPlugin()是导出的,可以用IDA查看导出表来确认。

实现体PluginImpl:

class PluginImpl : public IPlugin {
public:
    void on_dlopen(const char *name, void *handle){...}
    int plugin_init(void *p){...}
    char* plugin_invoke(const char *jsonParam){...}
};

2.2 抽象类使用方so

示例代码

2.2.1. 编译通过

假定为lib.so要使用interface.soIPlugin某个接口
1.iPlugin.h拷贝到lib.so的工程中,不一定要和interface.so中的iPlugin.h一致(子集即可),比如lib.so只使用了on_dlopen接口,那么lib.soiPlugin.h也可以定义为如下,其他接口可以砍掉:

class IPlugin {
public:
    virtual void on_dlopen(const char *name, void *handle) = 0;
};
extern "C" {
__attribute__ ((visibility ("default"))) extern IPlugin* NPlugin();
}

2.在lib.so的工程要放置一个interface.so, 我一般会放置在lib根目录下的libs目录(和src目录同级),当然还要加入ABI目录区分,
例如lib\libs\armeabi-v7a\interface.so
然后在lib.soandroid.mk中加入以下路径:

ifeq ($(TARGET_ARCH_ABI), armeabi-v7a)
    	LOCAL_LDFLAGS += $(MAIN_LOCAL_PATH)/../../../libs/armeabi-v7a/interface.so
endif

我这里只针对了armeabi-v7a,如果有其他ABI的so需求,就相应加入so并配置android.mk即可。
$(MAIN_LOCAL_PATH)/../../../libs/armeabi-v7a/libinterface.so指向了interface.so所在的绝对路径,根据自己的android.mk所在目录调整这部分内容。

如果你把interface.so放在其他自定义目录,lib.so不会自动把它加到ABI中,你还需要在build.gradle(lib)加入配置:

android {
		....
		  sourceSets{
        		main{
          				  jniLibs.srcDirs = ['xxx']
        			}
   				 }
}

xxx为相对于lib根目录的相对路径,比如放在lib\src\main\libs,那么jniLibs.srcDirs = ['src/main/libs']

前面做的全部目的是为了编译lib.so时,保证不会出现NPlugin()未找到链接这种错误。
所以同样,interface.so也不一定是真的和加载的interface.so是同一份,只需要它有NPlugin()这个导出接口即可。
因为是直接链接到interface.so,所以类似于window,系统会在启动时提前自动加载它能找到的interface.so,这一份就是真实的interface.so
要注意,如果interface.so需要依赖于其他的so,那么要保证其他的so也能被找到

2.2.2. 使用抽象类

lib.so中可以直接NPlugin()->on_dlopen(...);调用, 是不是非常方便。

如果要转发,你还可以写一个静态封装函数:

static void on_dlopen_s(const char *name, void *handle)
{
    if (NPlugin())
    {
        return NPlugin()->on_dlopen(name, handle);
    }
}

然后可以赋值给函数指针随意调用(必须先封成静态函数),比如下面p_on_dlopen

typedef void (*on_dlopen_func)(const char *name, void *handle);
 on_dlopen_func p_on_dlopen = (on_dlopen_func) (on_dlopen_s);
 p_on_dlopen(xx,xx)// 直接调用函数指针

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: android-ndk-r12b-windows-x86.zip 是一个 Android NDK 的压缩文件。Android NDK (Native Development Kit) 是一个让开发者能够使用 C 或 C++ 程序语言编写 Android 应用的工具集合。它主要用于开发需要高性能或使用底层库的应用程序。 android-ndk-r12b-windows-x86.zip 针对的是 Windows 系统的 32 位操作系统。该文件包含了 Android NDK 的相关文件和工具,可供开发者使用。开发者可以通过下载该文件,解压缩并配置环境变量,以便在 Windows 上编写和构建 C/C++ 代码的 Android 应用程序。 该压缩文件中包含了编译器、调试器、标准系统库、头文件以及其他构建和调试所需的文件。通过使用该工具集合,开发者可以充分利用 C/C++ 语言的速度和功能,编写高性能、复杂的 Android 应用程序。 同时需要注意的是,该压缩文件适用于 Windows 系统的 32 位操作系统。如果你的设备或操作系统是其他类型的,可能需要下载适用于该设备或操作系统的相应版本。 ### 回答2: android-ndk-r12b-windows-x86.zip 是一个Android NDK(Native Development Kit)的压缩文件。 NDK是一个用于开发Android平台上原生代码的工具集。它允许开发者在Java平台上编写Android应用程序的同时,通过使用C或C++等编程语言编写高性能和复杂度较高的代码模块。NDK通过将应用程序的代码编译为与硬件平台相关的机器码实现高效执行。 android-ndk-r12b-windows-x86.zip 这个文件是适用于Windows操作系统的NDK的压缩文件,特定于x86架构。这意味着该NDK版本适用于基于x86架构的32位执行环境,例如32位的Windows操作系统。 该压缩文件包含了开发者在Windows系统上使用NDK所需的所有文件和工具,如编译器、调试器、库文件等。开发者可以使用这个NDK版本来编写和构建在x86架构上运行的Android应用程序。 通过使用NDK,开发者可以利用C或C++编写更高效和功能强大的代码,也可以重用已有的C/C++代码库。这对于需要执行高计算密集型任务、需要访问底层硬件功能或需要与跨平台C/C++库集成的应用程序非常有用。 总之,android-ndk-r12b-windows-x86.zip 是一个适用于Windows操作系统的Android NDK的压缩文件,特定于x86架构。使用这个NDK版本,开发者可以编写高性能、复杂度较高的代码以实现更强大的Android应用程序。 ### 回答3: android-ndk-r12b-windows-x86.zip是一个用于Windows操作系统的Android NDK(Native Development Kit)软件包。Android NDK是一个开发工具集,可以帮助开发人员使用C和C++语言编写Android应用程序。它提供了一组库和工具,允许开发者直接使用本地代码进行开发。 android-ndk-r12b-windows-x86.zip包含了NDK的安装程序和相关的文件。用户可以通过解压缩这个压缩包来安装NDK。一旦安装完成,开发者就可以在Windows操作系统上使用NDK来开发Android应用程序。这个特定的压缩包适用于x86架构的计算机,这是一种常见的Windows计算机架构。 使用android-ndk-r12b-windows-x86.zip,开发者可以利用NDK的功能来更好地优化他们的Android应用程序。这包括直接使用本机代码编写应用程序的能力,以及访问底层系统功能的能力。通过使用NDK,开发者可以在性能关键的部分使用底层代码,提高应用程序的速度和效率。 android-ndk-r12b-windows-x86.zip是一个方便开发者使用Android NDK的软件包。它使开发者能够充分利用底层功能,并开发出更高效的Android应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值