Android NDK 开发(二) -- 从Hlello World学起【转】

 转载请注明出处:http://blog.csdn.net/allen315410/article/details/41805719 

        上篇文章讲述了Android NDK开发的一些基本概念,以及NDK的环境搭建,相信看过的朋友NDK开发环境搭建应该是没有问题了,还没有搭建或者不知道怎么搭建的朋友请点击这里。那么这篇文章,我们跟刚学Java编程语言一样,从世界知名程序“Hello World!”开始,开发出我们的第一个NDK程序。

 

NDK目录简单介绍  

        在进行NDK开发之前,我们有必须熟悉一下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方法:
[java] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends Activity {  
  2.   
  3.     public native String javaFromJNI();  
  4.   
  5.     @Override  
  6.     protected void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.activity_main);  
  9.         findViewById(R.id.button).setOnClickListener(new OnClickListener() {  
  10.   
  11.             @Override  
  12.             public void onClick(View v) {  
  13.                 Toast.makeText(MainActivity.this, javaFromJNI(),  
  14.                         Toast.LENGTH_SHORT).show();  
  15.             }  
  16.         });  
  17.     }  
  18.   
  19. }  
2,创建jni目录,编写c代码,方法名字要对应在c代码中导入jni.h头文件

 

[cpp] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. #include<stdio.h>  
  2. #include<jni.h>  
  3.   
  4. jstring Java_com_example_ndk_MainActivity_javaFromJNI(JNIEnv* env, jobject obj) {  
  5.     return (*(*env)).NewStringUTF(env, "hello jni!");  
  6. }  
         关于这个本地的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的结构体,这个结构体定义了很多的数据类型,那么我们返回字符串的类型或者方法是哪一个呢?

 

[cpp] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. 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修改成我们自己的名称:

 

[javascript] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. LOCAL_PATH := $(call my-dir)  
  2.   
  3. include $(CLEAR_VARS)  
  4.   
  5. LOCAL_MODULE    := Hello  
  6. LOCAL_SRC_FILES := Hello.c  
  7.   
  8. include $(BUILD_SHARED_LIBRARY)  
4,ndk编译生成动态库
然后在cygwin中编译一下:

可以看到编译通过了,下面刷新一下工程,就可以看到工程libs目录下多了个libHello.so的文件,这个就是Android认识的动态库了。

 

5,Java代码load 动态库.调用native代码

编译出来这个libHello.so文件后,就需要在Java代码中加载这个.so的库文件了,代码很简单,然后Toast一下看看效果:

 

[java] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends Activity {  
  2.   
  3.     static {  
  4.         System.loadLibrary("Hello");  
  5.     }  
  6.     public native String javaFromJNI();  
  7.   
  8.     @Override  
  9.     protected void onCreate(Bundle savedInstanceState) {  
  10.         super.onCreate(savedInstanceState);  
  11.         setContentView(R.layout.activity_main);  
  12.         findViewById(R.id.button).setOnClickListener(new OnClickListener() {  
  13.   
  14.             @Override  
  15.             public void onClick(View v) {  
  16.                 Toast.makeText(MainActivity.this, javaFromJNI(),  
  17.                         Toast.LENGTH_SHORT).show();  
  18.             }  
  19.         });  
  20.     }  
  21.   
  22. }  
System.loadLibrary(String 文件名);是用来加载动态库的方法,其中参数类型是字符串,参数是Android.mk文件中LOCAL_MODULE定义的名称。

 

 

运行效果上图所示,到这里,一个简单的ndk开发的Hello World就完成了。友情提示:本示例程序不支持x86架构的cpu,测试请开启arm模拟器!

使用javah命令帮助生成方法签名

      已知native代码中的方法签名规则是这样的:返回值类型 Java_包名_类名_native方法名(JNIEnv* env, jobject obj);但是有如以下特殊情况,Java的方法名中是可以带下划线“_”的,例如如下这样的定义native方法:

 

[java] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. public native String java_From_JNI();  
假如我们按照上述的规则,在C代码中套用,定义出这样的C函数:

 

 

[cpp] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. 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文件:

用记事本打开这个文件

 

[cpp] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. /* DO NOT EDIT THIS FILE - it is machine generated */  
  2. #include <jni.h>  
  3. /* Header for class com_example_ndk_MainActivity */  
  4.   
  5. #ifndef _Included_com_example_ndk_MainActivity  
  6. #define _Included_com_example_ndk_MainActivity  
  7. #ifdef __cplusplus  
  8. extern "C" {  
  9. #endif  
  10. /* 
  11.  * Class:     com_example_ndk_MainActivity 
  12.  * Method:    javaFromJNI 
  13.  * Signature: ()Ljava/lang/String; 
  14.  */  
  15. JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_javaFromJNI  
  16.   (JNIEnv *, jobject);  
  17.   
  18. /* 
  19.  * Class:     com_example_ndk_MainActivity 
  20.  * Method:    java_From_JNI 
  21.  * Signature: ()Ljava/lang/String; 
  22.  */  
  23. JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_java_1From_1JNI  
  24.   (JNIEnv *, jobject);  
  25.   
  26. #ifdef __cplusplus  
  27. }  
  28. #endif  
  29. #endif  
        上面就是我们需要的方法签名了,这就是javah工具自动为我们生成的native头文件,下面我们需要引用这个头文件到工程中去。将这个头文件直接剪切,粘贴到工程的jni的目录下,然后重写一个Hello.c的C代码,将#include"com_example_ndk_MainActivity.h"放在代码的头部,表示引入刚刚生成好的头文件,注:在C语言中#include<xx.h>表示引用C语言环境(编译)自带的头文件,#include"xx.h"表示引用当前自定义的头文件。引用好头文件之后,将头文件中的两个方法签名拷贝进来,实现逻辑:

 

 

[cpp] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. #include<stdio.h>  
  2. #include<jni.h>  
  3. #include"com_example_ndk_MainActivity.h"  
  4.   
  5. JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_javaFromJNI(  
  6.         JNIEnv* env, jobject obj) {  
  7.     return (*env)->NewStringUTF(env, "hello jni!");  
  8. }  
  9.   
  10. /* 
  11.  * Class:     com_example_ndk_MainActivity 
  12.  * Method:    java_From_JNI 
  13.  * Signature: ()Ljava/lang/String; 
  14.  */  
  15. JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_java_1From_1JNI(  
  16.         JNIEnv* env, jobject obj) {  
  17.     return (*env)->NewStringUTF(env, "hello_jni__");  
  18. }  
重新编译:

 

重新编译之后,我们clean一下工程,然后refresh一下工程,在libs目录下就可以找到我们重新编译的新的libHello.so文件,最后在Java代码中实现操作(省略)。

 

Android.mk简介

        一个Android.mk file用来向编译系统描述你的源代码。具体来说:该文件是GNU Makefile的一小部分,会被编译系统解析一次或多次。你可以在每一个Android.mk file中定义一个或多个模块,你也可以在几个模块中使用同一个源代码文件。编译系统为你处理许多细节问题。例如,你不需要在你的Android.mk中列出头文件和依赖文件。NDK编译系统将会为你自动处理这些问题。这也意味着,在升级NDK后,你应该得到新的toolchain/platform支持,而且不需要改变你的Android.mk文件。

 

[html] view plain copy
 
 print?在CODE上查看代码片派生到我的代码片
  1. #交叉编译器在编译C/C++代码所依赖的配置文件,linux下makefile的语法子集  
  2.       
  3.     #获取当前Android.mk的路径  
  4.     LOCAL_PATH := $(call my-dir)  
  5.     #变量的初始化操作 特点:不会重新初始化LOCAL_PATH的变量  
  6.     include $(CLEAR_VARS)  
  7.     #指定编译后生成的.so文件名,makefile语法约定文件名加前缀lib和后缀.so  
  8.     LOCAL_MODULE    := Hello  
  9.     #指定native代码文件  
  10.     LOCAL_SRC_FILES := Hello.c  
  11.     #指定native代码编译成动态库.so或者指定编译成静态库.a  
  12.     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。

posted @ 2016-11-16 17:24  请给我倒杯茶  阅读(378)  评论(0编辑  收藏  举报