Android学习笔记16-JNI
来源:互联网 发布:恩施宏观经济数据分析 编辑:程序博客网 时间:2024/05/16 11:24
1、JNI
java native interfaceAPPLICATION FRAMEWORK层是不能访问C语言类库的,所以需要通过JNI访问.google已经写好了这些JNI。但是我们有时候我们的应用需要调用自己C类库,google是没有的。所以我们需要自己编写JNI调用。重要的代码是用c编写的,因为c反编译出来没有用,安全性更高。java虽然有混淆,但是反编译出来的还是能看懂,安全性不高。
2、交叉编译
在一个平台下编译出另一个平台可以执行的二进制程序CPU平台:arm,x86,mips系统平台:Windows、Linux、Mac OS原理:模拟另一个平台的特性去编译代码 源代码->预编译->编译->链接->可执行程序工具链:一个工具使用完毕自动使用下一个常见工具 NDK:native development kits(google官方提供的,和sdk一样) CDT:C/C++ developmer tools eclipse插件 高亮显示C关键字 cygwin:Windows平台下的Linux命令行模拟器
3、下载NDK
自己在网上就可以下载(android-ndk-r14b-windows-x86_64.zip)大概700M。docs:帮助文档build/tools:Linux批处理文件platforms:存放开发jni用到的h头文件和so动态链接库prebuilt:预编译使用的工具sample:使用jni的案例source:NDK的部分源码toolchains:工具链ndk-build.cmd:编译打包C代码的指令配置下环境变量,把ndk-build.cmd所在的目录添加到path,这样可以在任何地方运行
4、编写JNI案例:
在c里面没必要使用main函数了,不会单独运行。android MeidaPlayer也调用了JNI,可以参考写。也可以看NDK里面的案例写。native 定义的方法是没有方法体的,就和接口类似。只是本地方法实现(c语言)。步骤 a. 新建个android工程,定义并调用本地方法
com.example.testjni public class MainActivity extends Activity { static{ //加载动态链接库 so库 System.loadLibrary("hello"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void click(View v){//定义一个按钮,点击调用C函数 Toast.makeText(this, helloFromC(), 0).show(); } //定义一个本地方法,本地方法没有方法体,由本地语言实现 public native String helloFromC(); }
b. 创建jni文件夹。 c. jni文件夹里创建c文件,并实现本地方法
hello.c #include <stdio.h> #include <stdlib.h> #include <jni.h>//注意一定要包含这个头文件 //定义一个函数实现本地方法:helloFromC() //env:结构体二级指针,该结构体中封装了大量的函数指针,可以帮助程序员实现某些常用功能 //thiz:本地方法调用者的对象(MainActivity的对象) jstring Java_com_example_testjni_MainActivity_helloFromC(JNIEnv* env, jobject thiz){ // char cstr[] = "hello from c"; char* cstr = "hello from C";//开发中常用的写法 //把C字符串转换成java字符串 //函数的原型(可以在ndk的jni.h中找到):jstring (*NewStringUTF)(JNIEnv*, const char*); jstring jstr = (*env)->NewStringUTF(env, cstr);//(*env代表一级指针) return jstr; }
注意方法名称一定要是包名+类名+函数名 d. 创建Android.mk文件,指定要编译的c文件 Android.mk://里面的内容我们可以翻阅NDK的doc中的文档介绍 ANDROID_MK.HTML
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := helloLOCAL_SRC_FILES := hello.cinclude $(BUILD_SHARED_LIBRARY)
e. 在jni目录下,执行ndk-build.cmd,编译打包出so动态链接库 如果成功了,界面会提示so的路径。在libs/armeabi/libhello.so。 生成了so文件后,jni文件夹其实已经没有用了。 f. 在java代码中加载动态链接库 非常重要 static{ //加载动态链接库 so库 System.loadLibrary("hello"); } g. 部署,运行 如果有中文报错,修改下编码,再重新生成下so就可以。 成功OK。注意总结: a.我们在armcpu架构下编译的so库在其他架构的机器上是不能运行的,提示找不到类库. 解决方案:我们可以在jni文件夹下创建一个Application.mk文件 输入:APP_ABI := armeabi x86 重新编译,就可以自动生成其他架构的so库了,就可以正常运行了 b.记住一定要加载类库才行
5、javah命令的使用:
我们在写本地C代码的时候,函数名太长容易写错,java给我们提供了个javah命令(java自带的指令)可以帮助我们。自动生成jni样式的头文件,头文件中就包含了我们需要的函数名1.7:在src目录下使用:javah com.example.testjni.MainActivity1.6:在bin/classes目录下使用:运行命令后,在src目录下面会生成一个com_example_testjni_MainActivity.h文件,我们把里面的方法拷出来用就可以,避免自己写错JNIEXPORT jstring JNICALL Java_com_example_testjni_MainActivity_helloFromC(JNIEnv *, jobject);注意:参数名称需要我们自己写。JNIEXPORT JNICALL关键字可以有,可以没有。
6、添加本地支持:(更加方便,有代码提示功能)
自动生成jni文件夹自动生成c文件和Android.mk文件指定jni.h头文件的路径,相当于关联源码不需要再去jni目录下使用ndk-build.cmd指令,项目部署时,会先打包编译so类库再去部署到手机上a.在Window---Preferences---Android---NDK指定你的NDK的路径 可能你的eclipse没有NDK的选项,(我本地adt-bundle-windows-x86_64-20131030里面的eclipse中有) 但是自己下载的luna eclipse没有,发现把plugins里面的ndk jar包拷贝过来没用。 于是就在eclipse中更新了插件 Help---Install New Software...---输入 p2repo - http://dl.google.com/android/eclipse/ 选择Developer Tools --- Android Native Development Tools 点击下一步更新,重启eclipse就可以了,默认就给你安装了cdt 注意:在添加的时候,出现“Not a valid NDK directory”错误, 在ndk目录新建一个ndk-build的空文件就可以了b.右击工程---Android Tools---Add Native Support... 输入你的so库名称: hello(随便起)c.点击finish之后我们的工程就是一个jni的工程了。 里面会自动生成jni文件夹 把里面的hello.cpp 修改hello.c 把Android.mk里面的hello.cpp 修改为hello.cd.我们在src目录下使用javah命令,生成h文件。e.把文件中的方法拷贝过来发现报错了,找不到jni.h。 我们需要指定下jni.h所在的目录。 右击工程---C/C++ General ---Paths and Symbols---Add...---File system... D:\Install_Program\Android-SDK\android-ndk-r14b\platforms\android-19\arch-arm\usr\include 你是哪个平台就选择哪个 选择完成,发现不报错了。添加本地支持的好处就是,不用自己手动用命令编译so了,可以使用代码提示功能了。
7、JNI的数组传递
我们基本类型的数据是值传递,所以你在c中修改不会影响到原来的数据。java的数组是对象,传递对象是传递对象的地址,c函数中修改了地址上的值,所以数组的值就改变了。简单的案例演示:java的代码:
MainActivity.java: package com.example.transmitarray; import android.os.Bundle; import android.app.Activity; import android.view.Menu; import android.view.View; public class MainActivity extends Activity { static{//加载本地的so库 System.loadLibrary("transmit"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } int[] array = {1,2,3,4,5}; public void click(View v){ transmit(array);//将java的数组传递给本地的C代码 for(int i = 0; i < array.length; i++){ System.out.println(array[i]); } } //定义本地方法 public native void transmit(int[] array); }
C代码:
jni文件夹下 transmit.c #include <jni.h> JNIEXPORT void JNICALL Java_com_example_transmitarray_MainActivity_transmit (JNIEnv * env, jobject thiz, jintArray array){//通过javah可以复制 //获取数组的长度,我们可以直接调用jni定义好的方法 //jsize (*GetArrayLength)(JNIEnv*, jarray); jsize size = (*env)->GetArrayLength(env, array); //获取数组首地址 //jint* (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*); jint* arrp = (*env)->GetIntArrayElements(env, array, 0); int i; //把所有元素都+5 for(i = 0; i < size; i++){ *(arrp + i) += 5; } }
8、在C代码中打印出Log
我们还可以反编译其他应用的apk,获取他们的本地调用方法,在拿到他们的so库,我们就可以按照他们调用,在我们自己的应用中调用了。本地C实现算法很难被反编译出来,所以可以提高安全性。我们怎么在C中,将log信息输出到logcat的控制台呢?例如:java代码:
public class MainActivity extends Activity { static{ System.loadLibrary("call"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void click(View v){//点击按钮,log就会输出 ccallJava(); } public native void ccallJava(); }
C代码:
#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_example_ccalljava_MainActivity_ccallJava (JNIEnv * env, jobject thiz){ LOGD("调试等级log"); LOGI("info等级的log"); }
注意:还要在Android.mk文件中加LOCAL_LDLIBS += -llog
9、C代码调用java中的方法-使用反射机制:
我们还是用上面的代码演示:例如:java代码:
public class MainActivity extends Activity { static{ System.loadLibrary("call"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void click(View v){//点击按钮,log就会输出 ccallJava(); } public native void ccallJava(); public void showDialog(String message){//我们定义一个方法,在C中反射调用它 AlertDialog.Builder builder = new Builder(this); builder.setTitle("标题"); builder.setMessage(message); builder.show(); } }
C代码:
#include <jni.h> JNIEXPORT void JNICALL Java_com_example_ccalljava_MainActivity_ccallJava (JNIEnv * env, jobject thiz){ //加载字节码 //jclass (*FindClass)(JNIEnv*, const char*); //char* 这个字符串我们是传我们activity的完整路径 jclass clazz = (*env)->FindClass(env, "com/example/ccalljava/MainActivity"); //获取方法 // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); //第一个char*字符串是我们的方法名称 //第二个char*字符串是我们的方法签名 jmethodID methodId = (*env)->GetMethodID(env, clazz, "showDialog", "(Ljava/lang/String;)V"); //运行方法 //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...); (*env)->CallVoidMethod(env, thiz, methodId, (*env)->NewStringUTF(env, "成功调用到java的Dialog了")); }
注意2点: a.我们在获取方法的时候,需要传人方法的签名。 我们可以使用javap -s这个命令查看,在classes目录下运行:(下面是cmd窗口的内容)
E:\Java_workspace\testjni\bin\classes>javap -s com.example.testjni.MainActivity Compiled from "MainActivity.java" public class com.example.testjni.MainActivity extends android.app.Activity { static {}; Signature: ()V public com.example.testjni.MainActivity(); Signature: ()V protected void onCreate(android.os.Bundle); Signature: (Landroid/os/Bundle;)V public void click(android.view.View); Signature: (Landroid/view/View;)V }
b.在调用方法的时候,注意把char* 类型的字符串转化为 jstring的,这样java才能获取到。
10、把java字符串转C字符串的过程分析
例如:我们要在java中调用C去加密字符串 java代码:
public void encode(View v){ EditText et = (EditText) findViewById(R.id.et); String pass = et.getText().toString(); String newPass = encodePass(pass, pas s.length()); et.setText(newPass); }
C代码:
encode.c: JNIEXPORT jstring JNICALL Java_com_example_strencode_MainActivity_encodePass (JNIEnv * env, jobject thiz, jstring pass, jint length){//pass是字符串常量传过来的 //自定义一个函数,把java字符串转换成c字符串 char* cstr = Jstring2CStr(env, pass);//返回的是一个指针 int i; for(i = 0; i < length; i++){//我们把length传过来使用方便,当然你也可以像下面一样使用反射拿到长度 //这个是直接在它的地址上进行修改的,当然改变该地址上的值了。 *(cstr + i) += 1; } return (*env)->NewStringUTF(env, cstr); } char* Jstring2CStr(JNIEnv* env, jstring jstr) { //rtn:字符指针变量,指向一个堆内存空间 char* rtn = NULL; //clsstring:java.lang.String的字节码 jclass clsstring = (*env)->FindClass(env,"java/lang/String"); //strencode:java字符串,值是GB2312 jstring strencode = (*env)->NewStringUTF(env,"GB2312"); //mid:String的getByte方法,参数是一个字符串 jmethodID mid = (*env)->GetMethodID(env,clsstring, "getBytes", "(Ljava/lang/String;)[B"); //barr:要转换的java字符串的字节数组,;类似java的 String .getByte("GB2312"); jbyteArray barr= (jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode); //alen:拿到转化后barr的长度 jsize alen = (*env)->GetArrayLength(env,barr); //ba: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;//手动置0结束 } //注意你手动申请了内存,用完后一定要释放内存 (*env)->ReleaseByteArrayElements(env,barr,ba,0); //返回转化后的字符数组的指针 return rtn; }
11、JNI调用C++
使用的步骤和调用C是一样的,但是要注意在编写C++代码有一些不一样。例如: a.C++中的JNIEnv和C的JNIEnv不是同一个结构体,C++的 JNIEnv 是jni.h中定义的 _JNIEnv _JNIEnv结构体中的函数其实就是调用了JNINativeInterface中的同名函数指针,所以调用函数不一样, 但是底层调用是一样的。 b.C++中函数要先声明才能使用C++代码:
#include <jni.h> //注意我们把javah生成的头文件放到jni目录,“”代表先从当前目录找头文件,找不到再到编译器目录找 //<>就是直接到编译器目录下找 #include "com_example_cplusplus_MainActivity.h" JNIEXPORT jstring JNICALL Java_com_itheima_cplusplus_MainActivity_helloFromCplusplus (JNIEnv * env, jobject thiz){ char* cstr = "hello from C++"; //注意因为env在C++和C中代表的结构体不一样,是一个一级指针,所以使用方法略有差异 jstring jstr = env->NewStringUTF(cstr); return jstr; }
12、分支C进程
java进程很容易被杀死,但是C进程比较难,系统进程都是c进程 fork函数分支一个C进程,返回子进程的pid 子进程执行fork函数时不会再分支进程了,返回0例如:
JNIEXPORT void JNICALL Java_com_example_fork_MainActivity_callC (JNIEnv * env, jobject thiz){ //分支C进程,返回一个整型 //返回的值是分支出来的子进程的进程id //子进程分支出来后,会把C代码又执行一次,但是不会再fork新的进程了,返回值为0 int pid = fork(); if(pid < 0 ){ LOGI("分支失败"); } else if(pid == 0){ //如果pid=0,说明代码执行在子进程 LOGI("子进程不能再fork了"); while(1){ LOGD("子进程while循环"); sleep(1); } } else if(pid > 0){ //如果pid>0,说明代码执行在主进程 LOGI("pid = %d", pid); } }
我们会在logcat中看到2句打印 pid=2789 pid=0 子进程while循环 子进程while循环 . . .注意: 我们结束进程,手动在应用管理中强制停止,或者卸载,分支出来的C进程仍然还在打印。 注意只适用于root后的手机,在没有root手机上是无效的。 所以慎用root权限。
阅读全文
0 0
- Android学习笔记16-JNI
- android JNI 学习笔记
- android JNI 学习笔记
- android jni 学习笔记
- android JNI 学习笔记
- Android JNI 学习笔记
- android JNI 学习笔记
- Android JNI学习笔记
- Android JNI 学习笔记
- Android JNI 学习笔记
- android JNI 学习笔记1
- android jni 学习笔记2
- android学习笔记之JNI
- android JNI入门-学习笔记
- 学习Android JNI开发笔记
- Android JNI学习笔记2
- [android学习笔记]学习jni编程
- Android JNI学习笔记1(Android Studio)
- 提高Eclipse的运行速度 去掉JPA这个Eclipse 插件
- 四旋翼电池、电机、螺旋桨选型与搭配
- php iis7 显示详细信息 web.config
- 《TensorFlow实战》TensorFlow上手代码
- squid启动服务脚本
- Android学习笔记16-JNI
- 字符设备驱动学习(1)
- * 24种设计模式——状态模式
- 1.C子空间
- 《零基础入门学习Python》学习过程笔记【36,37类】
- kerberos学习笔记
- SpringMVC使用JSR 303校验
- 畅通工程
- 最长公共子序列—C语言