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>