Android JNI/NDK

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Rozol/article/details/88322757

JNI


本文由 Luzhuo 编写,转发请保留该信息.
原文: https://blog.csdn.net/Rozol/article/details/88322757


AndroidStudio3.2
NDK(native develop kit): Google提供的做JNI的一套工具包.

JNI

  • Java native interface: Java本地接口, 使Java与C/C++可以相互调用.
  • 能够扩展Java的能力, 使其能够调用驱动
  • 由于C/C++高运行效率的特性, 优化耗时操作
  • C/C++的反编译难度比Java高
  • C/C++有大量的优秀的开源库

C 的最最最基础

编译步骤

1.搭建开发环境

DevCpp

下载并安装: https://pc.qq.com/detail/16/detail_163136.html

2.编写HelloWorld

// 导入包 .h:头文件
#include<stdio.h>  // io 标准输入
#include<stdlib.h>  // lib 标准函数库

main(){  // main函数
	printf("HelloWorld!\n");
	system("pause"); // system执行win的pause命令
}

3.编译运行

按F11: 编译 + 运行

基本数据类型

Java数据类型 Java字节数 C数据类型 C字节数 description
boolean 1 - - 0为False, 非0为True
byte 1 - - -
char 2 char 1 不同
short 2 short 2 -
int 4 int 4 -
long 8 long 4 不同
float 4 float 4 -
double 8 double 8 -
- - signed - 有符号(默认), 用来修饰整形变量(char int short long)
- - unsigned - 无符号, 同上
- - void - 无值, 无定向指针
unsigned char c = 128;

输出函数

占位符 description
%d int
%ld long int
%lld long long
%hd short
%c char
%f float
%lf double
%u 无符号数
%x 十六进制输出int / long int / short int
%o 八进制输出
%s 字符串
int len = 10;
printf("number: %d\n", len); 

double d = 3.1415926;
printf("bumber: %.7lf\n", d);  // .7是控制小数点后的位数.

printf("%#x\n", len);  // # 自动加前缀

char array[] = {'a', 'b', 'c', 'd', '\0'};  // \0 为结束符 , 占5字节
char array2[] = "Hello 世界!";  // 字符串 
printf("String: %s\n", array);
printf("String: %s\n", array2);

输入函数

int number;
scanf("%d", &number);  // & 取地址符 

char string[4];  // 注: c的数组不检测小标越界 (可输入3字节, \0 占1字节)
scanf("%s", &string);
printf("输入的数字是: %d ;输入的字符串是: %s", number, string);

指针

swap(int* p1, int* p2){
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
} 

main(){
	int i = 123;
	printf("i的内存地址: %#x\n", i); 
	
	int* pointer = &i;  // int* int类型(指针类型与变量类型要匹配)的指针变量 (指针变量只能保存地址); & 取地址符  
	
	int i2 = *pointer;  // 把指针变量里的值取出来
	printf("读指针值: %d\n", i2); 
	
	*pointer = 456;  // 修改指针变量里的值 
	printf("写指针值: %d\n", i); 
	
	// 方法中的 值传递 和 引用传递 修改效果同Java (值传递数据不可被修改, 引用传递数据可被修改) 
	int i3 = 123;
	int j3 = 456; 
	swap(&i3, &j3);
	printf("i: %d ;j: %d\n", i3, j3); 
	
	// 系统不允许存在野指针 (以下写法是错误的)
	// int* p3;
	// *p3 = 123; 
	// 以下写法是正确的 
	int* p3 = 123;
	char* p4 = "hello!";
	
	// 一个地址变量占的空间
	int* p5;
	double* p6;
	// 64位系统输出为8字节, 64位系统64位系统总线,支持内存2^64字节, 8*8bit=64bit刚好够用 
	// 32位系统输出为4字节, 32位系统为32位系统总线,支持内存2^32=4GB, 4*8bit=32bit刚好够用 
	printf("int类型指针变量占%d个字节\n", sizeof(p5));  // 8
	printf("double类型指针变量占%d个字节\n", sizeof(p6));  // 8
	
	// 多级指针
	int i7 = 123;
	int* p7 = &i7;  // 一级指针
	int** p72 = &p7;  // 二级指针, 只能保存一级指针地址
	int*** p73 = &p72;  // 三级指针, 只能保存二级指针地址 
	// ...
	printf("三级指针取值%d\n", ***p73);  // 取值 
	printf("一级指针的地址%#x\n", **p73);  // 取地址 
	
	// 栈内存, 静态内存分配(大小固定, 地址连续), 系统统一分配统一回收, 并且是无法控制的 (函数调用完自动释放) 
	int array8 = {1, 2, 3, 4};  // 栈内存 
	// 堆内存, 动态内存分配, 由自己控制分配和回收 
	int* p8 = malloc(sizeof(int)*8);  // 申请堆内存 
	p8 = realloc(p8, sizeof(int)*8 + 1);  // 重新申请堆内存 (空间足够则后续, 不足则寻找新空间, 旧元素会被自动转移)  
	free(p8);  // 回收堆内存
}

结构体 / 联合体 / 枚举 / 自定义类型

结构体

void show(){
	printf("hello.\n");
} 
int show2(int i){
	return i*2;
}
struct Person{
	char sex;
	int age;
	void(*funcp)();  // 函数指针 
	int(*funcp2)(int);  // 定义: 返回值(*函数指针变量名)(返回值); 
}; 

main(){
	// 结构体 (类似于Java中的Class) 
	// 结构体大小 >= 结构体中每个变量占字节数总和, 并且大小是最大那个变量占字节数的整数倍 
	struct Person p9 = {'f', 10};
	printf("age: %d\n", p9.age);  // 取值 
	// 调用函数 
	p9.funcp = &show;  // 赋予函数指针 
	p9.funcp();
	p9.funcp2 = &show2;
	printf("show: %d\n", p9.funcp2(5));
	// 结构体多级指针
	struct Person* p9p = &p9;
	printf("*Person.age: %d", (*p9p).age);
	(*p9p).age = 21;
	// 间接应用运算符
	printf("Person(p9p) -> age: %d", p9p->age);
}

联合体

union Peron1{
	char sex;
	int age;
};

main(){
	// 联合体, 多个变量占用同一块内存, 起到节省内存的作用 
	// 联合体占字节数为 其中成员占内存最大的一个 
	union Peron1 p10; 
	p10.sex = "g";
	p10.age = 123;
	printf("union p10.sex: %c\n", p10.sex); // union p10.sex: {
}

枚举

enum Color{
	RED, BLUE, YELLOW
}; 

main(){
	// 枚举, 从0开始, 规定了取值只能从枚举里取值 
	enum Color c = BLUE;
	printf("color: %d\n", c); // color: 1
}

自定义类型

typedef int I;

typedef struct Person{
	char sex;
	int age;
} Per; 

main(){
	I i = 123;
	printf("I: %d\n", i);
	
	Per p11 = {"f", 21}; 
	printf("Per: %d\n", p11.age);
}

JNI

交叉编译

交叉编译: 在一个平台上编译, 在另一平台执行的本地代码
原理: 模拟不同平台的特性去编译代码

  • cpu平台:
    • arm (90%+)
    • x86 (Intel)
    • mips (嵌入设备)
  • 操作系统平台:
    • windows
    • linux
    • mac
    • os

开发流程

1.下载NDK

SDK Manager -> Android SDK -> SDK Tools
选择以下工具进行下载

CMake // 构建工具
LLDB // 调试程序
NDK // jni工具包

2.创建项目

创建项目的第一页把Include C++ support√上.

比平时开发多了Customize C++ Support向导页:

  • C++ Standard: C++ 标准(默认就好), 默认Toolchain Default(使用CMake设置)
  • Exceptions Support: 启用对 C++ 异常处理的支持(√上)
  • Runtime Type Information Support: 启用RTTI(运行时类型信息)(√上)

创建完成后, 还会帮我们生成示范代码

  • cpp: C/C++放在cpp下
  • External Build Files: 构建脚本

CMakeLists.txt应用构建脚本

# 最低构建本机所需库
cmake_minimum_required(VERSION 3.4.1)

# 可以定义多个library库
add_library(
             # 设置库的名称
             native-lib

             # 设置为共享库
             SHARED

             # 源文件相对路径
             src/main/cpp/native-lib.cpp )

# 搜索预先构建的库
find_library( 
              # 设置path变量名
              log-lib

              # 指定NDK库名
              log )

# 指定库CMake应该链接到目标库, 可链接多个库
target_link_libraries( 
                       # 指定目标库
                       native-lib
                     
                       # 目标库到log库的链接导入到NDK
                       ${log-lib} )

示范代码是C++, 如果换成C也是支持的:

#include <jni.h>

jstring Java_me_luzhuo_hellondk_MainActivity_stringFromJNI(JNIEnv* env, jobject thiz){
    char* cstr = "Hello from C!";
    return (*env)->NewStringUTF(env, cstr);
}

2.1在原项目引进NDK

这是个小插曲, 如果你的项目已经开发, 并且还没有引入NDK, 那么这步适合你, 否则请跳过.

a.编写Native代码:

public class MainActivity extends AppCompatActivity {

    // 导入so库
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 调用本地方法
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    // 定义本地方法
    public native String stringFromJNI();
}

b.编写C/C++

app/src/main下创建cpp文件夹

然后编写c代码.

#include <jni.h>

// Java_包名_类名_本地方法名
// jobject thiz: 调用本地函数的Java对象(MainActivity)
// JNIEnv* env: 定义在jni.h文件里, 是结构体JNINativeInterface的二级指针, 所以JNIEnv是它的一级指针, JNINativeInterface中定义了常用的大量指针
jstring Java_me_luzhuo_hellondk_MainActivity_stringFromJNI(JNIEnv* env, jobject thiz){
    char* cstr = "Hello from C!";

    // 将char*转成jstring
    // 转换函数都在sdk\ndk-bundle\sysroot\usr\include\jni.h中的JNINativeInterface定义了,
    // 我们找到将char*转成jstring的函数 jstring (*NewStringUTF)(JNIEnv*, const char*);
    return (*env)->NewStringUTF(env, cstr);
}

以上的方法编写为了避免手动敲错, 可以生成头文件, 然后直接剪切粘贴:

生成.h头文件

cd app/src/main/java
javah me.luzhuo.hellondk.MainActivity

注: 我用的是java7

  • java7 在src运行javah
  • java6 在bin运行javah

生成的头文件并没有什么实际作用, 主要用于避免出错

打开me_luzhuo_hellondk_MainActivity.h头文件, copy相关方法代码到cpp下的相关.c文件里(如native-lib.c)

JNIEXPORT jstring JNICALL Java_me_luzhuo_hellondk_MainActivity_stringFromJNI
  (JNIEnv *, jobject);

JNIEXPORT 和 JNICALL 这些关键字可删可不删, 用完的头文件可以直接删了, 加上形参就可编写c代码了

c.在app下创建CMakeLists.txt文件

内容直接拷贝上面2的CMakeLists.txt内容即可, 内容都是一样的, 这里略

d.build.gradle(app)配置

android标签内配置

android {
	...

    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

e.然后Build -> Make Project, 会自动在build\intermediates\cmake\debug\obj生成对应的.so库

注意:

以下这段配置是不需要配置的, 如果你点的是Build -> Make Project构建会帮你构建全部cpu的so库, 如果你点运行构建, 只会帮你构建你运行设备的cpu动态链接库 (如 运行于联想S658t手机只会帮你编译armeabi-v7a这一个so库, 运行于华为Note8只会帮你编译arm64-v8a这一个so库)

ndk {
    abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}

旧版本的一些小知识

Android.mk // 告诉系统资源在哪

LOCAL_PATH := $(call my-dir)  // 当前目录

include $(CLEAR_VARS)  // 清除变量

LOCAL_MODULE    := lua  // 生成文件的名字
LOCAL_SRC_FILES := lapi.c lauxlib.c  // c的源文件, 多个用空格隔开
LOCAL_LDLIBS    := -ld -lm

include $(BUILD_STATIC_LIBRARY)  // 生成的是.so库

Application.mk

APP_ABI := all  // 编译支持cpu的so库, all表示全编译, 多平台用空格隔开
APP_PLATFORM := android-16  // 编译版本(最小)

Java 与 C 之间的相互调用

Log

CMakeLists.txt添加依赖库

find_library( 
              log-lib

              log )

target_link_libraries(
					   // ...
                       ${log-lib} )

在C中的使用:

// log
#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__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

JNIEXPORT jintArray JNICALL Java_me_luzhuo_ndkdemo_JavaCallCUtils_passArray(JNIEnv * env, jobject thiz, jintArray intArray){
  jsize length = (*env)->GetArrayLength(env, intArray);

  LOGD("intArray长度: %d", length);
  return intArray;

Java调用C

Java与C的关系见 sdk\ndk-bundle\sysroot\usr\include\jni.h 文件定义.

C中的数据类型与Java中数据类型的关系

typedef unsigned char   jboolean;       /* unsigned 8 bits */
typedef signed char     jbyte;          /* signed 8 bits */
typedef unsigned short  jchar;          /* unsigned 16 bits */
typedef short           jshort;         /* signed 16 bits */
typedef int             jint;           /* signed 32 bits */
typedef long long       jlong;          /* signed 64 bits */
typedef float           jfloat;         /* 32-bit IEEE 754 */
typedef double          jdouble;        /* 64-bit IEEE 754 */

typedef void*           jobject;
typedef jobject         jclass;
typedef jobject         jstring;
typedef jobject         jarray;
typedef jarray          jobjectArray;
typedef jarray          jbooleanArray;
typedef jarray          jbyteArray;
typedef jarray          jcharArray;
typedef jarray          jshortArray;
typedef jarray          jintArray;
typedef jarray          jlongArray;
typedef jarray          jfloatArray;
typedef jarray          jdoubleArray;
typedef jobject         jthrowable;
typedef jobject         jweak;

本地代码定义:

public class JavaCallCUtils {
    static {
        System.loadLibrary("jni-lib");
    }

    public native int passInt(int x, int y);

    public native String passString(String s);

    public native int[] passArray(int[] ints);
}

调用本地方法

public class JavaCallCActivity extends AppCompatActivity {
    private static final String TAG = JavaCallCActivity.class.getSimpleName();

    JavaCallCUtils jni = new JavaCallCUtils();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_javacallc);
    }

    /**
     * 传递基本数据类型
     */
    public void ints(View view) {
        Log.e(TAG, "" + jni.passInt(5, 6));
    }

    /**
     * 传递Java特有数据类型
     */
    public void strings(View view) {
        Log.e(TAG, "" + jni.passString("World"));
    }

    /**
     * 传递数组
     */
    public void arrays(View view) {
        Log.e(TAG, "" + Arrays.toString(jni.passArray(new int[]{1, 2, 3})));
    }
}

调用的C代码:

#include <jni.h>
#include <stdlib.h>
#include <string.h>

/*
 * Class:     me_luzhuo_ndkdemo_JavaCallCUtils
 * Method:    passInt
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_me_luzhuo_ndkdemo_JavaCallCUtils_passInt(JNIEnv * env, jobject thiz, jint intx, jint inty){
  /*
  typedef unsigned char   jboolean;       // unsigned 8 bits
  typedef signed char     jbyte;          // signed 8 bits
  typedef unsigned short  jchar;          // unsigned 16 bits
  typedef short           jshort;         // signed 16 bits
  typedef int             jint;           // signed 32 bits
  typedef long long       jlong;          // signed 64 bits
  typedef float           jfloat;         // 32-bit IEEE 754
  typedef double          jdouble;        // 64-bit IEEE 754
  */

  // C的int和java的jint相同, 直接操作就行了
  return intx * inty;
}

/*
 * Class:     me_luzhuo_ndkdemo_JavaCallCUtils
 * Method:    passString
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_me_luzhuo_ndkdemo_JavaCallCUtils_passString(JNIEnv * env, jobject thiz, jstring s){
  // typedef jobject         jstring;
  // 将Java的jstring转成C的char*类型
  // const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
  char* cstr = (*env)->GetStringUTFChars(env, s, NULL);
  int length = strlen(cstr); // 获取字符串长度

  int i;
  for(i = 0; i < length; i++){
    *(cstr+i) += 1;
  }

  // 将C的char*类型转成Java的jstring
  // jstring     (*NewStringUTF)(JNIEnv*, const char*);
  return (*env)->NewStringUTF(env, cstr);
}


/*
 * Class:     me_luzhuo_ndkdemo_JavaCallCUtils
 * Method:    passArray
 * Signature: ([I)[I
 */
JNIEXPORT jintArray JNICALL Java_me_luzhuo_ndkdemo_JavaCallCUtils_passArray(JNIEnv * env, jobject thiz, jintArray intArray){
  // jsize       (*GetArrayLength)(JNIEnv*, jarray);
  jsize length = (*env)->GetArrayLength(env, intArray);

  // jint*       (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
  int* arrayPointer = (*env)->GetIntArrayElements(env, intArray, NULL);
  int i;
  for(i = 0; i < length; i++){
    *(arrayPointer+i) += 10;
  }

  return intArray;
}

JNI的jstring类型和C的Char*类型相互转换的方法:

// 获取字符串长度
jsize       (*GetStringLength)(JNIEnv*, jstring);
jsize       (*GetStringUTFLength)(JNIEnv*, jstring);

// char* -> jstring
jstring     (*NewString)(JNIEnv*, const jchar*, jsize);
jstring     (*NewStringUTF)(JNIEnv*, const char*);

// jstring -> char*
const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);  // (utf-16)
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);  // (utf-8)

// jstring -copy-> jchar* (参数: jstring源 / start / len / jchar*目标)
void        (*GetStringRegion)(JNIEnv*, jstring, jsize, jsize, jchar*);
void        (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);

// 释放指针
void        (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);
void        (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);

基本类型数组的方法

// array -> array*
jboolean*   (*GetBooleanArrayElements)(JNIEnv*, jbooleanArray, jboolean*);
jbyte*      (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*);
jchar*      (*GetCharArrayElements)(JNIEnv*, jcharArray, jboolean*);
jshort*     (*GetShortArrayElements)(JNIEnv*, jshortArray, jboolean*);
jint*       (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
jlong*      (*GetLongArrayElements)(JNIEnv*, jlongArray, jboolean*);
jfloat*     (*GetFloatArrayElements)(JNIEnv*, jfloatArray, jboolean*);
jdouble*    (*GetDoubleArrayElements)(JNIEnv*, jdoubleArray, jboolean*);


void ReleaseBooleanArrayElements(jbooleanArray array, jboolean* elems, jint mode)
void ReleaseByteArrayElements(jbyteArray array, jbyte* elems, jint mode)
void ReleaseCharArrayElements(jcharArray array, jchar* elems, jint mode)
void ReleaseShortArrayElements(jshortArray array, jshort* elems, jint mode)
void ReleaseIntArrayElements(jintArray array, jint* elems, jint mode)
void ReleaseLongArrayElements(jlongArray array, jlong* elems, jint mode)
void ReleaseFloatArrayElements(jfloatArray array, jfloat* elems, jint mode)
void ReleaseDoubleArrayElements(jdoubleArray array, jdouble* elems, jint mode)


// array -copy-> array*
void GetBooleanArrayRegion(jbooleanArray array, jsize start, jsize len, jboolean* buf)
void GetByteArrayRegion(jbyteArray array, jsize start, jsize len, jbyte* buf)
void GetCharArrayRegion(jcharArray array, jsize start, jsize len, jchar* buf)
void GetShortArrayRegion(jshortArray array, jsize start, jsize len, jshort* buf)
void GetIntArrayRegion(jintArray array, jsize start, jsize len, jint* buf)
void GetLongArrayRegion(jlongArray array, jsize start, jsize len, jlong* buf)
void GetFloatArrayRegion(jfloatArray array, jsize start, jsize len, jfloat* buf)
void GetDoubleArrayRegion(jdoubleArray array, jsize start, jsize len, jdouble* buf)

void SetBooleanArrayRegion(jbooleanArray array, jsize start, jsize len, const jboolean* buf)
void SetByteArrayRegion(jbyteArray array, jsize start, jsize len, const jbyte* buf)
void SetCharArrayRegion(jcharArray array, jsize start, jsize len, const jchar* buf)
void SetShortArrayRegion(jshortArray array, jsize start, jsize len, const jshort* buf)
void SetIntArrayRegion(jintArray array, jsize start, jsize len, const jint* buf)
void SetLongArrayRegion(jlongArray array, jsize start, jsize len, const jlong* buf)
void SetFloatArrayRegion(jfloatArray array, jsize start, jsize len, const jfloat* buf)
void SetDoubleArrayRegion(jdoubleArray array, jsize start, jsize len, const jdouble* buf)


jbooleanArray NewBooleanArray(jsize length)
jbyteArray NewByteArray(jsize length)
jcharArray NewCharArray(jsize length)
jshortArray NewShortArray(jsize length)
jintArray NewIntArray(jsize length)
jlongArray NewLongArray(jsize length)
jfloatArray NewFloatArray(jsize length)
jdoubleArray NewDoubleArray(jsize length)

C调用Java

获取方法签名:

cd app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes
javap -s me.luzhuo.ndkdemo.JNIDemoClass

生成内容如下

Compiled from "JNIDemoClass.java"
public class me.luzhuo.ndkdemo.JNIDemoClass {
  public me.luzhuo.ndkdemo.JNIDemoClass();
    descriptor: ()V

  public native void callbackHelloFromJava();
    descriptor: ()V

  public native void add();
    descriptor: ()V

  public native void printString();
    descriptor: ()V

  public void helloFromJava();
    descriptor: ()V

  public int add(int, int);
    descriptor: (II)I

  public void printString(java.lang.String);
    descriptor: (Ljava/lang/String;)V

  static {};
    descriptor: ()V
}

比如: ()V就是helloFromJava()方法的方法签名

C里面调用Java方法的步骤:

Java代码:

package me.luzhuo.ndkdemo;

public class JNIDemoClass {
    static {
        System.loadLibrary("jni-lib");
    }

    public native void callbackHelloFromJava();

    public void helloFromJava(){
        Log.e(TAG, "" + "helloFromJava");
    }
}

C代码:

JNIEXPORT void JNICALL Java_me_luzhuo_ndkdemo_JNIDemoClass_callbackHelloFromJava
  (JNIEnv * env, jobject thiz){

  // 1.获取字节码对象
  // jclass      (*FindClass)(JNIEnv*, const char*);
  jclass clazz = (*env)->FindClass(env, "me/luzhuo/ndkdemo/JNIDemoClass");

  // 2.获取Method对象
  // jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
  // { return functions->GetMethodID(this, clazz, name, sig); }
  jmethodID methodid = (*env)->GetMethodID(env, clazz, "helloFromJava", "()V");

  // 3.字节码对象创建Object
  // 这里的object是thiz, 因为是通过Java调用C代码传进来的实例对象

  // 4.通过对象调用方法
  (*env)->CallVoidMethod(env, thiz, methodid);
}

1.获取字节码对象: 类名为全类名

// jclass      (*FindClass)(JNIEnv*, const char*);
jclass clazz = (*env)->FindClass(env, "me/luzhuo/ndkdemo/JNIDemoClass");

2.获取Method对象:

// jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
jmethodID methodid = (*env)->GetMethodID(env, clazz, "helloFromJava", "()V");

helloFromJava是Java中定义的方法名
()V是方法签名, 获取方式上面有

获取方法

// 参数: clazz / 方法名称 / 方法签名
jmethodID   GetMethodID(jclass clazz, const char* name, const char* sig)
jmethodID   (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);

当然还能获取字段

// 参数: clazz / 字段名称 / 字段签名
jfieldID    (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
jfieldID    (*GetStaticFieldID)(JNIEnv*, jclass, const char*, const char*);

3.字节码对象创建Object
由上面的java代码可知, 我把 native方法 和 被c调用的方法 都写在JNIDemoClass类中, 所以这里的objectobject就是thiz, 也是JNIDemoClass实例对象, 这是通过Java调用C代码传进来的实例对象.

如果不是写在同一类中怎么办, 可使用以下代码创建

// 3.字节码对象创建Object
// jobject     (*AllocObject)(JNIEnv*, jclass);
jobject obj = (*env)->AllocObject(env, clazz);

创建对象的方法

jobject     (*AllocObject)(JNIEnv*, jclass);
jobject     (*NewObject)(JNIEnv*, jclass, jmethodID, ...);

插播: 关于NewObject的案例

JNIEXPORT void JNICALL Java_me_luzhuo_ndkdemo_JNIDemoClass_time
  (JNIEnv * env, jobject thiz){
  // 1.获取字节码对象
  jclass clazz_date = (*env)->FindClass(env, "java/util/Date");
  // 2.获取Method对象
  jmethodID method_date_getTime = (*env)->GetMethodID(env, clazz_date, "getTime", "()V");
  // 3.字节码对象创建Object
  // 构造方法的方法名为<init>
  jmethodID method_date = (*env)->GetMethodID(env, clazz_date, "<init>", "()V");
  jobject obj_date = (*env)->NewObject(clazz_date, method_date);
  // 4.通过对象调用方法
  jlong time = (*env)->CallLongMethod(env, obj_date, method_date_getTime);
}

4.通过对象调用方法

// 4.通过对象调用方法
(*env)->CallVoidMethod(env, thiz, methodid);

由于java方法public void helloFromJava();是返回Void类型的, 所以选择CallVoidMethod函数, 如果是返回其他类型的, 还有以下可以选择

jobject     (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
jboolean    (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...);
jbyte       (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...);
jchar       (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...);
jshort      (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...);
jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
jlong       (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...);
jfloat      (*CallFloatMethod)(JNIEnv*, jobject, jmethodID, ...);
jdouble     (*CallDoubleMethod)(JNIEnv*, jobject, jmethodID, ...);
void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);


jobject     (*CallStaticObjectMethod)(JNIEnv*, jclass, jmethodID, ...);
jboolean    (*CallStaticBooleanMethod)(JNIEnv*, jclass, jmethodID, ...);
jbyte       (*CallStaticByteMethod)(JNIEnv*, jclass, jmethodID, ...);
jchar       (*CallStaticCharMethod)(JNIEnv*, jclass, jmethodID, ...);
jshort      (*CallStaticShortMethod)(JNIEnv*, jclass, jmethodID, ...);
jint        (*CallStaticIntMethod)(JNIEnv*, jclass, jmethodID, ...);
jlong       (*CallStaticLongMethod)(JNIEnv*, jclass, jmethodID, ...);
jfloat      (*CallStaticFloatMethod)(JNIEnv*, jclass, jmethodID, ...) __NDK_FPABI__;
jdouble     (*CallStaticDoubleMethod)(JNIEnv*, jclass, jmethodID, ...) __NDK_FPABI__;
void        (*CallStaticVoidMethod)(JNIEnv*, jclass, jmethodID, ...);


// 调用父类方法, 参数都要传父类的(father_obj / father_clazz / father_methodid)
jobject     (*CallNonvirtualObjectMethod)(JNIEnv*, jobject, jclass, jmethodID, ...);
jboolean    (*CallNonvirtualBooleanMethod)(JNIEnv*, jobject, jclass, jmethodID, ...);
jbyte       (*CallNonvirtualByteMethod)(JNIEnv*, jobject, jclass, jmethodID, ...);
jchar       (*CallNonvirtualCharMethod)(JNIEnv*, jobject, jclass, jmethodID, ...);
jshort      (*CallNonvirtualShortMethod)(JNIEnv*, jobject, jclass, jmethodID, ...);
jint        (*CallNonvirtualIntMethod)(JNIEnv*, jobject, jclass, jmethodID, ...);
jlong       (*CallNonvirtualLongMethod)(JNIEnv*, jobject, jclass, jmethodID, ...);
jfloat      (*CallNonvirtualFloatMethod)(JNIEnv*, jobject, jclass, jmethodID, ...) __NDK_FPABI__;
jdouble     (*CallNonvirtualDoubleMethod)(JNIEnv*, jobject, jclass, jmethodID, ...) __NDK_FPABI__;
void        (*CallNonvirtualVoidMethod)(JNIEnv*, jobject, jclass, jmethodID, ...);

当然字段的使用也同方法类似

jobject     (*GetObjectField)(JNIEnv*, jobject, jfieldID);
jboolean    (*GetBooleanField)(JNIEnv*, jobject, jfieldID);
jbyte       (*GetByteField)(JNIEnv*, jobject, jfieldID);
jchar       (*GetCharField)(JNIEnv*, jobject, jfieldID);
jshort      (*GetShortField)(JNIEnv*, jobject, jfieldID);
jint        (*GetIntField)(JNIEnv*, jobject, jfieldID);
jlong       (*GetLongField)(JNIEnv*, jobject, jfieldID);
jfloat      (*GetFloatField)(JNIEnv*, jobject, jfieldID) __NDK_FPABI__;
jdouble     (*GetDoubleField)(JNIEnv*, jobject, jfieldID) __NDK_FPABI__;

void        (*SetObjectField)(JNIEnv*, jobject, jfieldID, jobject);
void        (*SetBooleanField)(JNIEnv*, jobject, jfieldID, jboolean);
void        (*SetByteField)(JNIEnv*, jobject, jfieldID, jbyte);
void        (*SetCharField)(JNIEnv*, jobject, jfieldID, jchar);
void        (*SetShortField)(JNIEnv*, jobject, jfieldID, jshort);
void        (*SetIntField)(JNIEnv*, jobject, jfieldID, jint);
void        (*SetLongField)(JNIEnv*, jobject, jfieldID, jlong);
void        (*SetFloatField)(JNIEnv*, jobject, jfieldID, jfloat) __NDK_FPABI__;
void        (*SetDoubleField)(JNIEnv*, jobject, jfieldID, jdouble) __NDK_FPABI__;


jobject     (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);
jboolean    (*GetStaticBooleanField)(JNIEnv*, jclass, jfieldID);
jbyte       (*GetStaticByteField)(JNIEnv*, jclass, jfieldID);
jchar       (*GetStaticCharField)(JNIEnv*, jclass, jfieldID);
jshort      (*GetStaticShortField)(JNIEnv*, jclass, jfieldID);
jint        (*GetStaticIntField)(JNIEnv*, jclass, jfieldID);
jlong       (*GetStaticLongField)(JNIEnv*, jclass, jfieldID);
jfloat      (*GetStaticFloatField)(JNIEnv*, jclass, jfieldID) __NDK_FPABI__;
jdouble     (*GetStaticDoubleField)(JNIEnv*, jclass, jfieldID) __NDK_FPABI__;

void        (*SetStaticObjectField)(JNIEnv*, jclass, jfieldID, jobject);
void        (*SetStaticBooleanField)(JNIEnv*, jclass, jfieldID, jboolean);
void        (*SetStaticByteField)(JNIEnv*, jclass, jfieldID, jbyte);
void        (*SetStaticCharField)(JNIEnv*, jclass, jfieldID, jchar);
void        (*SetStaticShortField)(JNIEnv*, jclass, jfieldID, jshort);
void        (*SetStaticIntField)(JNIEnv*, jclass, jfieldID, jint);
void        (*SetStaticLongField)(JNIEnv*, jclass, jfieldID, jlong);
void        (*SetStaticFloatField)(JNIEnv*, jclass, jfieldID, jfloat) __NDK_FPABI__;
void        (*SetStaticDoubleField)(JNIEnv*, jclass, jfieldID, jdouble) __NDK_FPABI__;

需要上下文的方法:

比如说我想调用以下方法;

public void showToast(String s){
    Toast.makeText(getApplicationContext(), "" + s, Toast.LENGTH_SHORT).show();
}

这段代码需要getApplicationContext(), 这可不能直接new出来, 以下是解决方案:

public class JNIDemoClass {

    private Context mContext;
    public JNIDemoClass(Context context){
        this.mContext = context;
    }

    static {
        System.loadLibrary("jni-lib");
    }

    public native void toast();

    public void showToast(String s){
        Toast.makeText(mContext, "" + s, Toast.LENGTH_SHORT).show();
    }
}

就是在创建JNIDemoClass对象的时候, 直接把Context给他就好了.

c的代码都一样

JNIEXPORT void JNICALL Java_me_luzhuo_ndkdemo_JNIDemoClass_toast
  (JNIEnv * env, jobject thiz){
    // 1.获取字节码对象
    jclass clazz = (*env)->FindClass(env, "me/luzhuo/ndkdemo/JNIDemoClass");
    // 2.获取Method对象
    jmethodID methodid = (*env)->GetMethodID(env, clazz, "showToast", "(Ljava/lang/String;)V");
    // 3.字节码对象创建Object
    // jobject     (*AllocObject)(JNIEnv*, jclass);
    /* jobject obj = (*env)->AllocObject(env, clazz); */
    // 4.通过对象调用方法
    jstring str = (*env)->NewStringUTF(env, "这里是C");
    (*env)->CallVoidMethod(env, thiz/*obj*/, methodid, str);
}

Java的参数要String类型, 而C里只有char*类型, 需要转成String类型

jstring str = (*env)->NewStringUTF(env, "这里是C");

Java与C++的互调

上面写的都是C语言的, C++与C的用法差别很小.

1.编写Java代码

在java中写法都是一样的

package me.luzhuo.ndkdemo;

public class JavaCallCppUtils {
    static {
        System.loadLibrary("jni-lib");
    }
    
    public native String passString(String s);
}

public class JavaCallCppActivity extends AppCompatActivity {
    private static final String TAG = JavaCallCppActivity.class.getSimpleName();

    JavaCallCppUtils jni = new JavaCallCppUtils();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_javacallc);
    }

    public void strings(View view) {
        Log.e(TAG, "" + jni.passString("World"));
    }
}

2.生成.h头文件

使用javah命令生成.h头文件, 在C的用法里头文件是可以删除的, 但是在C++的用法里, 这个头文件有用.

cd app/src/main/java
javah me.luzhuo.ndkdemo.JavaCallCppUtils

把头文件剪切到cpp文件夹

3.编写C++代码

在cpp文件夹里创建以.cpp为后缀的文件 (如 javacallcpp.cpp)

同样需要把.h头文件里生成的方法copy到.cpp文件里.

C++代码:

#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include "me_luzhuo_ndkdemo_JavaCallCppUtils.h"  // ""号先从本地找, 再去include找; <>号直接去include找

/*
 * Class:     me_luzhuo_ndkdemo_JavaCallCppUtils
 * Method:    passString
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_me_luzhuo_ndkdemo_JavaCallCppUtils_passString
  (JNIEnv * env, jobject thiz, jstring s){
  // const char* GetStringUTFChars(jstring string, jboolean* isCopy)
  const char* cstr = env->GetStringUTFChars(s, NULL);

  int length = strlen(cstr); // 获取字符串长度
  char* ncstr = new char[length];
  strcpy(ncstr,cstr);

  // void ReleaseStringUTFChars(jstring string, const char* utf)
  env->ReleaseStringUTFChars(s, cstr);

  int i;
  for(i = 0; i < length; i++){
    *(ncstr+i) += 1;
  }

  // jstring NewStringUTF(const char* bytes)
  return env->NewStringUTF((const char*)ncstr);
}

C++函数要先声明再使用, 所以要把头文件include进来

C与C++的JNIEnv区别:

这是在jni.h里定义的JNIEnv.

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

C的JNIEnv是JNINativeInterface*, 是JNIEnv的二级指针, 所以用法是

// const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
char* cstr = (*env)->GetStringUTFChars(env, s, NULL);

C++的JNIEnv是_JNIEnv, 是_JNIEnv的一级指针, 实际还是去调用JNINativeInterface*里定义的方法, 并且传递了this(_JNIEnv对象), 所以我们调用时就不需要传env了,

struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    const char* GetStringUTFChars(jstring string, jboolean* isCopy)
    { return functions->GetStringUTFChars(this, string, isCopy); }

所以用法是

// const char* GetStringUTFChars(jstring string, jboolean* isCopy)
const char* cstr = env->GetStringUTFChars(s, NULL);

C/C++引用类型

  • 局部引用:
    • 最常见的应用类型, JNI返回的引用都是局部应用
    • 局部引用在该native函数中有效, 函数返回后自动释放, 也可使用DeleteLocalRef手动释放
    • 如果一个局部引用指向一个庞大的对象时, 最好使用完后调用DeleteLocalRef, 以确保触发垃圾回收器的时候能够回收
  • 全局引用:
    • 可以跨线程, 多个native都有效
    • 需要手动创建NewGlobalRef, 手动释放ReleaseGlobalRef
  • 弱引用:
    • 需要手动创建NewWeakGlobalRef, 手动释放ReleaseWeakGlobalRef
jobject     (*NewLocalRef)(JNIEnv*, jobject);
jobject     (*NewGlobalRef)(JNIEnv*, jobject);
jweak NewWeakGlobalRef(jobject obj)

void        (*DeleteLocalRef)(JNIEnv*, jobject);
void        (*DeleteGlobalRef)(JNIEnv*, jobject);
void DeleteWeakGlobalRef(jweak obj)

// 两引用是否相等
jboolean    (*IsSameObject)(JNIEnv*, jobject, jobject);

注意

  • 使用.so文件注意与它的全类名(包名+类名)要对应
    • 不要用了别人的so文件, 却把native代码却放到自己的包下, 这是不对的
  • native方法名和包名均不能被混淆
    • 混淆之后就找不到了
  • 被c调用的方法名也不能被混淆
  • jni调用在主线程进行, 耗时操作请用辅线程or辅进程

案例 (fork进程)

这里编写一个永远运行的Activity的方法案例.

这个案例能在Android5.0以下正常运行, 如果是5.0+的版本, 那就不行了.
因为Android5.0+系统回收策略改成了进程组, 并且应用被杀之后, 任何Service也会被杀.

相关命令

adb shell
ps // 查看进程
kill 28553  // 杀死某pid进程

Java代码

public class ForkUtils {

    static {
        System.loadLibrary("jni-lib");
    }

    public native void fork();
}

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 永久运行
        new ForkUtils().fork();
    }
}

C代码:

#include <jni.h>
#include <stdlib.h>
#include <stdio.h>

#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

int ppid;
/*
 * Class:     me_luzhuo_ndkdemo_jni_ForkUtils
 * Method:    fork
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_me_luzhuo_ndkdemo_jni_ForkUtils_fork
  (JNIEnv * env, jobject thiz){

  int pid = fork();
  /*
     fork只能在主进程, 分叉出的子进程还会再执行一次此处代码, 子进程不由Android管理

     pid > 0: fork成功, 返回子进程pid
     pid == 0: 子进程不能fork, 返回0
     pid < 0: fork失败

     adb shell ps
     root      140   1     503608 27568 ffffffff 00000000 S zygote
     u0_a121   28553 140   550756 26488 ffffffff 4005b624 S me.luzhuo.ndkdemo  // 主进程
     u0_a121   28863 28553 545644 13716 c006adc8 4005ad6c S me.luzhuo.ndkdemo  // 子进程, 26082本进程pid, 25784辅进程pid

     root      1     0     1380   672   c0119ff0 00061e78 S /init
     u0_a121   28863 1     545644 13716 c006adc8 4005ad6c S me.luzhuo.ndkdemo  // kill 28553 后
  */
  if(pid > 0){ // 主进程


  }else if(pid == 0){ // 子进程
    while(1){
      /*
        如果子进程不结束, 父进程被结束(即使被卸载), 那么父进程的pid变为1
      */
      LOGD("sub proceess is running");
      sleep(2); // 2s

      /*
        am命令: (命令文件存于:system/bin/am)

        // 打开浏览器 / 设备16+支持多用户, 需要指定--user 0
        am start --user 0 -a android.intent.action.VIEW -d http://www.baidu.com
        execlp("am", "am", "start", "--user","0","-a", "android.intent.action.VIEW", "-d", "http://luzhuo.me/blog", (char *) NULL);

        // 打开Activity
        am start --user 0 -n me.luzhuo.ndkdemo/me.luzhuo.ndkdemo.MainActivity
        execlp("am", "am", "start", "--user","0", "-n" , "me.luzhuo.ndkdemo/me.luzhuo.ndkdemo.MainActivity",(char *) NULL);

        // 打开Service
        am startservice --user 0 -n me.luzhuo.ndkdemome/me.luzhuo.ndkdemo.activity.MainService
      */
      ppid = getppid(); // 获取父进程id
      FILE* file;
      if(ppid == 1){
        file = fopen("/data/data/me.luzhuo.ndkdemo", "r");
        if(file == NULL){
          // 应用已被卸载, 打开网页
          execlp("am", "am", "start", "--user","0","-a", "android.intent.action.VIEW", "-d", "http://luzhuo.me/blog", (char *) NULL);
        }else{
          // 应用已停止运行, 打开Activity
          execlp("am", "am", "start", "--user","0", "-n" , "me.luzhuo.ndkdemo/me.luzhuo.ndkdemo.MainActivity",(char *) NULL);
        }
      }
    }
  }else{ } // 子进程创建失败
}

只需将start换成start service就能永久运行service了, 除非手机被重启, 当然这在Android5.0以下才有用.

更多的am相关命令参考命令相关文章.

猜你喜欢

转载自blog.csdn.net/Rozol/article/details/88322757