文章目录
官方参考:
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.so和app之间的c++
和java
互调。
1. 基础示例
首先创建一个项目NDKDemo,不需勾选 include c++ support
,勾选是使用cmake编译的,这里演示使用.mk
编译。
再创建一个名为lib
的库(aar),New Module处选择Android Library即可,不要选错了。
如下图:
![](https://img-blog.csdnimg.cn/201901141319118.png)
并让app依赖于lib.so: build.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.mk
和Application.mk
解释可以参看:https://blog.csdn.net/hgy413/article/details/86471848
到此,编译文件,就可以在apk包的lib目录内发现libva++.so
。
1.1.1.1 限定只编译某种ABI的so
可能有这种需求,我们的手机基本都是arm64
的,通常,so会编译多份,安装时会自动从apk中提出arm64
的那一份进行安装,但我们可能想只运行 armeabi-v7a
或x86
的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 设置为debug(APP_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_1SkinsCSV
和registerNativeMethods
方法注册并存时,会优先调用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方法签名
下面代码中使用到的签名参数,规则如下图:
![](https://img-blog.csdnimg.cn/20190114174819589.png)
签名规则:(参数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
,就可以在右边显示这个类的字节码信息了,比如要找的方法签名。
也可以配置javeh
的 external 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
)如:CallIntMethod
、CallFloatMethod
、CallObjectMethod
等,也同样提供了支持三种类型实参的函数实现
一个简单的示例,纯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.so
的IPlugin
某个接口
1.iPlugin.h
拷贝到lib.so
的工程中,不一定要和interface.so
中的iPlugin.h
一致(子集即可),比如lib.so
只使用了on_dlopen
接口,那么lib.so
的iPlugin.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.so
的android.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