C++开发支持Android共享库(so)教程

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 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值