首页 > 代码库 > Android NDK开发(二)——从Hello World学起
Android NDK开发(二)——从Hello World学起
转载请注明出处:http://blog.csdn.net/allen315410/article/details/41805719
上篇文章讲述了Android NDK开发的一些基本概念,以及NDK的环境搭建,相信看过的朋友NDK开发环境搭建应该是没有问题了,还没有搭建或者不知道怎么搭建的朋友请点击这里。那么这篇文章,我们跟刚学Java编程语言一样,从世界知名程序“Hello World!”开始,开发出我们的第一个NDK程序。
NDK目录简单介绍
1,samples目录。这个目录包含了Google为NDK开发撰写的一些小例子,包括本地JNI开发,图片处理,多个库文件开发等等,这些例子虽小但面面俱到,能看懂samples目录下的小例子程序,那么对于NDK开发来说,就很好应付了。
2,docs目录。这个目录下存放的都是Google给开发者提供的文档,指导开发者怎样在Android环境下进行NDK开发,这个非常重要。
3,sources目录。由于Android是开源操作系统,作为Android的一部分的NDK,同样也是开源的,这个目录下存放的是NDK源码。
4,platforms目录。里面存放的是当前ndk版本所支持的所有android平台的版本,做NDK开发的C代码也是可以指定由某个特定版本平台下编译,该platforms目录下存放的是不同版本所包含的C的库文件和头文件,不同版本有些微小的变化。
5,prebuilt目录。这是提供给在Windows下开发ndk程序的一些工具集。
6,build目录。里面存放大量的Linux编程脚本和Windows下的批处理文件,用来完成ndk开发中的交叉编译。
具体开发
1,NDK开发步骤
首先,我先列出NDK开发的简单步骤,然后再以此为大纲,用一个Hello World的实例讲述一下NDK开发:
(1)创建一个android工程
(2)JAVA代码中写声明native 方法 public native String helloFromJNI();
(3)创建jni目录,编写c代码,方法名字要对应在c代码中导入jni.h头文件
(4)编写Android.mk文件
(5)Ndk编译生成动态库
(6)Java代码load 动态库.调用native代码
2,NDK开发具体实践
下面就按照上述的步骤建立一个HelloWorld小案例来一步一步实现NDK开发
1,创建一个Android工程,并且在Java代码中声明一个native方法:
public class MainActivity extends Activity { public native String javaFromJNI(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, javaFromJNI(), Toast.LENGTH_SHORT).show(); } }); } }
2,创建jni目录,编写c代码,方法名字要对应在c代码中导入jni.h头文件
#include<stdio.h> #include<jni.h> jstring Java_com_example_ndk_MainActivity_javaFromJNI(JNIEnv* env, jobject obj) { return (*(*env)).NewStringUTF(env, "hello jni!"); }关于这个本地的C代码怎么写,还是需要一些C语言的基础的。没有也可以,我们可以参考一下ndk解压目录下的platforms\android-19\arch-arm\usr\include目录下的jni.h文件,也就是本地C代码需要include的那个,用记事本打开看看里面的内容。先来说一下JNI代码的简单格式:
方法签名规则:返回值类型 Java_包名_类名_native方法名(JNIEnv* env, jobject obj)
返回值类型就是JNI头文件中事先定义好的自定义C类型,直接拿来使用即可:
其后的参数列表是固定的(JNIEnv* env, jobject obj)形式,关于JNIEnv请在下面的定义:
可以看到啊,这个JNIEnv原来是一个名作JNINativeInterface的结构体,这个结构体定义了很多的数据类型,那么我们返回字符串的类型或者方法是哪一个呢?
jstring (*NewStringUTF)(JNIEnv*, const char*);以上就是我在JNINativeInterface结构体找到的返回字符串的方法,参数为JNINativeInterface指针和一个字符串,正如上面JNI代码使用的那样调用即可。
好,以上我们创建好了JNI本地代码,我们编译一下试试吧!打开cygwin,切换到工程目录下,执行ndk-build命令:
仔细看一下报错的日志,告诉我们/jni目录下缺少了一个叫Android.mk的文件,所以导致无法编译。
3,编写Android.mk文件
这个Android.mk文件怎么写呢?这时候我们得打开NDK的文档来看看了,位置E:/NDK/android-ndk-r10d/docs/Start_Here.html,找到
好,我们就先在jni目录下创建一个Android.mk的文件,将上面的这段话复制粘贴进去,将LOCAL_MODULE和LOCAL_SRC_FILES修改成我们自己的名称:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := Hello LOCAL_SRC_FILES := Hello.c include $(BUILD_SHARED_LIBRARY)
4,ndk编译生成动态库
然后在cygwin中编译一下:可以看到编译通过了,下面刷新一下工程,就可以看到工程libs目录下多了个libHello.so的文件,这个就是Android认识的动态库了。
5,Java代码load 动态库.调用native代码
编译出来这个libHello.so文件后,就需要在Java代码中加载这个.so的库文件了,代码很简单,然后Toast一下看看效果:
public class MainActivity extends Activity { static { System.loadLibrary("Hello"); } public native String javaFromJNI(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, javaFromJNI(), Toast.LENGTH_SHORT).show(); } }); } }System.loadLibrary(String 文件名);是用来加载动态库的方法,其中参数类型是字符串,参数是Android.mk文件中LOCAL_MODULE定义的名称。
运行效果上图所示,到这里,一个简单的ndk开发的Hello World就完成了。友情提示:本示例程序不支持x86架构的cpu,测试请开启arm模拟器!
使用javah命令帮助生成方法签名
已知native代码中的方法签名规则是这样的:返回值类型 Java_包名_类名_native方法名(JNIEnv* env, jobject obj);但是有如以下特殊情况,Java的方法名中是可以带下划线“_”的,例如如下这样的定义native方法:
public native String java_From_JNI();假如我们按照上述的规则,在C代码中套用,定义出这样的C函数:
jstring Java_com_example_ndk_MainActivity_java_From_JNI(JNIEnv* env, jobject obj)这样定义的方法签名显然是不合适的,这样会造成编译环境误以为MainActivity类下有个java内部类,其中又包含From内部类,From内部类下有个叫JNI的方法,实际上并没有这个方法,所以编译的时候肯定是会报错的。那么这个例子是个个例而已,其实按照上述的方法签名规则来看,C语言中定义native方法比较麻烦,很容易让人手敲失误,导致程序运行不了,其实我们可以用JDK提供好的javah工具来自动为我们生成方法签名,步骤如下:
1,在windows命令模式中,切换到工程包下class字节码文件所在的目录下,本示例的路径是D:\workspace-mime\NDKHelloWorld\bin\classes
先执行“ cd /d D:\workspace-mime\NDKHelloWorld\bin\classes ”命令进入到class字节码文件的包名根目录下
然后执行“ javah com.example.ndk.MainActivity ”
会得到如下图的一个.h文件:
用记事本打开这个文件
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_example_ndk_MainActivity */ #ifndef _Included_com_example_ndk_MainActivity #define _Included_com_example_ndk_MainActivity #ifdef __cplusplus extern "C" { #endif /* * Class: com_example_ndk_MainActivity * Method: javaFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_javaFromJNI (JNIEnv *, jobject); /* * Class: com_example_ndk_MainActivity * Method: java_From_JNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_java_1From_1JNI (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif上面就是我们需要的方法签名了,这就是javah工具自动为我们生成的native头文件,下面我们需要引用这个头文件到工程中去。将这个头文件直接剪切,粘贴到工程的jni的目录下,然后重写一个Hello.c的C代码,将#include"com_example_ndk_MainActivity.h"放在代码的头部,表示引入刚刚生成好的头文件,注:在C语言中#include<xx.h>表示引用C语言环境(编译)自带的头文件,#include"xx.h"表示引用当前自定义的头文件。引用好头文件之后,将头文件中的两个方法签名拷贝进来,实现逻辑:
#include<stdio.h> #include<jni.h> #include"com_example_ndk_MainActivity.h" JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_javaFromJNI( JNIEnv* env, jobject obj) { return (*env)->NewStringUTF(env, "hello jni!"); } /* * Class: com_example_ndk_MainActivity * Method: java_From_JNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_java_1From_1JNI( JNIEnv* env, jobject obj) { return (*env)->NewStringUTF(env, "hello_jni__"); }重新编译:
重新编译之后,我们clean一下工程,然后refresh一下工程,在libs目录下就可以找到我们重新编译的新的libHello.so文件,最后在Java代码中实现操作(省略)。
Android.mk简介
一个Android.mk file用来向编译系统描述你的源代码。具体来说:该文件是GNU Makefile的一小部分,会被编译系统解析一次或多次。你可以在每一个Android.mk file中定义一个或多个模块,你也可以在几个模块中使用同一个源代码文件。编译系统为你处理许多细节问题。例如,你不需要在你的Android.mk中列出头文件和依赖文件。NDK编译系统将会为你自动处理这些问题。这也意味着,在升级NDK后,你应该得到新的toolchain/platform支持,而且不需要改变你的Android.mk文件。
#交叉编译器在编译C/C++代码所依赖的配置文件,linux下makefile的语法子集 #获取当前Android.mk的路径 LOCAL_PATH := $(call my-dir) #变量的初始化操作 特点:不会重新初始化LOCAL_PATH的变量 include $(CLEAR_VARS) #指定编译后生成的.so文件名,makefile语法约定文件名加前缀lib和后缀.so LOCAL_MODULE := Hello #指定native代码文件 LOCAL_SRC_FILES := Hello.c #指定native代码编译成动态库.so或者指定编译成静态库.a include $(BUILD_SHARED_LIBRARY)参数介绍:
LOCAL_MODULE: 就是你要生成的库的名字,这个名字要是唯一的.不能有空格.
编译后系统会自动在前面加上lib的头, 比如说我们的Hello 就编译成了libHello.so
还有个特点就是如果你起名叫libHello 编译后ndk就不会给你的module名字前加上lib了
但是你最后调用的时候 还是调用Hello这个库
LOCAL_SRC_FILES:这个是指定你要编译哪些文件
不需要指定头文件 ,引用哪些依赖, 因为编译器会自动找到这些依赖 自动编译
include $(BUILD_SHARED_LIBRARY) BUILD_STATIC_LIBRARY
.so 编译后生成的库的类型,如果是静态库.a 配置include $(BUILD_STATIC_LIBRARY)
LOCAL_CPP_EXTENSION := cc :指定c++文件的扩展名
LOCAL_MODULE := ndkfoo
LOCAL_SRC_FILES := ndkfoo.cc
LOCAL_LDLIBS += -llog -lvmsagent -lmpnet -lmpxml -lH264Android
指定需要加载一些别的什么库.
另:关于Android.mk文件的介绍和用法可以参考Google NDK提供的文档,位置是ndk解压目录下的docs目录下,Programmers_Guide/html/md_3__key__topics__building__chapter_1-section_8__android_8mk.html。
Android NDK开发(二)——从Hello World学起