Android 我的第一个 NDK 程序 (AndroidStudio)

----------------------搞了一天终于搞出来了,写了这个帖子希望能给需要的人------------------------------------

1.what is JNI?

 JNI全称是Java Native Interface(Java本地接口)单词首字母的缩写,本地接口就是指用C和C++开发的接口。由于JNI是JVM规范中的一部份,因此可以将我们写的JNI程序在任何实现了JNI规范的Java虚拟机中运行。同时,这个特性使我们可以复用以前用C/C++写的大量代码。

       开发JNI程序会受到系统环境的限制,因为用C/C++语言写出来的代码或模块,编译过程当中要依赖当前操作系统环境所提供的一些库函数,并和本地库链接在一起。而且编译后生成的二进制代码只能在本地操作系统环境下运行,因为不同的操作系统环境,有自己的本地库和CPU指令集,而且各个平台对标准C/C++的规范和标准库函数实现方式也有所区别。这就造成使用了JNI接口的JAVA程序,不再像以前那样自由的跨平台。如果要实现跨平台,就必须将本地代码在不同的操作系统平台下编译出相应的动态库。

2.what is the step of JNI?

JNI开发流程主要分为以下6步:

1、编写声明了native方法的Java类

2、将Java源代码编译成class字节码文件

3、用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni参数表示将class中用native声明的函数生成jni规则的函数)

4、用本地代码实现.h头文件中的函数

5、将本地代码编译成动态库(windows:*.dll,linux/unix:*.so,mac os x:*.jnilib

6、拷贝动态库至 java.library.path 本地库搜索目录下,并运行Java程序

3.what is NDK

NDK简介

       1.NDK是一系列工具的集合

       NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的。

NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so

NDK可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。

       2.NDK提供了一份稳定、功能有限的API头文件声明

       Google明确声明该API是稳定的,在后续所有版本中都稳定支持当前发布的API。从该版本的NDK中看出,这些API支持的功能非常有限,包含有:C标准库(libc)、标准数学库(libm)、压缩库(libz)、Log库(liblog)。

3.什么场景使用ndk

代码保护,使用大部分开源库都是用c/c++代码编写的,便于移至(c/c++)写的库便于其他嵌入式平台上再次使用。

4.交叉编译

在一个平台上生成另一个平台上可执行的代码。电脑x86 手机arm 

5.链接库

A:静态链接库 依赖资源编译到一个文件中 体积大 .a  整体性好

B:动态链接库 运行时去查找它所依赖的资源 体积小 .so 容易找不到资源

java官方JNI学习资料:

http://docs.oracle.com/javase/6/docs/technotes/guides/jni/index.html

慕课网有对应JNI的课程(Eclipse)

Can Android NDK include a windows dll to a an Android shared library?

No. A Windows DLL is in a different format to Android and Linux.

Android is not compatible with Windows out of the box.Also, Windows DLLs tend to be compiled for Intel x86 CPUs, while the majority of Android devices have ARM CPUs. While Windows for ARM exists (Windows Phone, Windows RT to name some), the environment that the DLL is supposed to live in doesn't exist on Android.So, get the source code and recompile it natively? That might work, but Windows DLLs sometimes have an optional DLLMain() entry point which Linux .so files have no direct equivalent. What about the rest of the code? Is it written in a portable manner? Has it stuck to using the ISO language standard throughout? To be truly portable, code has to be designed and written that way.

4.开始我的第一个NDK程序


实践版本:

AS:1.5

gradle: 2.5

build.gradle: 'com.android.tools.build:gradle:1.5.0'

gradle.properties :android.useDeprecatedNdk=true 不加这个会报错的哦


可以看到Android上层的ApplicationApplicationFramework都是使用Java编写,

底层包括系统和使用众多的LIiraries都是C/C++编写的。

  所以上层Java要调用底层的C/C++函数库必须通过JavaJNI来实现


实现步奏:

1.下载NDK 并配置环境变量。( 我用的是android-ndk-r10e)

2.编写Gradle配置生成的so文件相关信息.

3.创建新工程 新建一个类 编写带有native 的本地方法 编译一下(Make Project)

4.使用 javah -jni  [src]类的包地址  >>生成头文件。

5.需要将JNI的代码放在app/src/main/jni下包括刚生成的.h ||你需要实现的.c ||.cpp文件 

6.使用gradle构建so 存于app/build/intermediates/ndk/debug 下面

7.将so拷贝至app/src/main/jniLibs目录下(没有就自己建一个)调用System.loadLibrary("so的名字")加载so,以后就可以调用方法啦!

请看下面详细介绍:

1.下载NDK并配置环境变量:

http://jingyan.baidu.com/article/3ea51489e7a9bd52e61bbac7.html  这个就不多说了。

2.如何编写app的build.gradle来配置so相关信息?

为了让AS能够自动编译JNI代码,我们需要在App的build.gradle的defaultConfig内添加NDK选项,其中moduleName指定了模块的名称,而这个名称

就是so库打包后的文件名。默认AS会将所有平台的so库全部打包,而手机上一般只需要armeabi平台的so,为了防止无用的so占据过大的空间,我们可以对so进行过滤筛选。

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "c.example.com.jni"
        minSdkVersion 14
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        ndk {
            moduleName "aly"
            abiFilters "armeabi", "armeabi-v7a", "x86"  //输出指定三种abi体系结构下的so库。目前可有可无。
        }
默认的JNI代码存放目录为app/src/main/jni  你也可以通过如下方式自定义

    sourceSets {
        main {
            jni.srcDirs 'src/main/jni_src'
//            java.srcDirs = ['src/main/java', 'src/main/jni']
        }
AS的默认读取so的文件夹为 app/src/main/jniLibs当然你可以自定义 不过nkd/debug下的so总是能找到的大笑

    sourceSets {
        main {
            jniLibs.srcDir "src/main/jni_libs"
        }
    }

3.创建jni文件夹,新建类并定义native方法。

package c.example.com.jni;

/**
 * Created by Administrator on 2015/10/29.
 */
public class jnidemo {
    public jnidemo()
    {
        System.loadLibrary("JniTest");
    }
    static
    {
        System.loadLibrary("JniTest");
    }
    public static native String SayHello();
}
调用这个方法(先写好了在去搞他的实现别着急)

public class MainActivity extends AppCompatActivity {


private TextView id_text;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        id_text= (TextView) findViewById(R.id.id_text);
        jnidemo j=new jnidemo();
        id_text.setText(j.SayHello());
    }
配置app的build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "c.example.com.jni"
        minSdkVersion 14
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        ndk{
            moduleName "JniTest"
        }

    }
moduleName 是将要生成的so文件的名字 

最外面的gradle

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.2.3'
//        classpath ' com.android.tools.build:gradle-experimental:0.2.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
我用的这个版本 貌似可以不用实验版本

重点来啦

然后Makeproject得到其中中间文件,我们关注的是.class文件。编译OK以后生成的class文件在AS工程的如下目录:

app\build\intermediates\classes\debug
然后接下来的步骤就是根据生成的class文件,利用javah生成对应的 .h头文件。

点开AS的Terminal标签,默认进入到该项目的app文件夹下。我在windows平台下输入如下命令跳转到class中间文件生成路径:

xxxxx\app> cd build\intermediates\classes\debug

然后执行如下javah命令生成h文件。

xxxxx\debug> javah -jni c.example.com.jni.jnidemo
记住了格式必须是包名+类名哈

app\build\intermediates\classes\debug下看见生成的 .h头文件为:


在工程的main目录下新建一个名字为jni的目录,然后将刚才的 .h文件剪切过来。在jni目录下新建一个c文件,随意取名,我的叫jnitest.c

头文件内容:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class c_example_com_jni_jnidemo */

#ifndef _Included_c_example_com_jni_jnidemo
#define _Included_c_example_com_jni_jnidemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     c_example_com_jni_jnidemo
 * Method:    SayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_c_example_com_jni_jnidemo_SayHello
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

.c文件内容:

#include "c_example_com_jni_jnidemo.h"
#include <string.h>

/*
 * Class:     io_github_yanbober_ndkapplication_NdkJniUtils
 * Method:    getCLanguageString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_c_example_com_jni_jnidemo_SayHello
  (JNIEnv *env, jclass obj){
     return (*env)->NewStringUTF(env,"This just a test for Android Studio NDK JNI developer!");
  }

.cpp内容

#include "c_example_com_jni_jnidemo.h"
#include <string.h>

/*
 * Class:     io_github_yanbober_ndkapplication_NdkJniUtils
 * Method:    getCLanguageString
 * Signature: ()Ljava/lang/String;
 */

JNIEXPORT jstring JNICALL Java_c_example_com_jni_jnidemo_SayHello
  (JNIEnv *env, jclass obj){
//     return (*env)->NewStringUTF(env,"This just a test for Android Studio NDK JNI developer!");
return env->NewStringUTF("HelloWorld from JNI !");
  }

然后打开右边的gradler build 一下下 你会发现你的第一个NDK程序 跑起来了~~~

项目地址:http://download.csdn.net/detail/lvwenbo0107/9226683

6.JNI_OnLoad & NativeRegister

运行app时经常会有如下提示“No JNI_OnLoad found in /data/app-lib/io.github.yanbober.ndkapplication-2/libYanboberJniLibName.so 0xa6a4e120, skipping init”下面我总结一下JNI_OnLoad 。

JNI_OnLoad()函数主要的用途有两点:

  • 通知VM此C组件使用的JNI版本。如果你的.so文件没有提供JNI_OnLoad()函数,VM会默认该.so使用最老的JNI 1.1版本。而新版的JNI做了许多扩充,如果需要使用JNI的新版功能,例如JNI 1.4的java.nio.ByteBuffer, 就必须藉由JNI_OnLoad()函数来告知VM。
  • 因为VM执行到System.loadLibrary()函数时,会立即先调运JNI_OnLoad(),所以C组件的开发者可以由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization)。

既然有JNI_OnLoad(),那就有相呼应的函数,那就是JNI_OnUnload(),当VM释放JNI组件时会呼叫它,因此在该方法中进行善后清理,资源释放的动作最为合适。

再来看RegisterNatives函数

在上面第一部分时我们看见通过javah命令生成的io_github_yanbober_ndkapplication_NdkJniUtils.h里函数的名字好长,看着就蛋疼。你肯定也想过怎么这么长,而且当有时候项目需求原因导致类名变了的时候,函数名必须一个一个的改,更加蛋疼。我第一次接触时那时候自己经验不足,就遇上了这个蛋疼问题。泪奔啊!

既然这样那就有解决办法的,那就是RegisterNatives大招。接下来来看下这个大招:

App的Java程序寻找c本地方法的过程一般是依赖VM去寻找*.so里的本地函数,如果需要连续调运很多次,每次都要寻找一遍,会多花许多时间。因此为了解决这个问题我们可以自行将本地函数向VM进行登记,然后让VM自行调registerNativeMethods()函数。

VM自行调registerNativeMethods()函数的作用主要有两点:  

  • 更加有效率去找到C语言的函数  
  • 可以在执行期间进行抽换,因为自定义的JNINativeMethod类型的methods[]数组是一个名称-函数指针对照表,在程序执行时,可以多次调运registerNativeMethods()函数来更换本地函数指针,从而达到弹性抽换本地函数的效果。
具体代码参考: http://blog.csdn.net/yanbober/article/details/45310589




注意事项:

1,如果采用静态注册的方式请注意C文件中严格按照命名规则 Java_packageName_className_method()的方式命名

2,在Android项目中建立同上述命名规则中packageName中相同的包名,在此包名下建立同上述命名规则中className相同的类名

3,在className声明native方法

4,程序中加载so库 System.loadLibrary("data/data/xxx.xxx.xxx/lib/xx.so")或者 System.loadLibrary("xx"),例如:System.loadLibrary("data/data/com.dtBank.app.service/lib/libjnixcld.so");

推荐用:System.loadLibrary("xx")


猜你喜欢

转载自blog.csdn.net/lvwenbo0107/article/details/49495361