C++开发支持Android共享库(so)教程
概述:
Android是运行于Linux上的移动系统,Android开发语言是java,Linux开发语言是C/C++。虽然google和java为Android提供了大部份功能库和SDK,但在一些情况下还是需要使用C++开发一些功能供Android调用,扩充Android功能。比如我们需要开发特有的功能库,但这样的库java没有提供;比如我们需要调用Linux系统的API,但java没有提供这样的调用接口等。
通过JNI(Java Native Interfac,中文为JAVA本地调用,通过这个接口可以实现C++,java相互调用)的支持可以开发出支持java调用的C++共享库(so),通过Android的NDK编译,可以生成支持Android调用的so库。
开发支持Android调用的so库大概需要经过下列三大步:
1) 用C/C++开发出Linux的so库,如果已经熟悉可跳过阅读。
2) 在C/C++代码里加入JNI,支持java通过JNI调用so库,如果已经熟悉可跳过阅读。
3) 通过android的NDK编译,生成支持Android调用的so库。
下面我们通过这三步实现Android调用C/C++开发so库
4) 补充主题
用C/C++开发出Linux的so库。
开发分析:
一个so需要提供允许外面调用的接口和so内部调用外部的调口,因些我们的so库需要向外部提供三个接口:1注册一个外部接口到so,so运行过程中调用;2..外部调用so功能的接口。现在我们把第一个接口定义成外部通过个接口注册一个接口个so内部,第二个接口设计通过外部调用,so内部开始调用第一个接口注册外部函数。
逻辑分析:1.定义一个全局的函数指针,第一个接口注册函数把外部函数指针保到这个变量里;2.第二个启动函数开启一个线程,这个线程定时调用全局函数指针。
开发准备:
1、ubuntu14-server,搭建好C/C++开发环境,搭建过程不在本教程讨论范围,搭建过程可参考其它文档。
2、C++ 代码编辑IDE,一个普通的文件编辑器或专业的C++IDE都可。
编写so库的代码如下
新建一个C++文件jni_demo1.cpp 在文件输入下列代码
//本文件演示Android通过JNI调用C++开发so过程
#include <iostream>
#include <pthread.h> //线程库
#include <unistd.h> //usleep函数
using namespace std;
typedef void (*pExternalFunction)(unsigned int iParam);
pExternalFunction g_pExternalFunction = NULL; //实现函数指针,保存外部注册的函数
//外部向so注册一个函数供so内部调用
extern "C" void RegisterExternalFunction(pExternalFunction pExFun)
{
g_pExternalFunction = pExFun; //把回调函数保存起来
}
//线程启动后,定时调用外部注册的函数
void* start_routine(void *pParam)
{
if(NULL == g_pExternalFunction){
cout<<"start_routine pParam is NULL"<<endl;
return (void*)0;
}
unsigned int iParam = (unsigned int)pParam;
for(int i=0; i<2; i++){
g_pExternalFunction(iParam); //调用外部函数,并把参数传入
usleep(10000);
}
return (void*)0;
}
//创建一个线程,通过线程定时调用外部注册的回调数
extern "C" bool Start(unsigned int iParam)
{
pthread_t thread = 0;
pthread_attr_t attr;
void *status = NULL;
if(0 != pthread_create(&thread, NULL,start_routine, (void*)iParam)){
cout<<"pthread_create no"<<endl;
return false;
}
int iResult = pthread_join(thread, &status); //等待线程结束返回
return true;
}
编写makefile把上述代码编译成so库
1) 在jni_demo1.cpp同目录下新建一个文件makefile,这个主文件没有扩展名,输入下面代码。
jni_demo1.so:jni_demo1.o
g++ -shared -fpic -o jni_demo1.so jni_demo1.o -pthread
jni_demo1.o:jni_demo1.cpp
g++ -c jni_demo1.cpp
.PHONY:clean
clean:
rm -rf *.o
rm -rf *.so
2)将这两个文件放到Linux某个目录上,jni_demo1.cpp和makefile在同一目录,进入存放这两个文件的目录,执行make编译,输出如下信息,产生一个so共享库。
jni_demo1.so就是我们的目录文件so共享库文件
3) 编写一个测试文件,测试so文件是否正常工作。在同级目录下新建一个tmain1.cpp文件,输入下面代码
//演示怎样调用so接口
#include <iostream>
#include <dlfcn.h>
#include <unistd.h> //usleep函数
using namespace std;
typedef void (*pExternalFunction)(unsigned int iParam);
//定义so的函数原型
typedef void (*pRegisterExternalFunction)(pExternalFunction pExFun);
typedef bool (*pStart)(unsigned int iParam);
//注册到so内部,高so从内部调用的函数
void ExternalFunction(unsigned int iParam)
{
cout<<"ExternalFunction out iParam="<<iParam<<endl;
}
int main(int argc, char *argv[])
{
void *handle = NULL;
pRegisterExternalFunction RegisterExternalFunction = NULL;
pStart Start = NULL;
//加载so库,相当于Win32的LoadLibrary
handle = dlopen("/home/pengac/jni/jni_demo1.so", RTLD_LAZY); ///home/pengac/jni/jni_demo1.so 共享库存放的绝对地址,根据实现环境设置
if(NULL == handle){
cout<<"dlopen jni_demo1.so failed"<<endl;
cout<<dlerror()<<endl;
return 0;
}
//获取so库里的函数
RegisterExternalFunction = (pRegisterExternalFunction)dlsym(handle,"RegisterExternalFunction");
if(NULL == RegisterExternalFunction){
cout<<"dlsym get RegisterExternalFunction failed"<<endl;
cout<<dlerror()<<endl;
goto out;
}
Start = (pStart)dlsym(handle,"Start");
if(NULL == Start){
cout<<"dlsym get Start failed"<<endl;
cout<<dlerror()<<endl;
goto out;
}
//调用函数so提供的接口
RegisterExternalFunction(ExternalFunction); //注册外部函数到so
if(true == Start(55)){
cout<<"Start return true"<<endl;
}
else
{
cout<<"Start return failed"<<endl;
}
out:
//释放so库,相当于Win32的FreeLibrary
if(0 != dlclose(handle))
{
cout<<"dlclose jni_demo1.so failed"<<endl;
cout<<dlerror()<<endl; //输出失败原因
return 0;
}
return 0;
}
在控制台上执行 g++ tmain1.cpp -ldl 编译,产生可执行文件a.out,总共文件如下图所示
执行./a.out测试so是否正常,输入如下图结果表示so工作正常。
结论:
通过上述步骤已经实现了一个可以正常工作的so库,本so库共暴露出两个接口RegisterExternalFunction和Start。RegisterExternalFunction注册一个部外部方法到so内部,Start在so内部启动一个线程,实现从so内部调用外部方法的示例。通过上述演示,我们清楚地看到了外部怎样调用so方法和so怎么调用外部方法的过程,接下来我们通过往上述so加入JNI支持,实现java调用so方法和so调用java方法示例
在C/C++代码里加入JNI,支持java通过JNI调用so库
前述
1、JNI 是Java Native Interface的缩写,中文译为“Java本地调用”。通常使用JNI技术可以做到以下两点:
(1)Java程序中的函数可以调用Native语言函数,Native函数一般指的是C/C ++编写的函数;
(2)Native程序中的函数可以调用Java层的函数,也就是说在C/C++程序中可以调用Java层函数。
2、当然任何事物都有两面性,JNI也不例外,使用JNI主要缺点有以下两点:
(1)使用JNI技术将导致Java Application不能跨平台。如果要移植到别的平台上,那么Native代码就要重新进行编写;
(2)Java是强类型的语言,而C/C++不是,因此在编写JNI的时候要非常谨慎。
3、Android主要使用java开发,但Android可以通过java的JNI调用C/C++编写的so库,实现Android对C/C++的支持。接下来我们在前面so的基础上加入JNI,以支持java通过JNI调用so的方法。
Java部分
1、搭建java环境、
(1)在控制台输入java,如果电脑上还没有安装,则显示下面信息,让我们选择一个安装
(2)我选择的是default-jre,在控制台`输入sudo apt-get install default-jre,等待一段时间后显示下面,表示安装结束。
(3)执行命令java -version,显示下面所示信息表示安装成功
(4)我们需要javah把java声明的接口生成C++对应的接口,输入javah,如果出下图信息,表示还没安装。
我选择安装openjdk-7-jdk,执行命令 sudo apt-get install openjdk-7-jdk 安装。如果安装过程没有出错,将出现下图所示。
输入javah -version出现下图所示信息,表示安装成功
2、编写java与C++对应的接口代码,在C++文件同级目录下新建一个tjni.java文件,输入下面代码
//本文件演示如何通过JNI实现java与C++互相调用
import java.util.*;
//定义供so调用的java外部函函数
class ExternalFunction
{
public void externalFunction(int iParam)
{
System.out.println("This is so invoke java function iParam=" + iParam);
}
}
public class tjni
{
//声明与so对应的两个函数,native关键字表示
public native void RegisterExternalFunction(ExternalFunction func);
public native boolean Start(int iParam);
static{
System.loadLibrary("jni_demo2"); //在win32下,加载jni_demo2.dll 在Linux下加载libjni_demo2.so,注意:库文件必须放在任何java.library.path包含的路径中,可用 System.getProperties().list(System.out);打印出来
}
public static void main(String[] args)
{
tjni obj = new tjni();
ExternalFunction Efun = new ExternalFunction();
obj.RegisterExternalFunction(Efun); //注册外部的函数到so
if(true == obj.Start(55)) //调用so的接口函数
{
System.out.println("Start return true");
}
else
{
System.out.println("Start return false");
}
}
}
3、编译tjni.java文件。
执行javac tjni.java 命令,如果没有输出任何信息,表示编译成功,这时将产生两个 .class文件:tjni.class;ExternalFunction.class
4、生成对应的C++头文件
a) 执行 javah tjni 命令,如果没误发生,将产生tjni.h文件,文件里声明了两个函数,与tjni.java 的两个navtive函数对应,分别是:
JNIEXPORT void JNICALL Java_tjni_RegisterExternalFunction
(JNIEnv *, jobject, jobject) 与 public native void RegisterExternalFunction(ExternalFunction func) 对应。
JNIEXPORT jboolean JNICALL Java_tjni_Start
(JNIEnv *, jobject, jint) 与 public native boolean Start(int iParam) 对应。
5、在原来的so的C++代码基础上,加入JNI支持so与java互相调用。
a) 把原来的 jni_demo1.cpp 改成 jni_demo2.cpp,并把代码修改成如下代码
//本文件演示Android通过JNI调用C++开发so过程
#include <iostream>
#include <pthread.h> //线程库
#include <unistd.h> //usleep函数
#include "tjni.h" //java生成的对应C++头文件
using namespace std;
typedef void (*pExternalFunction)(unsigned int iParam);
pExternalFunction g_pExternalFunction = NULL; //实现函数指针,保存外部注册的函数
//定义全局对象,用于so通过JNI调用外部java函数
typedef struct _tagJNIAdapter {
jobject objInterface;
jobject objCallBack;
JavaVM* pVm;
_tagJNIAdapter() {
pVm = NULL;
}
} JNIADA;
JNIADA g_jniAda;
//外部向so注册一个函数供so内部调用
//extern "C" void RegisterExternalFunction(pExternalFunction pExFun) {
// g_pExternalFunction = pExFun; //把回调函数保存起来
//}
//线程启动后,定时调用外部注册的函数
void* start_routine(void *pParam)
{
if (NULL != g_jniAda.pVm) {
unsigned int iParam = (unsigned int)pParam;
JNIEnv *pEnv = NULL;
g_jniAda.pVm->AttachCurrentThread((void**) &pEnv, NULL); //与当前线程关联
jclass jcls = pEnv->GetObjectClass(g_jniAda.objInterface);
if (jcls == NULL) {
g_jniAda.pVm->DetachCurrentThread();
return (void*) 0;
}
jmethodID jmetFunc = pEnv->GetMethodID(jcls, "externalFunction", "(I)V"); //获取外java函数数
if (NULL == jmetFunc) {
g_jniAda.pVm->DetachCurrentThread();
return (void*)0;
}
for (int i = 0; i < 2; i++) {
pEnv->CallIntMethod(g_jniAda.objInterface, jmetFunc, iParam); //通过java 虚拟机调用java外部java函数
}
g_jniAda.pVm->DetachCurrentThread();
}
return (void*) 0;
}
//创建一个线程,通过线程定时调用外部注册的回调数
extern "C" bool Start(unsigned int iParam) {
pthread_t thread = 0;
pthread_attr_t attr;
void *status = NULL;
if (0 != pthread_create(&thread, NULL, start_routine, (void*) iParam)) {
cout << "pthread_create no" << endl;
return false;
}
int iResult = pthread_join(thread, &status); //等待线程结束返回
return true;
}
//实现java实现的头文件
void JNICALL Java_tjni_RegisterExternalFunction
(JNIEnv *pEnv, jobject obj1, jobject obj2) {
cout << "this is Java_tjni_RegisterExternalFunction" << endl;
//把java外部实现的方法实现保存起来,
pEnv->GetJavaVM(&g_jniAda.pVm);
g_jniAda.objCallBack = pEnv->NewGlobalRef(obj1);
g_jniAda.objInterface = pEnv->NewGlobalRef(obj2);
}
jboolean JNICALL Java_tjni_Start
(JNIEnv *, jobject, jint iParam) {
cout << "this is Java_tjni_Start" << endl;
return Start(iParam);
}
b)代码解释:
C++代码包含了头文件#include "tjni.h", 这个头文件由刚刚的javah tjni生成,而tjni.h 里又包含了 #include <jni.h> 头文件,这个头文件是由java安装时自还的,在java 的安装目录下,修改的时间需要链接上这个头文件。
c)关键代码说明:
jmethodID jmetFunc = pEnv->GetMethodID(jcls, "externalFunction", "(I)V");
这句C++代码通过虚拟机调用java外部的函函数,externalFunction是tjni.java文件里定义的public void externalFunction(int iParam)函数。(I)V表示这个函数的类型,可以通过javap来获取函数的定义信息,本教程中,externalFunction函数是定义在ExternalFunction在类中,所以执行 javap -s -p ExternalFunction 获取得到下面信息。
6、修改makefile文件
a) makefile修改如下
libjni_demo2.so:jni_demo2.o
g++ -shared -fpic -o libjni_demo2.so jni_demo2.o -pthread
jni_demo2.o:jni_demo2.cpp
g++ -c jni_demo2.cpp -I /usr/lib/jvm/java-7-openjdk-i386/include -I /usr/lib/jvm/java-7-openjdk-i386/include/linux
.PHONY:clean
clean:
rm -rf *.o
rm -rf *.so
代码解释: 原因翻译时需要加入jni.h头文件的支持,需要加入-I /usr/lib/jvm/java-7-openjdk-i386/include让make编译时可以找到jni.h文件。
7、编译新的so库
a) 执行 make命令重新编译so库,出现下面信息表示编译成功,生成新的so库libjni_demo2.so
8、执行java tjni测试java与so的调用。
a)这里如果执行 java tjni,将出现下图所示错误,原因是在 java.library.path目录下找不到libjni_demo2.so文件,在tjni.java中的代码System.loadLibrary("jni_demo2"); 将到java.library.path目录下加载libjni_demo2.so共享库文件。
b)解决办法是将libjni_demo2.so拷贝到java.library.path指向的目录,通过System.getProperties().list(System.out); 代码可以查看java.library.path指向的目录,我的测试环境指向 /usr/lib ,执行 cp libjni_demo2.so /usr/lib命令把so库复到目录。
c)执行java tjni输出如下图所示,表示java与so库互调成功
this is Java_tjni_RegisterExternalFunction
this is Java_tjni_Start
上面这两句是java调用C++,C++打印的信息
This is so invoke java function iParam=55
This is so invoke java function iParam=55
上面这两句是C++回调java,java打印的信息
结论:
通过上述过程,演示了java与C++ so的相互调用,Win32的调用方式也一样。本节的关键点是。
a) java方面
Java 要与C++ 交互的函数要用 native 声明,表明这个函数将与C++交互,将由C++实现native声明的函数,如本教程的public native void RegisterExternalFunction(ExternalFunction func);与public native boolean Start(int iParam);两个函数。
编译通过后,用javah来生成C++头文件,每一个native都会生成一个C++的函数对应,比如 public native boolean Start(int iParam) 和JNIEXPORT jboolean JNICALL Java_tjni_Start(JNIEnv *, jobject, jint)对应。
b)C++部分
C++通过实现native对应的函数实现java的调用功能,如本教程JNIEXPORT jboolean JNICALL Java_tjni_Start(JNIEnv *, jobject, jint)实现了java函数的调用。
c)C++的so和java如何关联。
通过java代码
static{
System.loadLibrary("jni_demo2");
}
在win32下,加载jni_demo2.dll 在Linux下加载libjni_demo2.so,注意:库文件必须放在任何java.library.path包含的路径中,可用 System.getProperties().list(System.out);打印出来
至于so调用java部分,参考本教程代码。
三、通过android的NDK编译,生成支持Android调用的so库
前述
一、虽然android java也是通过JNI调用so库,但与Linux的JNI还有些不同:
a) Linux使用makefile编译,android使用NDK编译,NDK是在makefile的基础上包装了一层,以适应android调用。
b) 代码有细微不同,在代码示例中碰到会讲
二、本节还会讲到怎样在C++代码中加入调试日志信息,在android开发中通过C++输出的代码定位日志。
c) 更多的NDK介绍参考:http://emuch.net/html/201207/4690329.html
准备工作
一、下载NDK
我下载的是win32版本
下载地址:http://dl.google.com/android/ndk/android-ndk-r9-windows-x86.zip
二、搭建并验证NDK环境是否正常。
a) 将下载的文件android-ndk-r9-windows-x86.zip解压到任何目录,本教程解压到E盘根目录。
b)按顺序执行:开始->运行->cmd,进入控制台窗口,进入E:\android-ndk-r9\samples\hello-jni目录,如下图所示:
c) 执行ndk-build命令,将编译NDK自带的demo,如下图所示表示编译环境正常。
使用NDK把C++代码编译成可支持android的so
一、为了其前面两步的区分,把jni_demo2.cpp改成jni_demo3.cpp,进入刚刚测试NDK的目录:E:\android-ndk-r9\samples\hello-jni。目录结果如下图所示。
把我们的C++源文件放jni目录下,并修改Android.mk文件,就可以编译了,原始jni目录如下图所示。Android.mk是makefile文件;hello-jni.c文件是原始的demo文件。
a)我们把自己的jni_demo3.cpp和tjni.h拷贝到这个目录,拷贝后的E:\android-ndk-r9\samples\hello-jni\jni目录如下图所示。
b)修改Android.mk文件。把原来的
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
改成
LOCAL_MODULE := jni_demo3
LOCAL_SRC_FILES := jni_demo3.cpp
修改后的内容如下
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
#LOCAL_MODULE := hello-jni
#LOCAL_SRC_FILES := hello-jni.c
LOCAL_MODULE := jni_demo3#编译后的so文件名
LOCAL_SRC_FILES := jni_demo3.cpp #C++源文件列表。
include $(BUILD_SHARED_LIBRARY)
c) 先执行ndk-build clean清楚工程,再执行ndk-build,编译时出现如下图所示错误,这个错误在Linux 的JNI下是没有出现的,这是NDK与makefile代码差别引起,接下来我们对这个错误进行修改。
二、在C++代码加入开关宏。
为解决上面翻译错误,我们在代码里加入一个开宏,用于区分是在普通makefile下编译还是在NDK下编译,通过本小节,知道怎么在NDK编译时传入开关宏。
a) 通过代码分析,定位出是g_jniAda.pVm->AttachCurrentThread((void**) &pEnv, NULL);编译不通过,在NDK这句代码应该改成g_jniAda.pVm->AttachCurrentThread((void**) &pEnv, NULL);。在代码里加入开关宏,在NDK编译时编译NDK的代码,修改后的代码如下。
#ifdef NDK
g_jniAda.pVm->AttachCurrentThread(&pEnv, NULL); // android平台
#else
g_jniAda.pVm->AttachCurrentThread((void**) &pEnv, NULL);
#endif
b) 并修改 Android.mk文件,加入LOCAL_CPPFLAGS += -D NDK,支持编译时加入开关宏。修改后的文件内容如下,注意:必须加在include $(CLEAR_VARS)后面,否则无效
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_CPPFLAGS += -D NDK
#LOCAL_MODULE := hello-jni
#LOCAL_SRC_FILES := hello-jni.c
LOCAL_MODULE := jni_demo3
LOCAL_SRC_FILES := jni_demo3.cpp
include $(BUILD_SHARED_LIBRARY)
c) 再执行ndk-build命令,编译通过
编译通过后,目标文件在E:\android-ndk-r9\samples\hello-jni\libs目录下,armeabi目录下的是arm平台的so
d) 再加入一个文件Application.mk到E:\android-ndk-r9\samples\hello-jni\jni,添加后目录结构如下图所示。
文件内部是:
APP_ABI := all
APP_STL := stlport_static
再重新编译,将生成更多硬件平台的so,生成后的目录如下图所示。
到些,已经把C++怎么写出一个支持andorid运行的so过程讲解完成。下面是一些C++开发andorid一些补充。
其它主题
加入日志输出:
1、包含头文件 #include <android/log.h>
2、打印日志函数 __android_log_print(ANDROID_LOG_DEBUG, "title", "content:%s Line:%d\n", __FILE__, __LINE__);
3、并在Android.mk加入 LOCAL_LDLIBS := -llog 注意:一定要在include $(CLEAR_VARS)后面,否则无效。
当android开发调试的时间,调用这个函数将在androd调试输出信息
NDK对stl 的支持
在 Application.mk文件中加入 APP_STL := stlport_static,NDK编译半支持STL。加入后的文件内部如下
APP_ABI := all #编译所有硬件平台
APP_STL := stlport_static #支持STL
C++文件比较多的项目NDK配置写法。
在 Android.mk 文件的 LOCAL_SRC_FILES 加入类似下面内容,可实现多C++文件编译。
LOCAL_SRC_FILES := a.cpp a.cpp c.cpp d.cpp