Android JNI入门

#网络安全 共 4,253 字 约 14 分钟

前言#

初步学习Android NDK开发,尝试用as编写so文件并在java层调用它,从而更好地理解Android JNI(java native interface)技术。关于so文件的格式解析工作还没有做好,过几天再补上文档。

JNI介绍#

What##

JNI其实就是java和c/cpp之间进行通信的一个接口规范,java可以调用c/cpp里面的函数,同样,c/cpp也可以调用java类的方法。

Why##

  • 效率:C/C++是本地语言,比java效率更高
  • 安全:c语言的反编译比java更困难,一般加密算法都用C/C++
  • 便捷:可代码移植,复用一些C语言开发过的模块

基本数据类型对应##

Demo编写#

关于ndk的相关配置和项目内的一些设置不再赘述,直接看教程贴即可。主要记录一下代码实现部分及原理,还有一些过程中踩的坑。

简单输出字符串##

myJNI.java

plaintext
UTF-8|12 Lines|
package com.fish.demo6;

/**
 * Created by 75723 on 2018/7/31.
 */

public class myJNI {
static{
System.loadLibrary("JniTest");
}
public static native String sayHello();
}

加载so文件,声明sayHello()方法。

com_ fish_ demo6_myJNI.h

plaintext
UTF-8|21 Lines|
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_fish_demo6_myJNI */

#ifndef _Included_com_fish_demo6_myJNI
#define _Included_com_fish_demo6_myJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: com_fish_demo6_myJNI
 * Method:sayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_fish_demo6_myJNI_sayHello
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif   

将myJNI.java编译,再使用javah生成.h头文件(要在java目录下)

main.c

plaintext
UTF-8|4 Lines|
JNIEXPORT jstring JNICALL Java_com_fish_demo6_myJNI_sayHello
(JNIEnv *env, jclass jobj){
    return (*env)->NewStringUTF(env, "hello nicefish !");
}	

main.c的内容就是把头文件的内容拷贝过来,把函数声明实现。

  • 因为方法为static,所以需要传入jclass参数,表明是哪个类的方法
  • 如果是C++代码,则用(env),如果是C代码,则用env

MainActivity.java

plaintext
UTF-8|15 Lines|
package com.fish.demo6;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Log.i("开始调用so中的sayHello方法", myJNI.sayHello());
   }
}

调用myJNI的sayHello方法并用log打印出来。在logcat中查看

简单整型加法操作##

myJNI.java中添加

plaintext
UTF-8|1 Line|
public static native int Add(int a, int b);

对应生成的头文件也多了这个函数声明,然后main.c中实现

plaintext
UTF-8|9 Lines|
/*
 * Class: com_fish_demo6_myJNI
 * Method:Add
 * Signature: (II)I
 */
  JNIEXPORT jint JNICALL Java_com_fish_demo6_myJNI_Add
(JNIEnv *env, jclass jobj, jint a, jint b){
return a+b;
}

MainActivity.java打印结果。需要注意的是log.i()两个参数都是string类型的,myJNI.Add返回的结果是int,所以要转成string格式不然会报错。

plaintext
UTF-8|1 Line|
Log.i("a+b"," "+myJNI.Add(6,7));

打印Java对象信息##

首先创建个实体类User,定义了三个属性name,age,sex。以及get方法。

在myJNI.java里添加方法声明

plaintext
UTF-8|1 Line|
public static native String printUser(User user);

编译User.java,然后编译myJNI.java这时候会报错,说找不到符号User。因为myJNI里用到了另一个类User,所以把这两个.java一块编译即可

plaintext
UTF-8|1 Line|
javac User.java myJNI.java

main.c中实现方法。获得obj对象的类,然后获取对象中特定方法的id,调用该方法。这里的话只返回了个js_name,看一下打印效果即可。

plaintext
UTF-8|15 Lines|
 jclass cls_objClass =(*env)->GetObjectClass(env, obj);
jmethodID nameMethodId = (*env)->GetMethodID(env, cls_objClass, "getName","()Ljava/lang/String;");
jstring js_name = (jstring)(*env)->CallObjectMethod(env, obj, nameMethodId);
char name = (char)(*env)->GetStringUTFChars(env, js_name, 0);

jmethodID ageMethodId = (*env)->GetMethodID(env, cls_objClass, "getAge", "()I");
jint ji_age = (*env)->CallIntMethod(env, obj, ageMethodId);

jmethodID sexMethodId = (*env)->GetMethodID(env, cls_objClass, "getSex", "()Ljava/lang/String;");
jstring js_sex = (jstring)(*env)->CallObjectMethod(env, obj, sexMethodId);
char  sex = (char )(*env)->GetStringUTFChars(env, js_sex, 0);

(*env)->ReleaseStringUTFChars(env, js_name, name);
(*env)->ReleaseStringUTFChars(env, js_sex, sex);
return js_name;

这里GetMehodID的第四个参数是signature,用字符串描述了函数的参数和返回值 。例如:

”()V”

“(II)V”

“(Ljava/lang/String;Ljava/lang/String;)V”

实际上这些字符是与函数的参数类型一一对应的。 ”()” 中的字符表示参数,后面的则代表返回值。例如”()V” 就表示void Func(); “(II)V” 表示 void Func(int, int);

另外的补充:

  • 1、方法参数或者返回值为java中的对象时,签名中必须以“L”加上其路径,不过此路径必须以“/”分开,自定义的对象也使用本规则
  • 2、方法参数或者返回值为数组类型时,请前加上[

MainActivity.java里调用printUser()方法,log出name

踩坑#

在实际创建android项目,配置及编写过程中出现了很多错误,有一些是因为缺乏经验的低级错误,也一并记录当作教训。

  • ndk版本:最初使用的是最新的r17,但是过程中一直报错,也没追究到ndk的问题,后来尝试更换了r15,错误就没有了。测试后r14也可以,r16和r17不支持 ARM5 (armeabi)。
  • sdk版本:也是一些兼容问题,因为最初的报错找不到解决方法我就把sdk版本也换了,但是换了过低版本就会出现新的错误,说一些资源找不到比如TextAppearance.Material.Widget.Button.Borderless.Colored not found。sdk25/26一般就没啥错。
  • as版本:一开始也没注意我的版本只有1.x,参考教程时发现有很多特性不一样,就比如新建Activity默认继承AppCompatActivity等等。还有一些设置问题,总之换成2.3就好了。
  • v7包版本:注意这里的版本和app下的build.gradle里写的要对应起来
  • adb端口占用:参考https://blog.csdn.net/liranke/article/details/42524851
  • as部署:部署项目时出现错误:Instant Run requires ‘Tools | Android | Enable ADB integration’ to be enabled。这个是由于Android Studio2.x版本中的Instant Run (即时运行)引起的,点击工具栏中的Tools,选中Android,最后点击Enable ADB Integration,使其前面出现✔号就OK了。
  • 安装apk:出现错误INSTALL_FAILED_UPDATE_INCOMPATIBLE。一般是原来的程序卸载不干净导致的,可以找到旧安装包,安装成功再卸载即可。
  • ndk编译bug
    ,就是源文件(.c/.cpp)只有一个的话,编译会有error,所以需要建一个空的.c放在那里,就可以避免编译出错了。如图中的util.c就是一个空的.c文件。

参考#

会飞的丑小鸭系列教程

Android:JNI 与 NDK到底是什么?

简单JNI使用demo

Android NDK开发之Jni调用Java对象