c++封装so库,供安卓android调用


一、概述

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库

  1. 补充主题

二、用C/C++开发出Linux的so库

1.开发分析:

一个so需要提供允许外面调用的接口和so内部调用外部的调口,因些我们的so库需要向外部提供三个接口:1注册一个外部接口到so,so运行过程中调用;2…外部调用so功能的接口。现在我们把第一个接口定义成外部通过个接口注册一个接口个so内部,第二个接口设计通过外部调用,so内部开始调用第一个接口注册外部函数。

逻辑分析:1.定义一个全局的函数指针,第一个接口注册函数把外部函数指针保到这个变量里;2.第二个启动函数开启一个线程,这个线程定时调用全局函数指针。

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 -fPIC jni_demo1.cpp

    .PHONY:clean

    clean:
    rm -rf *.o
    rm -rf *.so

2)将这两个文件放到Linux某个目录上,jni_demo1.cpp和makefile在同一目录,进入存放这两个文件的目录,执行make编译,输出如下信息,产生一个so共享库。

在这里插入图片描述

jni_demo1.so就是我们的目录文件so共享库文件

  1. 编写一个测试文件,测试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,如果电脑上还没有安装,则显示下面信息,让我们选择一个安装

![在这里插入图片描述](https://img-blog.csdnimg.cn/26ca77a81872439ea471e520f78f441b.png在这里插入图片描述

(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 -fPIC 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(&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 
  • 17
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值