Android学习笔记16-JNI

1、JNI

java native interface
APPLICATION 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    := hello
LOCAL_SRC_FILES := hello.c
include $(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.MainActivity
1.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.c

d.我们在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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值