一天掌握Android JNI本地编程 快速入门

一、JNI(Java Native Interface)

 1、什么是JNI:

              JNI(Java Native Interface):java本地开发接口

JNI是一个协议,这个协议用来沟通java代码和外部的本地代码(c/c++)

外部的c/c++代码也可以调用java代码

 2、为什么使用JNI

效率上 C/C++是本地语言,比java更高效

代码移植,如果之前用C语言开发过模块,可以复用已经存在的c代码

java反编译比C语言容易,一般加密算法都是用C语言编写,不容易被反编译

3、Java基本数据类型与C语言基本数据类型的对应

 3、引用类型对应

4、堆内存和栈内存的概念

栈内存:系统自动分配和释放,

保存全局、静态、局部变量,

在站上分配内存叫静态分配,

大小一般是固定的

堆内存:程序员手动分配(malloc/new)和释放(free/java不用手动释放,由GC回收),

在堆上分配内存叫动态分配,

一般硬件内存有多大堆内存就有多大

二、交叉编译

 1、交叉编译的概念

交叉编译即在一个平台,编译出另一个平台能够执行的二进制代码

主流平台有: Windows、 Mac os、 Linux

主流处理器: x86、 arm、 mips

 2、交叉编译的原理

即在一个平台上,模拟其他平台的特性

编译的流程: 源代码-->编译-->链接-->可执行程序

 3、交叉编译的工具链

多个工具的集合,一个工具使用完后接着调用下一个工具

 4、常见的交叉编译工具

NDK(Native Development Kit): 开发JNI必备工具,就是模拟其他平台特性类编译代码的工具

CDT(C/C++ Development Tools): 是Eclipse开发C语言的一个插件,高亮显示C语言的语法

Cygwin: 一个Windows平台的Unix模拟器(可以参考之前博客Cygwin简介及使用

 5、NDK的目录结构(可以在Google官网下载NDK开发工具,需要FQ)

docs: 帮助文档

build/tools:linux的批处理文件

platforms:编译c代码需要使用的头文件和类库

prebuilt:预编译使用的二进制可执行文件

sample:jni的使用例子

source:ndk的源码

toolchains:工具链

ndk-build.cmd:编译打包c代码的一个指令,需要配置系统环境变量

三、JNI的第一个例子

好了,准备知识已经完毕,下面开始我们的一个JNI例子。

1、新建一个Android项目,在根目录下创建 jni文件夹,用于存放 C源码。

2、在java代码中,创建一个本地方法 getStringFromC 本地方法没有方法体。

  1. private native String getStringFromC();

    3、在jni中创建一个C文件,定义一个函数实现本地方法,函数名必须用使用 本地方法的全类名,点改为下划线。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <jni.h>
 4 //方法名必须为本地方法的全类名点改为下划线,穿入的两个参数必须这样写,
 5 //第一个参数为Java虚拟机的内存地址的二级指针,用于本地方法与java虚拟机在内存中交互
 6 //第二个参数为一个java对象,即是哪个对象调用了这个 c方法
 7 jstring Java_com_mwp_jnihelloworld_MainActivity_getStringFromC(JNIEnv* env,
 8                                                                jobject obj){
 9     //定义一个C语言字符串
10     char* cstr = "hello form c";
11     //返回值是java字符串,所以要将C语言的字符串转换成java的字符串
12     //在jni.h 中定义了字符串转换函数的函数指针
13     //jstring   (*NewStringUTF)(JNIEnv*, const char*);
14     //第一种方法:很少用
15     jstring jstr1 = (*(*env)).NewStringUTF(env, cstr);
16     //第二种方法,推荐
17     jstring jstr2 = (*env) -> NewStringUTF(env, cstr);
18     return jstr2;
19 }

4、在jni中创建 Android.mk文件,用于配置 本地方法

  1.  LOCAL_PATH := $(call my-dir)
        include $(CLEAR_VARS)
        #编译生成的文件的类库叫什么名字
        LOCAL_MODULE    := hello
        #要编译的c文件
        LOCAL_SRC_FILES := Hello.c
        include $(BUILD_SHARED_LIBRARY)

    5、在jni目录下执行 ndk-build.cmd指令,编译c文件

6、在java代码中加载编译后生成的so类库,调用本地方法,将项目部署到虚拟机上之后就会发现toast弹出的C代码定义的字符串,第一个例子执行成功了。

static{
        //加载打包完毕的 so类库
        System.loadLibrary("hello");
    }

7、jni打包的C语言类库默认仅支持 arm架构,需要在jni目录下创建 Android.mk 文件添加如下代码可以支持x86架构

  1. APP_ABI := armeabi armeabi-v7a x86

    四、JNI常见错误

1、findLibrary returned null:

CPU平台不匹配 或者 在加载类库时,类库名字写错了

2、本地方法找不到:

忘记加载类库了 或者 C代码中方法名写错了

五、javah工具与javap工具

 1、javah:  生成本地方法头文件

需要在C/C++模块下才能生效

在JDK1.7中,在src目录下执行javah 全类名

在JDK1.6中,在bin/classes目录下执行

2、javap:  打印方法签名

在C语言中调用java的方法需要用到反射,C语言的反射需要一个方法签名,使用javap能够生成方法签名,很熟练的话也可以自己写方法签名

在bin/classes目录下执行 javap -s 全类名

六、使用本地方法加密字符串的一个小例子

C语言字符串与Java中的字符串类型不同,所以需要进行字符串类型转换。

一个重要的思想:C语言计算字符串的长度不方便,但是java很方便,只需要调用一个length()方法就可以,所以像这种需求,那个语言有优势就用哪个语言算,算完当做参数传递给另一种语言就ok。

混合语言编程这应该是一种非常有用的思想。

Java非常容易被反编译,所以加密都是用 c语言写的

#include <jni.h>
#include <string.h>
//将java字符串转换为c语言字符串(工具方法)
char*   Jstring2CStr(JNIEnv*   env,   jstring   jstr)
{
     char*   rtn   =   NULL;
     jclass   clsstring   =   (*env)->FindClass(env,"java/lang/String");
     jstring   strencode   =   (*env)->NewStringUTF(env,"GB2312");
     jmethodID   mid   =   (*env)->GetMethodID(env,clsstring,   "getBytes",   "(Ljava/lang/String;)[B");
     jbyteArray   barr=   (jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode); // String .getByte("GB2312");
     jsize   alen   =   (*env)->GetArrayLength(env,barr);
     jbyte*   ba   =   (*env)->GetByteArrayElements(env,barr,JNI_FALSE);
     if(alen   >   0)
     {
      rtn   =   (char*)malloc(alen+1);         //"\0"
      memcpy(rtn,ba,alen);
      rtn[alen]=0;
     }
     (*env)->ReleaseByteArrayElements(env,barr,ba,0);  //
     return rtn;
}
JNIEXPORT jstring JNICALL Java_com_mwp_encodeanddecode_MainActivity_encode
  (JNIEnv * env, jobject obj, jstring text, jint length){
      char* cstr = Jstring2CStr(env, text);
      int i;
      for(i = 0;i<length;i++){
          *(cstr+i) += 1; //加密算法,将字符串每个字符加1
      }
      return (*env)->NewStringUTF(env,cstr);
}
JNIEXPORT jstring JNICALL Java_com_mwp_encodeanddecode_MainActivity_decode
(JNIEnv * env, jobject obj, jstring text, jint length){
     char* cstr = Jstring2CStr(env, text);
     int i;
     for(i = 0;i<length;i++){
         *(cstr+i) -= 1;
     }
     return (*env)->NewStringUTF(env, cstr);
}

七、JNI操作一个数组(引用传递)

传递数组其实是传递一个堆内存的数组首地址的引用过去,所以实际操作的是同一块内存,

当调用完方法,不需要返回值,实际上参数内容已经改变,

Android中很多操作硬件的方法都是这种C语言的传引用的思路

 1 public class MainActivity extends Activity {
 2
 3     static{
 4         System.loadLibrary("encode");
 5     }
 6     int[] array = {1,2,3,4,5};
 7     @Override
 8     protected void onCreate(Bundle savedInstanceState) {
 9         super.onCreate(savedInstanceState);
10         setContentView(R.layout.activity_main);
11     }
12
13     public void click(View v){
14         encodeArray(array);
15         //不需要返回值,实际操作的是同一块内存,内容已经发生了改变
16         for (int i : array) {
17             System.out.println(i);
18         }
19     }
20
21     //传递数组其实是传递一个堆内存的数组首地址的引用过去,所以实际操作的是同一块内存,
22     //当调用完方法,不需要返回值,实际上参数内容已经改变,
23     //Android中很多操作硬件的方法都是这种C语言的传引用的思路,要非常熟练
24     private native void encodeArray(int[] arr);
25 }
 1 #include <jni.h>
 2 /*
 3  * Class:     com_mwp_jniarray_MainActivity
 4  * Method:    encodeArray
 5  * Signature: ([I)V
 6  */
 7 JNIEXPORT void JNICALL Java_com_mwp_jniarray_MainActivity_encodeArray
 8   (JNIEnv * env, jobject obj, jintArray arr){
 9      //拿到整型数组的长度以及第0个元素的地址
10      //jsize       (*GetArrayLength)(JNIEnv*, jarray);
11     int length = (*env)->GetArrayLength(env, arr);
12     // jint*       (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
13     int* arrp = (*env)->GetIntArrayElements(env, arr, 0);
14     int i;
15     for(i = 0;i<length;i++){
16         *(arrp + i) += 10; //将数组中的每个元素加10
17     }
18 }

八、偷用美图秀秀的C语言本地类库加深JNI的理解

项目中不需要有c代码,只需要有一个编译过后的类库供Java调用就可以了。

将美图秀秀的apk文件解压缩,将lib目录下C类库导入自己的项目,

反编译美图秀秀的apk文件,将其本地方法类 JNI.java复制到自己的项目

根据本地方法名和参数猜函数的作用及如何使用,

下例调用了美图的一个LOMO美化效果

 1 public class MainActivity extends Activity {
 2
 3     static{
 4         //加载美图秀秀的类库
 5         System.loadLibrary("mtimage-jni");
 6     }
 7     private ImageView iv;
 8     private Bitmap bitmap;
 9     @Override
10     protected void onCreate(Bundle savedInstanceState) {
11         super.onCreate(savedInstanceState);
12         setContentView(R.layout.activity_main);
13
14         iv = (ImageView) findViewById(R.id.iv);
15
16         bitmap = BitmapFactory.decodeFile("sdcard/aneiyi.jpg");
17         iv.setImageBitmap(bitmap);
18     }
19
20     public void click(View v){
21
22         int width = bitmap.getWidth();
23         int height = bitmap.getHeight();
24
25         //用于保存所有像素信息的数组
26         int[] pixels = new int[width*height];
27         //获取图片的像素颜色信息,保存至pixels
28         bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
29
30         JNI jni = new JNI();
31         //调用美图秀秀本地库中的美图方法,靠猜
32         //arg0:保存了所有像素颜色信息的数组
33         //arg1:图片的宽
34         //arg2:图片的高
35         //此方法是通过改变pixels的像素颜色值来实现美化效果,传递一个数组参数是不需要返回值的
36         jni.StyleLomoB(pixels, width, height);
37
38         Bitmap bmNew = Bitmap.createBitmap(pixels, width, height, bitmap.getConfig());
39         iv.setImageBitmap(bmNew);
40     }
41 }

九、在C语言中调用java方法(反射)

1、有时需要在C语言中调用java的方法,如刷新UI显示加载资源进度

在本地方法C语言代码中打印 Android的Logcat日志输出,Google已经帮我们封装好了方法,只需要调用一下就可以

如果要输出中文的话,必须将C语言的文件编码改成 utf-8,否则乱码

在C语言中调用java的方法需要用到反射,C语言的反射需要一个方法签名,使用javap能够生成方法签名,很熟练的话也可以自己写方法签名

在bin/classes目录下执行 javap -s 全类名

 1 public class MainActivity extends Activity {
 2     static{
 3         System.loadLibrary("hello");
 4     }
 5
 6     @Override
 7     protected void onCreate(Bundle savedInstanceState) {
 8         super.onCreate(savedInstanceState);
 9         setContentView(R.layout.activity_main);
10     }
11
12     public void click(View v){
13         cLog();
14     }
15
16     public native void cLog();
17
18     public void show(String message){
19         Builder builder = new Builder(this);
20         builder.setTitle("标题");
21         builder.setMessage(message);
22         builder.show();
23     }
24
25 }
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
JNIEXPORT void JNICALL Java_com_mwp_ccalljava2_MainActivity_cLog
  (JNIEnv * env, jobject obj){
    //打印log输出
    LOGD("我是C语言打印的debug日志");
    LOGI("我是C语言打印的info日志");
    //通过反射来调用java的方法,需要知道方法签名,使用javap得到方法签名
    //在bin/classes目录下执行 javap -s 全类名
    //1、得到类的字节码对象
    //jclass      (*FindClass)(JNIEnv*, const char*);
    jclass clazz = (*env)->FindClass(env, "com/mwp/ccalljava2/MainActivity");
    //jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    jmethodID methodID = (*env)->GetMethodID(env, clazz, "show", "(Ljava/lang/String;)V");
    //void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
    (*env)->CallVoidMethod(env,obj,methodID, (*env)->NewStringUTF(env, "这是弹窗的内容"));
}
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS += -llog
LOCAL_MODULE    := hello
LOCAL_SRC_FILES := log.c
include $(BUILD_SHARED_LIBRARY)

十、模拟监测压力传感器

传感器的原理是使用敏感电阻如(光敏电阻,热敏电阻)等监测电流电压的变化

Android程序只需要处理传感器传递的数据,并将其显示在界面上就可以。

下面模拟一个压力传感器来练习JNI编程

 1 public class MainActivity extends Activity {
 2     static{
 3         System.loadLibrary("monitor");
 4     }
 5     private MyProgressBar mpb;
 6     @Override
 7     protected void onCreate(Bundle savedInstanceState) {
 8         super.onCreate(savedInstanceState);
 9         setContentView(R.layout.activity_main);
10
11         mpb = (MyProgressBar) findViewById(R.id.mpb);
12         mpb.setMax(100);
13     }
14
15     public void start(View v){
16         new Thread(){
17             public void run() {
18                 startMonitor();
19             };
20         }.start();
21     }
22
23     public void stop(View v){
24         stopMonitor();
25     }
26
27     public native void startMonitor();
28     public native void stopMonitor();
29
30     //供本地方法调用刷新UI
31     public void show(int pressure){
32         mpb.setPressure(pressure);
33     }
34 }
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
//模拟压力传感其传递数据
int getPressure(){
    return rand()%101;
}
//用于控制循环的开关
int monitor;
JNIEXPORT void JNICALL Java_com_mwp_monitor_MainActivity_startMonitor
  (JNIEnv * env, jobject obj){
    monitor = 1;
    int pressure;
    jclass clazz;
    jmethodID methodid;
    while(monitor){
        //本地方法获取传感器数据
        pressure= getPressure();
        //使用反射调用java方法刷新界面显示
        //jclass      (*FindClass)(JNIEnv*, const char*);
        clazz= (*env)->FindClass(env, "com/mwp/monitor/MainActivity");
        //jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
        methodid= (*env)->GetMethodID(env, clazz, "show","(I)V");
        // void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
        (*env)->CallVoidMethod(env, obj, methodid, pressure);
        sleep(1);
    }
}
JNIEXPORT void JNICALL Java_com_mwp_monitor_MainActivity_stopMonitor
(JNIEnv * env, jobject obj){
    //结束循环
    monitor = 0;
}

十一、使用C++代码实现本地方法

1、把c文件后缀名换成cpp

2、Android.mk文件中的hello.c也要换成hello.cpp

3、c++的使用的环境变量结构体中,访问了c使用的结构体的函数指针,函数名全部都是一样的,只是参数去掉了结构体指针

4、访问函数指针时,把env前面的*号去掉,因为此时env已经是一级指针

5、clean,清除之前编译的残留文件

6、把声明函数的h文件放入jni文件夹中,include该h文

#include <jni.h>
#include "com_mwp_cplusplus_MainActivity.h"
JNIEXPORT jstring JNICALL Java_com_mwp_cplusplus_MainActivity_helloC
  (JNIEnv * env, jobject obj){
    char* cstr = "hello from c";
  //return (*env)->NewStringUTF(env, cstr);
    return env->NewStringUTF(cstr);
}

来自为知笔记(Wiz)

时间: 2024-11-19 06:41:16

一天掌握Android JNI本地编程 快速入门的相关文章

Android jni本地编程入门

在某些情况下,java编程已经不能满足我们的需要,比如一个复杂的算法处理,这时候就需要用到jni技术: jni : java native interface jni 其实就是java和c/cpp之间进行通信的一个接口规范,java可以调用c/cpp里面的函数,同样,c/cpp也可以调用java类的方法: jni开发工具ndk的安装:在最新的ndk版本中,安装ndk很简单,只需要装ndk的路径配置到系统环境变量中即可:在编译的时候,进入工程根目录:执行命令  ndk-build  即可完成编译:

超全Android JNI&NDK编程总结

由于网上关于JNI/NDK相关的知识点介绍的比较零散而且不具备参照性,所以写了这篇JNI/NDK学习笔记,便于作为随时查阅的工具类型的文章,本文主要的介绍了在平时项目中常用的命令.JNI数据类型.签名等,便于查阅相关资料.文末相关参考资料比较适合刚接触或者不熟悉Android NDK开发的朋友参阅. 常用命令 javac 编译java源文件生成.class文件 由于JNI对应的头文件由javah工具根据对应的.class文件生成,所以在进行JNI编程之前,写好Java代码后需要先编译,在使用ja

bash编程快速入门

首先,我们简单的介绍一下bash,bash是GNU计划编写的Unixshell,它是许多Linux平台上的内定shell,它提供了用户与系统的很好的交互,对于系统运维人员,bash的地位是举足轻重的,bash编程能很快处理日常的任务 bash入门,一个最简单的bash例子 #vim hello.sh #!/bin/bash #This is the first example of the bash #echo "Hello world" 下面,我们就这个简单的bash 脚本来介绍一下

Haskell 函数式编程快速入门【草】

什么是函数式编程 用常规编程语言中的函数指针.委托和Lambda表达式等概念来帮助理解(其实函数式编程就是Lambda演算延伸而来的编程范式). 函数式编程中函数可以被非常容易的定义和传递. Haskell 快速入门 概述 Haskell是一个按照纯函数式编程思想创造的语言,支持静态类型.类型推断.惰性处理(推迟计算).支持并发编程. 安装 从官方网站的下载页面 https://www.haskell.org/downloads 根据自己的操作系统选择. 第一次接触Haskell

bash shell编程快速入门教程

Shell 俗称壳(用来区别于核),是指"提供使用者使用界面"的命令解析器(软件).它类似于DOS下的command和后来的cmd.exe.它接收用户命令,然后调用相应的应用程序. 同时,Shell又是一种程序设计语言.作为命令语言,它交互式解释和执行用户输入的命令,或者自动地解释和执行预先设定好的一连串的命令.Shell不像C/C++等语言,它不需要编译就能执行.作为程序设计语言,Shell 定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支. UNIX系

Android jni/ndk编程二:jni数据类型转换(primitive,String,array)

一.数据类型映射概述 从我们开始jni编程起,就不可能避开函数的参数与返回值的问题.java语言的数据类型和c/c++有很多不同的地方,所以我们必须考虑当在java层调用c/c++函数时,怎么正确的把java的参数传给c/c++函数,怎么正确的从c/c++函数获取正确的函数返回值:反之,当我们在c/c++中使用java的方法或属性时,如何确保数据类型能正确的在java和c/c++之间转换. 回顾我们上一篇文章中的那个c函数: #include <stdio.h> #include <jn

Android JNI 本地开发接口

前言 JNI java native interface java 本地开发接口 JNI理解 JNI 简单的理解就是一个桥梁或者适配器,是C/C++语言和JAVA语言进行进行沟通的桥梁和中间件,相当于android开发中ListView和GridView的Adapter适配器,将数据和界面显示的view进行连接起来的桥梁.也可以理解为协议(接口),即C/C++与java语言相互沟通(传参,调用)的协议,在实际开发中也是通过本地开发接口(native interface)来实现相互沟通.

Android jni/ndk编程五:jni异常处理

在Java的编程中,我们经常会遇到各种的异常,也会处理各种的异常.处理异常在java中非常简单,我们通常会使用try-catch-finally来处理,也可以使用throw简单抛出一个异常.那么在jni编程的时候我们又是如何处理异常的呢? 异常处理流程 jni规范已经给我们做好了所有需要做的事情.回想一下处理异常的过程: 我们首先要在有可能产生异常的地方检测异常 处理异常 是的,我觉得异常的处理就是可以简单的总结为两步,在异常处理中我们通常会打印栈信息等.在jni编程中,异常处理的思路应该也与之

Android jni/ndk编程三:native访问java

一.访问静态字段 Java层的field和method,不管它是public,还是package.private和protected,从 JNI都可以访问到,Java面向语言的封装性不见了. 静态字段和非静态的字段访问方式不同,jni规范提供了一系列带static标示的访问静态字段的函数: jobject (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID); jboolean (*GetStaticBooleanField)(JNIEnv*, j