概述
相信网上有很多类似的文章,大家看到的也比较多,但对于Eclipse上创建so,我觉得网上并没有一个全面的介绍搭建过程来避免开发和编译中出现的一些问题,比如:一些C/C++库include失败(Unresolved inclusion: <jni.h>)、或者引入成功了确提示类型错误(Type 'size_t' could not be resolved)等等错误。
虽然在Android Studio 2.2后开始对ndk开发进行了有好的支持,而且使用了跨平台的cmake编译工具替换掉了之前的ndk-build方式进行so的生成,显而易见使用as成为必然,但是由于目前公司的so开发主要都是在eclipse上进行构建的,下一步打算迁移到as上了使用更高效的cmake,但是本人当初在eclipse上构建so的时候遇到了种种问题想把它记录下来,留作纪念,现在大家项目可能大部分ndk的编码还是用eclipse完成,也许也会帮助到一些朋友
安装环境
1、eclipse版本:
Eclipse IDE for Java Developers(Luna 4.4.2)
本人使用的版本有点老,因为自从使用了as后就没有更新eclipse了,不过可以在eclipse官网下载到
下载地址 第一个就是2、ADT版本:
使用的 23.0.6至于版本大家网上自行搜索,很好找到3、NDK版本:
android-ndk-r10e ,自行网上下载,目前最新版本为13.1,此处依然使用老的版本
搭建步骤
1. 创建项目
-
首先在Preferences里面把NDK的根路径设置好
像创建普通Android项目一样创建一个叫NDKTest的项目
创建好后在项目上右键>Android Tools>Add Android Native Support 然后随便填写一个名词代表我们要生成的so名,此处我们随便写一个,后面会讲怎么修改。然后Finish,会在我们项目根目录生成一个jni的文件夹这个文件就是我们以后编写C/C++在目录。
2. 引入本地库文件
- 我们要与java交互需要用到jni头文件还有一些C的标准库比如:string.h、stdio.h、stdlib.h、time.h等等,还有android的、linux的、opengl的、系统相关的等等基本都包含在此目录下:
android-ndk-r10e\platforms\android-21\arch-arm\usr\include
我们来介绍一下ndk目录下的platforms,里面存放了android在Native层给我们提供的API引用库文件
此处有Android各系统版本所对应的引用库,我们这里选择了android-21,咱们再往里看里面是对应了各个CPU架构的库文件,所以这里我选择了arch-arm就行。一般如果我们不对特定的平台做单独特性开发的话不需要做其他选择,其实最终ndk-build编译的时候会根据Android.mk文件的配置来生成对应平台的so。
我的项目中还用到了stddef.h、stdarg.h、stdbool.h等库,,但是这些头文件并没有在platforms目录下,这里我在在ndk的根目录通过模糊搜索搜出了这个目录:
android-ndk-r10e/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/lib/gcc/aarch64-linux-android/4.9/include
好了,这里确定我的项目中需要引入两个目录的库文件,在Eclipse中怎样引用呢?
首先在jni目录下创建一个名为ndktest.c的源文件,代码如下:
#include <jni.h>
jstring Java_com_example_ndktest_NativeApp_version(JNIEnv *env, jobject thiz) {
return (*env)->NewStringUTF(env, "1.0.0");
}
此处我创建了一个jni方法,返回一个字符串,但是看图都是错误,我们需要关联我们需要使用的库文件才能正常使用。
先不用着急我们再在Android.mk文件中进行如下修改
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := ndktest
LOCAL_SRC_FILES := ndktest.c
include $(BUILD_SHARED_LIBRARY)
正常情况下我们项目右键Build Project就会为我们打好包并且会帮我们把引用库关联上,请看图:
目前一切没有问题,该关联的只多不少,编译工具自动为我们Includes所需引用,并且还在libs目录下为我们打好了so包,也不知道是什么原因这次创建示例项目如此顺利,但是起初自己搭建环境时并没有那么顺利,遇到了些问题掉了些坑,如果有朋友在此步骤上遇到问题无法继续,请点击此处查看我的另一片文章: Eclipse NDK无法引用类库问题解决
接着我们创建一个java类,我们之前在jni目录下创建了一个ndktest.c文件里面声明了一个方法,方法名是这样的Java_com_example_ndktest_NativeApp_version ,大家一看此名字是不是很有规则,它是C程序与java代码相互调用的基本定义,以Java打头,后面以下划线分割,跟上包名类名,还有我们在这个类里定义的方法名,这样我们就来看看我们的java代码部分:
package com.example.ndktest;
public class NativeApp {
static{
System.loadLibrary("ndktest");
}
public static native String version();
}
最后在activity里面调用version方法显示在一个文本上,试试吧。
SO的生成
说到生成SO就离不开两个文件,Android.mk、Applicaiton.mk,想要了解这两个文件的语法这里推荐几篇文章
1、Android.mk的制作
2、Android.mk实例和NDK实用技巧
3、Application.mk简介
4、编写Android.mk中的LOCAL_SRC_FILES的终极技巧
Android.mk
这几篇文章写得很好,说的也比较全面,但在这里我也把我在实际工作中用到的关于这两个文件的内容简单介绍下,我们来看Android.mk里面的代码:
LOCAL_PATH := $(call my-dir) #每个Android.mk文件必须以定义LOCAL_PATH为开始,my-dir返回包含Android.mk的目录路径
include $(CLEAR_VARS) #此处是编译之前清除之前编译的信息,防止冲突,是编译一组模块的第一行
LOCAL_LDLIBS := -llog #//打印logcat日志用的,此处打正式包的时候建议去掉,这样可以减少SO文件大小
LOCAL_MODULE := jni_mantou #生成的so文件名,编译器会自动添加前缀lib最后则生成libjni_mantou.so,如
# 果模块名被定为:lib_mantou.则生成lib_mantou.so. 不再加前缀。在java代码里加载so,只需要加载jni_mantou就行不加前缀和后缀
LOCAL_SRC_FILES := device.c codec.c #此处是此SO库需要编译的源文件,此处为两个,如后面还要追加则用空格分离就行
include $(BUILD_SHARED_LIBRARY) #此处是编译一组模块的结尾
#BUILD_SHARED_LIBRARY:编译为动态库
#BUILD_STATIC_LIBRARY:编译为静态库。
#BUILD_EXECUTABLE:编译为二进制可执行程序
在Android中只有动态库才能被打包到安装包中,静态库可以被引用用来生成动态库.对于静态链接库和动态链接库概念不清楚的朋友推荐一篇文章,可以去了解下:C++静态库与动态库
简单归纳一下:
动态链接库:
- 动态库把对一些库函数的链接载入推迟到程序运行的时期。
- 可以实现进程之间的资源共享。(因此动态库也称为共享库)
- 将一些程序升级变得简单。
- 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
静态链接库:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
以上代码生成一个名为libjni_mantou.so的动态库文件,并且文件会自动放到libs目录和obj/local目录,我们只使用libs目录生成的文件即可。
如果想要生成多个.so(动态库)文件、.s(静态库)文件或者二进制可执行程序,需要多组以include $(CLEAR_VARS)开始,并以include $(BUILD_SHARED_LIBRARY|BUILD_STATIC_LIBRARY|BUILD_EXECUTABLE)结束的语句。我们可以在Android.mk文件里面再编译一个二进制可执行程序只需在添加如下代码:
include $(CLEAR_VARS)
LOCAL_MODULE := daemon #可执行文件名
LOCAL_SRC_FILES := daemon.c #需要编译的源文件,多个以空格隔开
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog -lm -lz #设置需要链接的库文件
#大写的-L表示附加库路径,$(SYSROOT)表示Linux系统根目录,在NDK里面则代表/platforms/android-21/arch-arm
#这个目录意思就是说定位到/platforms/android-21/arch-arm/usr/lib 目录下去找liblog.so(android输出日志库)
#、libm.so(数学库math)、libz.so(压缩库)
include $(BUILD_EXECUTABLE)
以上代码生成一个名为daemon的文件,没有后缀名,依然会放到libs目和obj/local目录,我们只用libs下的文件即可,此文件在Android平台有一个好处,我们知道一般我们在C层用fork()函数生成一个进程它会复制父进程的进程信息和携带一个Dalvik或者ART虚拟机它可以直接与java代码通信,但是有些时候我们紧紧需要单独进程处理一些不需要直接java代码通信的任务,可以通过unistd.h库里面的execlp函数启动一个service或者activity间接来达到目的,或者通过icp通过其他进程来达到交互的作用,这时在Android端生成一个【二进制可执行文件】也就是我们刚才生成的daemon文件,它有的优点是暂用内存极少只有几百kb左右,普通调用fork函数普遍内存暂用在十多兆甚至在有些高端机器上达到二三十兆这样有些时候违背了我们创建一个进程的目的而显得大材小用,它的不携带虚拟机不赋值父进程信息所以很小巧,同样使用execlp函数启动一个二进制可执行程序,至于在Android上如何启动一个二进制可执行程序我有时间会另开一篇文章专门讲这一块。
Application.mk
APP_ABI := all
APP_PLATFORM := android-8
此文件里有两个重要的属性:
APP_ABI
代表我们所要编译的平台,比如arm64-v8a、armeabi、armeabi-v7a、mips、mips64、x86、x86_64,如果没有Application.mk这个文件依然能顺利编译,但是默认只会编译armeabi平台的so,此处all代表编译所有平台也就是刚才列出的7个或者直接申明需要编译的平台,APP_ABI := armeabi armeabi-v7a
多个用空格分开。
APP_PLATFORM
表示使用的ndk库函数版本号。一般和SDK的版本相对应,各个版本在NDK目录下的platforms文件夹中,次属性配置不当可能导致ndk版本问题,比如你的值是APP_PLATFORM := android-21 或者你就没有创建Application.mk文件编译器默认以最高版本android-21来运行而你的manifest文件里面的minSdkVersion又是8而导致以下警告:
Android NDK: WARNING: APP_PLATFORM android-21 is larger than android:minSdkVersion 8 in ./AndroidManifest.xml
解决方法:这只是一个警告而已,不处理的话程序也照样运行,但是我们最好同manifest文件里的minSdkVersion版本一致把Application.mk里面的版本改为android-8。
C++的JNI方法申明
上面我们用c语言申明了一个version方法返回一个字符串,现在我们用c++来实现一遍,看看有哪些不同,我们新建一个ndktest.cpp源文件:
#include <jni.h>
jstring Java_com_example_ndktest_NativeApp_version(JNIEnv *env, jobject thiz) {
return env->NewStringUTF("1.0.0");
}
我们看之前我们创建的ndktest.c文件,在C中没有引用,传递的env是个两级指针,用(*env)->
调用方法且方法中要传入env。而ndktest.cpp文件,C++中env为一级指针,用env->
调用方法,无需传入env;
然后我们在Android.mk文件里把编译源文件修改一下 LOCAL_SRC_FILES := ndktest.cpp
,然后我们在Activity里面调用NativeApp类的version方法,噢噢.... force close了,错误是:
java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.example.ndktest.NativeApp.version() (tried Java_com_example_ndktest_NativeApp_version and Java_com_example_ndktest_NativeApp_version__)
说是找不到我们在C++代码里实现的version方法,是什么原因呢?
C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern "C"进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名;exter "C"{jni代码}。
#include <jni.h>
extern "C"{
jstring Java_com_example_ndktest_NativeApp_version(JNIEnv *env, jobject thiz) {
return env->NewStringUTF("1.0.0");
}
}
好了,运行一切正常。下一篇文章会讲讲基本的jni语法、生命周期和内存回收方面的知识点,还有在eclipse上面进行native代码的断点调试和日志追踪问题查找,好了本篇文章到此结束!