JNI&NDK编程小结及建议
damk3990
9年前
<h2>前言</h2> <p>由于网上关于JNI/NDK相关的知识点介绍的比较零散而且不具备参照性,所以写了这篇JNI/NDK笔记,便于作为随时查阅的工具类型的文章,本文主要的介绍了在平时项目中常用的命令、JNI数据类型、签名等,便于查阅相关资料。文末相关参考资料比较适合刚接触或者不熟悉Android NDK开发的朋友参阅。</p> <h2>常用命令</h2> <h3>javac 编译java源文件生成.class文件</h3> <p>由于JNI对应的头文件由javah工具根据对应的.class文件生成,所以在进行JNI编程之前,写好Java代码后需要先编译,在使用javah生成对应的头文件</p> <h3>javah -jni自动生成头文件</h3> <p>举例说明:</p> <ul> <li> <p>生成普通的JNI头文件</p> <pre> <code class="language-java">javah -classpath path -jni -d outputdirpath com.mrljdx.JavaNativeCode </code></pre> </li> <li> <p>在Java函数中包含Android相关的参数代码,则需要在classpath中添加android.jar包的绝对路径地址</p> <pre> <code class="language-java">javah -classpath path:$ANDROID_HOME/path/android.jar -jni -d outputdirpath com.mrljdx.JavaNativeCodeWithAndroid </code></pre> </li> </ul> <h3>javap -s -p 查看函数签名</h3> <p>-s: 显示签名(只显示public类型的签名) -p:显示所有函数、成员变量的签名</p> <p>举例说明:</p> <pre> <code class="language-java">javap -classpath pacakage_path_dir -s -p com.mrljdx.JavaCode </code></pre> <h2>JNI数据类型和类型签名</h2> <h3>数据类型</h3> <p>JNI的数据类型包括: <strong>基本类型</strong> 和 <strong>引用类型</strong> 。这一点和Java的语言特性一致,基本类型包括jboolean、jchar、jint、jlong、jbyte、jshort、jfloat、jdouble、void,与Java类型的对应关系如下:</p> <table> <thead> <tr> <th>JNI类型</th> <th>Java类型</th> <th>描述</th> </tr> </thead> <tbody> <tr> <td>jboolean</td> <td>boolean</td> <td>无符号8位整型</td> </tr> <tr> <td>jbyte</td> <td>byte</td> <td>有符号8位整型</td> </tr> <tr> <td>jchar</td> <td>char</td> <td>无符号16位整型</td> </tr> <tr> <td>jshort</td> <td>short</td> <td>有符号16位整型</td> </tr> <tr> <td>jint</td> <td>int</td> <td>32位整型</td> </tr> <tr> <td>jlong</td> <td>long</td> <td>64位整型</td> </tr> <tr> <td>jfloat</td> <td>float</td> <td>32位整型</td> </tr> <tr> <td>jdouble</td> <td>double</td> <td>64位整型</td> </tr> <tr> <td>void</td> <td>void</td> <td>无类型</td> </tr> </tbody> </table> <p>JNI中引用类型主要有类、对象和数组,这点也上符合Java的语法规范,对应的关系如下:</p> <table> <thead> <tr> <th>JNI 类型</th> <th>Java引用类型</th> <th>描述</th> </tr> </thead> <tbody> <tr> <td>jobject</td> <td>Object</td> <td>Object类型</td> </tr> <tr> <td>jclass</td> <td>Class</td> <td>Class类型</td> </tr> <tr> <td>jstring</td> <td>String</td> <td>String类型</td> </tr> <tr> <td>jobjectArray</td> <td>Object[]</td> <td>对象数组</td> </tr> <tr> <td>jbooleanArray</td> <td>boolean[]</td> <td>boolean数组</td> </tr> <tr> <td>jbyteArray</td> <td>byte[]</td> <td>byte数组</td> </tr> <tr> <td>jcharArray</td> <td>char[]</td> <td>char数组</td> </tr> <tr> <td>jshortArray</td> <td>short[]</td> <td>short数组</td> </tr> <tr> <td>jintArray</td> <td>int[]</td> <td>int数组</td> </tr> <tr> <td>jlongArray</td> <td>long[]</td> <td>long数组</td> </tr> <tr> <td>jfloatArray</td> <td>float[]</td> <td>float数组</td> </tr> <tr> <td>jdoubleArray</td> <td>double[]</td> <td>double数组</td> </tr> <tr> <td>jthrowable</td> <td>Throwable</td> <td>Throwable</td> </tr> </tbody> </table> <h3>JNI类型签名</h3> <p>JNI的类型签名标识了一个特定的Java类型,这个类型可以是类和方法,也可以是数据类型。</p> <ul> <li>类型签名<br> 类的签名采用”L+包名+类名+;”标识,包名中将 . 替换为 / 即可。<br> 比如String类的签名:<br> Ljava/lang/String;<br> 注意末尾的 ; 属于签名的一部分。<br> 再比如Android中Context类的签名:<br> Landroid/content/Context;</li> <li>基本数据类型签名<br> 基本数据类型的签名采用一系列大写字母来标识,如下:</li> </ul> <table> <thead> <tr> <th>Java类型</th> <th>签名</th> </tr> </thead> <tbody> <tr> <td>boolean</td> <td>Z</td> </tr> <tr> <td>byte</td> <td>B</td> </tr> <tr> <td>char</td> <td>C</td> </tr> <tr> <td>short</td> <td>S</td> </tr> <tr> <td>int</td> <td>I</td> </tr> <tr> <td>long</td> <td>J</td> </tr> <tr> <td>float</td> <td>F</td> </tr> <tr> <td>double</td> <td>D</td> </tr> <tr> <td>void</td> <td>V</td> </tr> </tbody> </table> <p>可以发现除了 long 基本数据类型的签名为 J 之外其他的都比较容易辨识,估计是由于之前的类类型的签名开头为 L+包名+类名+; 设计者为了区分所以签名为 J</p> <ul> <li>数组的类型签名<br> 数组的类型签名比起类类型和基本数据类型的要稍微复杂一点,不过还是很好理解的。对于数组来说,它的签名为 [+类型签名 ,举例说明:<br> String[] 数组类型对应的签名:<br> [Ljava/lang/String;<br> 可以发现,就是在String的类签名前加了个 [<br> 同理基本数据类型签名int[]的签名:<br> [I<br> 注意这里基本类型后面是不带分号的。<br> 那么多维数组呢?可以类推,int[][] 的签名为 [[I ,而String[][]的签名为 [[Ljava/lang/String;</li> <li>方法的签名<br> 在JNI中会经常需要在C/C++代码中调用Java的函数,这时候就会用到方法的签名。方法的签名为 (+参数类型签名+)+返回值类型签名 ,比如:<br> 方法: boolean login(String username,String password) 的方法签名如下:<br> (Ljava/lang/String;Ljava/lang/String)B 如果这里不理解的话,请再去看看之前关于基本类型,类类型的签名部分内容。</li> </ul> <p>小技巧:使用 类似于 javap -classpath pathdir -s -p com.sample.JavaCode 的 javap -s -p 命令也可以帮助查看一些类中各种方法和成员变量的签名。</p> <h2>JNI相关命名解释</h2> <ul> <li>函数名的格式遵循规则: Java_包名_类名_方法名</li> <li>JNIEXPORT、JNICALL、JNIEnv和jobject 都是JNI标准中所定义的类型或者宏</li> <li>JNIEnv * : 指向JNI环境的指针,可以通过JNIEnv * 访问JNI提供的接口方法</li> <li>JNIEXPORT、JNICALL:是jni.h中所定义的宏。</li> </ul> <p>注:JNIEnv * 可以简单的理解为Java和C/C++ 之间相互调用的桥梁,我们可以通过JNIEnv * 调用C/C++定义的方法,也可以在C/C++中通过JNIEnv * 来调用Java类中的方法。下面将会讲到C/C++中调用Java的方法,注意JNIEnv *的作用。</p> <h2>在C/C++中调用Java方法</h2> <p>首先说明一点,在Android开发过程中使用NDK主要是为了提高代码的安全性,有些游戏公司可能是为了方便利用已有的C/C++开源库来进行平台移植,其实在性能提升方面,NDK的作用并不是很明显。所以有时候一些在Java中实现起来非常简单的代码放在JNI里面做会显得吃力不讨好,所以干脆就直接在JNI中调用Java的方法,我们只把加密和验证的一些逻辑写到JNI层就行了。在JNI中调用Java方法流程如下:</p> <ol> <li>在Java中定义一个 <strong>静态</strong> 方法供JNI调用,注意要是静态的。</li> <li>在JNI中利用env来调用Java中定义的静态方法</li> <li>调用声明好的静态方法</li> </ol> <p>可能流程说的比较抽象,用代码简单说明一下:</p> <ol> <li> <p>定义静态方法:</p> <pre> <code class="language-java">//对应包名:com.mrljdx.jni.HelloJNI public static void helloJava() { System.out.println("Hello JavaCode"); } </code></pre> </li> <li> <p>JNI声明静态方法:</p> <pre> <code class="language-java">static void static_helloJava(JNIEnv *env){ jclass clazz = env->FindClass("com/mrljdx/jni/HelloJNI"); } </code></pre> </li> <li> <p>调用声明好的静态方法:</p> <pre> <code class="language-java">static_helloJava(env); </code></pre> </li> </ol> <h2>在AndroidStudio中NDK编程配置注意事项:</h2> <ol> <li> <p>在项目的 gradle.properties 中添加ndk支持:</p> <pre> <code class="language-java">android.useDeprecatedNdk=true </code></pre> </li> <li> <p>配置 build.gradle 看代码注释:</p> <pre> <code class="language-java">defaultConfig { minSdkVersion 9 targetSdkVersion 23 versionCode 1 versionName "1.0" //配置ndk 支持 ndk { //编译的so库名称 libsecurity.so moduleName "security" //指定编译后的库支持的平台 abiFilters "armeabi", "mips", "x86", "armeabi-v7a" //用于指定应用应该使用哪个标准库,此处添加c++库支持 stl "stlport_static" } } </code></pre> </li> <li> <p>在AndroidStudio中写JNI代码有一个比较爽的地方,就是Android.mk系统会在编译时自动帮你生成,你只需要配置build.gradle就行了。注意jni相关代码需要放在 src/main/jni 目录下。如果对gradle配置不了解可以参考我的博客:Gradle实战及学习建议</p> </li> </ol> <h2>小结</h2> <p>在我们做产品的时候,应该考虑该用JNI&NDK的时候就用,一切出发点是基于用户的体验和数据安全,我觉得在以下几种情况下建议使用NDK:</p> <ol> <li>重用现有的代码,比如C/C++的代码在Android中的重用。</li> <li>数据安全,比如将Http的请求加密和解密算法放在NDK中去实现,这样可以提高应用的安全。</li> <li>提升性能,由于Android设备制造商在手机中给每个应用分配了可用的最大RAM,有时候为了性能考虑,可以通过Native代码向系统来“借”一些内存,尽量少的使用系统分配给应用的内存。(参考Infoq: <a href="/misc/goto?guid=4958986149242339462" rel="nofollow,noindex">Android内存优化</a> )</li> </ol> <p>来自: <a href="/misc/goto?guid=4959670892711548519" rel="nofollow">http://mrljdx.com/2016/04/16/JNI-NDK编程小结及建议/</a> </p>