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权限。
原创粉丝点击