首页 > 代码库 > 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目录简单介绍  

        在进行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方法:
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学起