前几天在参加腾讯模拟考的时候,腾讯出了一道关于JNI的题,具体如下:

JNI本身是一个非常复杂的知识,但是其实对于腾讯的这道题而言,如果你懂JNI,那么你可能会觉得这道题非常简单,就相当于C语言中的hello world级难度,但是事实上这道题一点都不简单,它涉及到JNI函数的调用的一些细节,很可能正是因为这些细节导致程序运行结果和你所预期的不一样,真的不得不说腾讯不愧为腾讯,出题不会出那种很难很难的但绝对不简单,这道题出的真的很有水平。下面就我自己翻看的一些参考书及网上的一些资料,综合自己的思考与理解,以腾讯的这道考题为例来详细讲解关于JNI的知识。

JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。JNI的作用就是使用java语言与本地宿主语言通信,那么我们为何需要了解JNI呢?这就要从安卓系统的整个体系框架说起。安卓系统的整个体系框架如下图所示:

可以看到Android上层的Application和ApplicationFramework都是使用Java编写,底层包括系统和使用众多的LIiraries都是C/C++编写的。所以上层Java要调用底层的C/C++函数库必须通过Java的JNI来实现。实际上我们不鼓励使用JNI,除非必须使用。

因为一旦使用JNI,JAVA程序就丧失了JAVA平台的两个优点:

1、程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。

2、程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了JAVA和C/C++之间的耦合性。

一如何使用第三方为我们定义好的JNI接口:

可能对于绝大多数APP而言,自己实现JNI接口的情况比较少,除非是像BAT级别对产品性能要求非常高的企业,对于绝大多数企业开发人员来讲自己通常要完成某些高性能模块开发时只需要调用第三方为我们写好的SDK即可,如本人的仿腾讯QQ的即时通讯软件中就用到了环信的SDK,而在这个过程中就用到了.so动态库文件。即JNI本地接口文件。所以我们通常只需要掌握如何使用so文件即可。

假如已有一个JNI实现——libxxx.so文件,那么如何在APK中使用它呢?

在我最初写类似程序的时候,我会将libxxx.so文件push到/system/lib/目录下,然后在Java代码中执行System.loadLibrary(xxx),这是个可行的做法,但需要取得/system/lib 目录 的写权限(模拟器通过adb remount取得该权限)。但模拟器 重启之 后libxxx.so文件会消失。现在 我找到了更好的方法,把.so文件打包到apk中分发给最终用户,不管是模拟器
或者 真机 ,都不再需要system分区的写权限。实现步骤如下:



1、在你的项目根目录下建立libs/armeabi目录;



2、将libxxx.so文件copy到 libs/armeabi/下;



3、此时ADT插件自动编译输出的.apk文件中已经包括.so文件了;



4、安装APK文件,即可直接使用JNI中的方法;



我想还需要简单说明一下libxxx.so的命名规则,沿袭Linux传统,lib<something>.so是类库文件名称的格式,但在Java的System.loadLibrary(" something ")方法中指定库名称时,不能包括 前缀—— lib,以及后缀——.so。

二如何自己实现JNI接口

以hello world为例,具体步骤如下:

1首先创建含有native方法的Java类:

  1. package com.lucyfyr;
  2. import android.app.Activity;
  3. import android.os.Bundle;
  4. import android.util.Log;
  5.  
  6. public class HelloWorld extends Activity {
  7. /** Called when the activity is first created. */
  8. @Override
  9. public void onCreate(Bundle savedInstanceState) {
  10.   super.onCreate(savedInstanceState);
  11.   setContentView(R.layout.main);
  12.   Log.v("dufresne", printJNI("I am HelloWorld Activity"));
  13. }
  14.   static
  15.   {
  16.     //加载库文件
  17.     System.loadLibrary("HelloWorldJni");
  18.   }
  19.   //声明原生函数 参数为String类型 返回类型为String
  20.   private native String printJNI(String inputStr);
  21. }



2通过javah命令生成生成共享库的头文件:

 进入到eclipse生成的Android Project中 :/HelloWorld/bin/classes/com/lucyfyr/ 下,可以看到里面后很多后缀为.class的文件,就是eclipse为我们自动编译好了的java文件,其中就有HelloWorld.class文件。退回到classes一级目录:/HelloWorld/bin/classes/。执行如下命令:

javah com.lucyfyr.HelloWorld 生成文件:com_lucyfyr_HelloWorld.h。

  1. /* DO NOT EDIT THIS FILE - it is machine generated */
  2. #include <jni.h>
  3. /* Header for class com_lucyfyr_HelloWorld */
  4. #ifndef _Included_com_lucyfyr_HelloWorld
  5. #define _Included_com_lucyfyr_HelloWorld
  6. #ifdef __cplusplus
  7. extern "C" {
  8. #endif
  9. /*
  10. * Class: com_lucyfyr_HelloWorld
  11. * Method: printJNI
  12. * Signature: (Ljava/lang/String;)Ljava/lang/String;
  13. */
  14. JNIEXPORT jstring JNICALL Java_com_lucyfyr_HelloWorld_printJNI
  15. (JNIEnv *, jobject, jstring);
  16. #ifdef __cplusplus
  17. }
  18. #endif
  19. #endif

可以看到自动生成对应的函数:Java_com_lucyfyr_HelloWorld_printJNI

Java_ + 包名(com.lucyfyr) + 类名(HelloWorld) + 接口名(printJNI):必须要按此JNI规范来操作;java虚拟机就可以在com.simon.HelloWorld类调用printJNI接口的时候自动找到这个C实现的Native函数调用。这是一个标准的C语言头文件,其中的JNIEXPORT、JNICALL是JNI关键字(事实上它是没有任何内容的宏,仅用于指示性说明),而jint、jstring是JNI环境下对int及java.lang.String类型的映射。这些关键字的定义都可以在jni.h中看到。

3实现JNI原生函数源文件,即在本地.c文件中实现上述java类中定义的native函数

这一步是整个JNI实现中最重要的步骤,本质上就是将java中传入的参数运用JNI函数先转换为C/C++能处理的数据结构,如字符串一般使用const char *GetStringUTFChars(JNIEnv *env, jstring string,jboolean * isCopy)函数将java格式转换为char
*格式,这样就可以调用本地C/C++函数来处理该字符串,处理完之后再调用JNI函数将C/C++中的数据结构转换为java数据类型,然后返回给调用该函数的java对象,如返回字符串一般使用jstring   NewStringUTF(JNIEnv *env, const char *bytes);将char *类型字符串转换为java中的String类型。

总之,该模块是核心,主要任务就是将java层数据结构转换为本地语言能处理的格式,然后调用本地API处理,然后将处理后的结果转换为java层能识别的数据类型,然后返回该结果。

  1. #include <jni.h>
  2. #define LOG_TAG "HelloWorld"
  3. #include <utils/Log.h>
  4. /* Native interface, it will be call in java code */
  5. JNIEXPORT jstring JNICALL Java_com_lucyfyr_HelloWorld_printJNI(JNIEnv *env, jobject obj,jstring inputStr)
  6. {
  7.   LOGI("dufresne Hello World From libhelloworld.so!");
  8.   // 从 instring 字符串取得指向字符串 UTF 编码的指针
  9.   const char *str =
  10.   (const char *)(*env)->GetStringUTFChars( env,inputStr, JNI_FALSE );
  11.   LOGI("dufresne--->%s",(const char *)str);
  12.   // 通知虚拟机本地代码不再需要通过 str 访问 Java 字符串。
  13.   (*env)->ReleaseStringUTFChars(env, inputStr, (const char *)str );
  14.   return (*env)->NewStringUTF(env, "Hello World! I am Native interface");
  15. }
  16.  
  17. /* This function will be call when the library first be load.
  18. * You can do some init in the libray. return which version jni it support.
  19. */
  20. jint JNI_OnLoad(JavaVM* vm, void* reserved)
  21. {
  22.   void *venv;
  23.   LOGI("dufresne----->JNI_OnLoad!");
  24.   if ((*vm)->GetEnv(vm, (void**)&venv, JNI_VERSION_1_4) != JNI_OK) {
  25.     LOGE("dufresne--->ERROR: GetEnv failed");
  26.     return -1;
  27.   }
  28.   return JNI_VERSION_1_4;
  29. }

OnLoadJava_com_lucyfyr_HelloWorld_printJNI函数里面做一些log输出 注意JNI中的log输出的不同。

JNI_OnLoad函数JNI规范定义的,当共享库第一次被加载的时候会被回调,这个函数里面可以进行一些初始化工作,比如注册函数映射表,缓存一些变量等,最后返回当前环境所支持的JNI环境。本例只是简单的返回当前JNI环境,重点了解一下JNI组件的入口函数——JNI_OnLoad()、JNI_OnUnload()

JNI组件的入口函数——JNI_OnLoad()、JNI_OnUnload()

JNI组件被成功加载和卸载时,会进行函数回调,当VM执行到System.loadLibrary(xxx)函数时,首先会去执行JNI组件中的JNI_OnLoad()函数,而当VM释放该组件时会呼叫JNI_OnUnload()函数。先看示例代码:

//onLoad方法,在System.loadLibrary()执行时被调用

jint JNI_OnLoad(JavaVM* vm, void* reserved){

LOGI("JNI_OnLoad startup~~!");

return JNI_VERSION_1_4;

}

//onUnLoad方法,在JNI组件被释放时调用

void JNI_OnUnload(JavaVM* vm, void* reserved){

LOGE("call JNI_OnUnload ~~!!");

}

JNI_OnLoad()有两个重要的作用:

指定JNI版本:告诉VM该组件使用那一个JNI版本(若未提供JNI_OnLoad()函数,VM会默认该使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI 1.4版,则必须由JNI_OnLoad()函数返回常量JNI_VERSION_1_4(该常量定义在jni.h中) 来告知VM。

初始化设定,当VM执行到System.loadLibrary()函数时,会立即先呼叫JNI_OnLoad()方法,因此在该方法中进行各种资源的初始化操作最为恰当。

JNI_OnUnload()的作用与JNI_OnLoad()对应,当VM释放JNI组件时会呼叫它,因此在该方法中进行善后清理,资源释放的动作最为合适。

JNI常用的函数,主要是关于处理字符串的:

const char *GetStringUTFChars(JNIEnv
*env, jstring string,jboolean * isCopy
将java格式转换为char
*格式,这样就可以调用本地C/C++函数来处理该字符串

参数说明:

JNIEnv
*env:JNI接口指针,一般JNI函数中都存在这个参数,就像Android中的Context

jstring
string:java类型的字符串对象

jboolean
* isCopy:布尔类型指针,该值如果被传入JNI_TRUE则表示该函数返回的字符串将会是string字符串的一份拷贝,如果传入JNI_FALSE则表示返回的结果是和原字符串指向的是JVM中的同一份数据,当该值为JNI_FALSE时,本地代码绝不能更改字符串的内容,否则JVM中的原始字符串也会被修改,这不符合java语言中字符串一旦产生就是固定的这一原则,该值通常传入JNI_FALSE。

void
   ReleaseStringUTFChars(JNIEnv *env, jstring string,const char *str):
该函数通知虚拟机本地代码已经不再需要访问str字符串,这个字符串即上述GetStringUTFChars函数返回的字符串,这两个函数一般成对出现,类似于C中的malloc与free,该函数的作用就是防止内存泄漏,因为java层存在GC机制,而JNI中会调用本地代码,所以得自己释放不用的内存,来防止内存泄漏,就像C++中的析构函数。

参数说明:

JNIEnv *env:JNI接口指针,一般JNI函数中都存在这个参数,就像Android中的Context

jstring string:java类型的字符串对象

const char * str指向GetStringUTFChars函数返回的字符串的指针

jstring
  NewStringUTF(JNIEnv *env, const char *bytes);
将本地char* 类型的字符串构造为java层的String对象

参数说明:

JNIEnv *env:JNI接口指针,一般JNI函数中都存在这个参数,就像Android中的Context

const char *bytes:指向char *类型的指针,即用来操作本地字符串的指针

返回值:
Java 字符串对象。如果无法构造该字符串,则为 NULL

4编译生成so库:编译com_lucyfyr_HelloWorld.c成so库可以和app一起编译,也可以都单独编译。在当前目录下建立jni文件夹:HelloWorld/jni/

下建立Android.mk ,并将com_lucyfyr_HelloWorld.c和 com_lucyfyr_HelloWorld.h 拷贝到进去

编写编译生成so库的Android.mk文件:

  1. LOCAL_PATH:= $(call my-dir)
  2. # 一个完整模块编译
  3. include $(CLEAR_VARS)
  4. LOCAL_SRC_FILES:=com_lucyfyr_HelloWorld.c
  5. LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
  6. LOCAL_MODULE := libHelloWorldJni
  7. LOCAL_SHARED_LIBRARIES := libutils
  8. LOCAL_PRELINK_MODULE := false
  9. LOCAL_MODULE_TAGS :=optional
  10. include $(BUILD_SHARED_LIBRARY)

系统变量解析:

  LOCAL_PATH - 编译时的目录

  $(call 目录,目录….) 目录引入操作符

    如该目录下有个文件夹名称 src,则可以这样写 $(call src),那么就会得到 src 目录的完整路径

  include $(CLEAR_VARS) -清除之前的一些系统变量

  LOCAL_MODULE - 编译生成的目标对象

  LOCAL_SRC_FILES - 编译的源文件

  LOCAL_C_INCLUDES - 需要包含的头文件目录

  LOCAL_SHARED_LIBRARIES - 链接时需要的外部库

  LOCAL_PRELINK_MODULE - 是否需要prelink处理 

  include$(BUILD_SHARED_LIBRARY) - 指明要编译成动态库

这就涉及到另一块知识,即android.mk编译模块添加,不懂得请自行百度。

编译此模块:输入编译命令

  ./makeMtk mm packages/apps/HelloWorld/jni/

  上面是我的工程根目录编译命令。具体编译方式根据自己系统要求执行。

编译输出: libHelloWorldJni.so (system/lib中视具体而定)

  此时库文件编译好了可以使用,如果在eclipse中模拟器上使用,需要将 libHelloWorldJni.so导入到system/lib 下或者对应app的data/data/com.lucyfyr/lib/下;

看一下HelloWorld中Android.mk文件的配置

其中存在:include $(LOCAL_PATH)/jni/Android.mk 表示编译库文件

LOCAL_JNI_SHARED_LIBRARIES := libHelloWorldJni 表示app依赖库,打包的时候会一起打包。



5运行程序: 将编译好的apk安装到手机上,使用adb
push到手机上去需要自己去导入库文件libHelloWorldJni.so到data/data/com.lucyfyr/lib/使用adb install方式安装则会自动导入。

启动HelloWorld :输入命令 adb logcat |grep dufresne

输出log如下:

 I/HelloWorld(28500): dufresne Hello World From libhelloworld.so!

 I/HelloWorld(28500): dufresne--->I am HelloWorld Activity

 V/dufresne(28500): Hello World! I am Native interface

符合调用打印顺序正确。

讲到这里,腾讯的那道考题是不是很简单,其实腾讯的那道考题就是谷歌官方写的JNI的sample。

Android中JNI编程详解的更多相关文章

  1. Android中Service(服务)详解

    http://blog.csdn.net/ryantang03/article/details/7770939 Android中Service(服务)详解 标签: serviceandroidappl ...

  2. Android中mesure过程详解

    我们在编写layout的xml文件时会碰到layout_width和layout_height两个属性,对于这两个属性我们有三种选择:赋值成具体的数值,match_parent或者wrap_conte ...

  3. Android中Intent组件详解

    Intent是不同组件之间相互通讯的纽带,封装了不同组件之间通讯的条件.Intent本身是定义为一个类别(Class),一个Intent对象表达一个目的(Goal)或期望(Expectation),叙 ...

  4. Android中JNI编程的那些事儿(1)

    转:Android中JNI编程的那些事儿(1)http://mobile.51cto.com/android-267538.htm Android系统不允许一个纯粹使用C/C++的程序出现,它要求必须 ...

  5. Android中的动画详解系列【4】——Activity之间切换动画

    前面介绍了Android中的逐帧动画和补间动画,并实现了简单的自定义动画,这一篇我们来看看如何将Android中的动画运用到实际开发中的一个场景--Activity之间跳转动画. 一.定义动画资源 如 ...

  6. Android中shape属性详解

    一.简单使用 刚开始,就先不讲一堆标签的意义及用法,先简单看看shape标签怎么用. 1.新建shape文件 首先在res/drawable文件夹下,新建一个文件,命名为:shape_radius.x ...

  7. RxJava在Android中使用场景详解

    RxJava 系列文章 <一,RxJava create操作符的用法和源码分析> <二,RxJava map操作符用法详解> <三,RxJava flatMap操作符用法 ...

  8. Android中的Intent详解

    前言: 每个应用程序都有若干个Activity组成,每一个Activity都是一个应用程序与用户进行交互的窗口,呈现不同的交互界面.因为每一个Acticity的任务不一样,所以经常互在各个Activi ...

  9. Android中的Service详解

    今天我们就来介绍一下Android中的四大组件中的服务Service,说到Service, 它分为本地服务和远程服务:区分这两种服务就是看客户端和服务端是否在同一个进程中,本地服务是在同一进程中的,远 ...

随机推荐

  1. day4 liaoxuefeng---函数

    一.调用函数: 调用abs函数:取绝对值函数, >>> abs(100) 100 >>> abs(-20) 20 >>> abs(12.34) 1 ...

  2. Python的IO编程

    原文传送门:请点击 原文传送门:请点击

  3. Kafka,Mq,Redis作为消息队列使用时的差异?

    redis 消息推送(基于分布式 pub/sub)多用于实时性较高的消息推送,并不保证可靠.其他的mq和kafka保证可靠但有一些延迟(非实时系统没有保证延迟).redis-pub/sub断电就清空, ...

  4. redis在java客户端的操作

    redis高性能,速度快,效率高的特点,用来做缓存服务器是很不错的选择.(和memcache相似)redis在客户端的操作步骤: 1.redis单机版操作 1.1通过Jedis对象操作 (1)将安装r ...

  5. 基于Windows服务器,从0开始搭建一个基于RTSP协议的直播平台

    作案工具下载 EasyDarwin 服务端程序,用来接受推流和拉流 FFmpeg 可以用来推流视频数据到服务端,也可以从服务端拉流下来播放,也可以从一个服务端拉流下来,转推到另一个服务端去. Easy ...

  6. PHP 5 Filesystem 函数

    PHP Filesystem 简介 Filesystem 函数允许您访问和操作文件系统. 安装 Filesystem 函数是 PHP 核心的组成部分.无需安装即可使用这些函数. Runtime 配置 ...

  7. PHP 实例 AJAX 与 MySQL

    AJAX 数据库实例 下面的实例将演示网页如何通过 AJAX 从数据库读取信息: 实例   Person info will be listed here... 实例解释 - MySQL 数据库 在上 ...

  8. webpack4.x配置详解,多页面,多入口,多出口,新特性新坑!!

    花了差不多一天多的时间,重新撸了一遍webpack4.x的常用配置. 基本上常用的配置都熟悉了一遍,总体上来讲,为了对parcel进行反击,webpack从4.x开始,正在朝着尽可能的简化配置文件的方 ...

  9. 前端技术之_CSS详解第二天

    前端技术之_CSS详解第二天 1.css基础选择器 html负责结构,css负责样式,js负责行为. css写在head标签里面,容器style标签. 先写选择器,然后写大括号,大括号里面是样式. & ...

  10. MacOS下对postgresql的简单管理操作

    如何安装在另一篇blog中有述,这里不再赘述.本篇简单说一下安装完postgresql之后的一些管理和查询操作. 首先安装完postgresql之后需要初始化数据库: initdb /usr/loca ...