1.文章简介
本文想要介绍App开发过程中,JNI开发的基础流程;目的是为了获取App开发过程中的JNI开发相关技巧,并且有助于我们学习AOSP系统源码。
2.JNI简介
2.1.JNI定义
JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。 从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。
在安卓系统中,JNI定义了 Android 从受管理代码(使用 Java 或 Kotlin 编程语言编写)编译的字节码与原生代码(使用 C/C++ 编写)互动的方式。
2.2.准备的学习方法和参考资料
预期的学习方法:
通过谷歌官方网站和博客分享,学习JNI相关编码、编译、运行情况;
写一个简单Demo,尝试实现Java和native代码的相互调用和传值;
写一个稍微复杂的Demo-TimerTick秒表功能;
参考资料:
https://developer.android.google.cn/ndk/samples/sample_hellojni?hl=zh_cn
https://developer.android.google.cn/ndk/guides?hl=zh_cn
https://developer.android.google.cn/training/articles/perf-jni
Jni入门blog:https://www.jianshu.com/p/87ce6f565d37
Jni log打印:https://blog.csdn.net/u012005313/article/details/52059053
Jni线程:https://blog.csdn.net/weixin_34161064/article/details/87996322
Ndk下载:https://developer.android.google.cn/ndk/downloads?hl=zh_cn
NDK遇到的问题: https://blog.csdn.net/acm2008/article/details/44747787/
2.3.JNI与NDK的关系
JNI是Java环境提供的一套与native交互的工具,想要跟android建立关系,就需要NDK的支持。
原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,您可使用这些平台库管理原生 Activity 和访问实体设备组件,例如传感器和触摸输入。
NDK 可能不适合大多数 Android 编程初学者,这些初学者只需使用 Java 代码和框架 API 开发应用。然而,如果您需要实现以下一个或多个目标,那么 NDK 就能派上用场:
进一步提升设备性能,以降低延迟或运行游戏或物理模拟等计算密集型应用。
重复使用您自己或其他开发者的 C 或 C++ 库。
3.简单JNI Demo示例
本章涉及的代码,可以通过以下链接下载:
https://download.youkuaiyun.com/download/Railshiqian/39924879
3.1.简单JNI Demo描述
此处的JNI Demo会展示以下功能:
搭建NDK编译环境进行JNI开发,演示JNI静态方法注册功能,通过java接口从native接口获取一个字符串,并进行打印;
搭建NDK编译环境进行JNI开发,演示JNI动态方法注册功能,通过java接口调用native接口,传入一个long类型数据并进行打印;Java接口调用native接口后,native接口调用另一个java接口并传入一个字符串;
搭建Cmake编译环境进行JNI开发;
3.2.JNI Demo实施细节
以下内容会介绍JNI Demo的实施细节,包括以下几个步骤:
创建工程;
下载并配置NDK开发包;
编写Java和Native代码(包括静态绑定和动态绑定Java和Native方法);
把Native代码加入AndroidStudio编译apk的过程;
3.2.1.NDK编译环境静态绑定方法进行JNI开发
3.2.1.1.方法调用流程
3.2.1.2.创建一个App工程JniTest
3.2.1.3.下载并配置NDK路径
SDK Manager中下载NDK版本:
注意studio版本过低的话,可能不支持高版本的NDK。
为JniTest项目配置NDK版本,在Project Structure中,配置Android NDK Location选项,选择SDK路径下我们下载的NDK:
3.2.1.4.定义NdkStaticRegisterTools.java
NdkStaticRegisterTools.java用于跟native层进行交互,定义以下接口:
接口 描述
public static native String getStringFromNDK() 从native层获取一个字符串
在MainActivity的onCreate中调用此接口:
private void getStringFromNative() {
String stringFromNative = NdkStaticRegisterTools.getStringFromNDK();
Log.d("jniTest111", "stringFromNative:" + stringFromNative);
}
NdkStaticRegisterTools.java代码如下:
package com.demo.jnitest;
public class NdkStaticRegisterTools {
static {
System.loadLibrary("ndkdemotest-jni");
}
public static native String getStringFromNDK();
}
3.2.1.5.编译项目并生成native代码
选择build-Build APK(s)按钮编译可以生成APK,然后在Terminal终端环境中,执行以下操作,自动生成jni层的适配代码;
在此目录下JniTest/app/build/intermediates/classes/debug/, 执行
javah -jni com.demo.jnitest.NdkStaticRegisterTools命令,会在当前debug/目录生成com_demo_jnitest_NdkStaticRegisterTools.h文件;
在app/src/main/文件夹下创建jni文件夹,把此.h文件复制到jni文件夹中;
内容如下,可见javah此命令已经帮我们生成了jni对应的接口:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_demo_jnitest_NdkStaticRegisterTools */
#ifndef _Included_com_demo_jnitest_NdkStaticRegisterTools
#define _Included_com_demo_jnitest_NdkStaticRegisterTools
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_demo_jnitest_NdkStaticRegisterTools
* Method: getStringFromNDK
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_demo_jnitest_NdkStaticRegisterTools_getStringFromNDK
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
此时我们把.h文件复制复制到app/main/jni目录,并创建一个c++文件:
com_demo_jnitest_NdkStaticRegisterTools.cpp,修改为以下内容,在此方法中,返回了一个字符串"This str is from jni native world":
#include "com_demo_jnitest_NdkStaticRegisterTools.h"
JNIEXPORT jstring JNICALL Java_com_demo_jnitest_NdkStaticRegisterTools_getStringFromNDK
(JNIEnv *env, jclass clazz) {
return (env)->NewStringUTF("This str is from jni native world");
}
这样当java层调用此方式时,就会获得字符串"This str is from jni native world";
3.2.1.6.配置build.gradle,将JNI加入APK编译和打包
在app的build.gradle中添加jni编译配置,并在刚才创建的jni文件夹下创建Android.mk文件,这样编译apk时,.so文件就可以编译进apk文件中;
Android.mk内容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := ndkdemotest-jni
LOCAL_SRC_FILES := com_demo_jnitest_NdkStaticRegisterTools.cpp
LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY)
app的build.gradle中配置如下,新增了以下红色字体部分配置:
android {
compileSdkVersion 30
defaultConfig {
applicationId "com.chen.jnitimertick"
minSdkVersion 19
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk{
moduleName "jnitimertick"
abiFilters "armeabi", "armeabi-v7a", "x86", "x86_64", "arm64-v8a"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
sourceSets.main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
}
经过以上配置后,进行build APK编译出来的apk文件,就打包了各个cpu架构相关的so文件,可以解压缩apk进行确认。
Activity启动后,会调用NdkStaticRegisterTools.getStringFromNDK()方法,触发System.loadLibrary(“ndkdemotest-jni”);及调用native String getStringFromNDK();方法,就可以获得native层返回的字符串数据。
至此,NDK编译环境静态绑定方法进行JNI开发的Demo已完成。
3.2.2.NDK编译环境动态绑定方法进行JNI开发
3.2.2.1.JNI动态绑定和静态绑定的区别
此章节用来描述NDK编译环境中,动态绑定方法进行JNI开发的流程;
3.2.1章节静态绑定方法,需要native层声明一个固定的方法名称,此方法名称是JAVA_包名_类型_方法名的组合,特别长,且在java方法和native方法进行一一匹配时有额外的时间消耗;
动态绑定方法,对native层方法没有这样的要求,可以是任意的c/c++方法;但是需要重写一个系统方法。指定java方法和native方法的一一对应关系;
3.2.2.2.动态绑定示例代码方法调用流程
3.2.2.3.JNI动态绑定实施过程
实施过程跟3.2.1章节差别在于,不再需要根据java class生成.h文件,因为动态绑定jni方法不需要那样一串很长的方法名。
定义Java层接口NdkActiveRegisterTools .java,有两个native接口和一个等待native调用的接口:
package com.demo.jnitest;
import android.util.Log;
public class NdkActiveRegisterTools {
static {
System.loadLibrary("ndkdemotest-jni");
}
public static native void sayHelloStatic(long temp);
public static native void sayHiStatic(long temp);
public static void expectedCalledByNative(long temp) {
Log.d("jnitest111", "expectedCalledByNative response, temp:" + temp);
}
}
定义一个c++文件ndkdemo_active_register_test.cpp,定义几个c++接口,并通过重写JNI_OnLoad(JavaVM *vm, void *reserved)系统方法,调用env->RegisterNatives(clazz, gMethods, numMethods) 把java方法和c++接口进行一一对应。
内容如下:
//
// Created by chen on 21-10-19.
//
#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>
static const char *className = "com/demo/jnitest/NdkActiveRegisterTools";
static void sayHiNativeMethod(JNIEnv *env, jclass clazz, jlong handle) {
LOGI("native: say hi ### %ld", handle);
}
static void sayHelloNativeMethodCallJavaMethod(JNIEnv *env, jclass clazz, jlong handle) {
LOGI("native: say hello ###, handle:%ld", handle);
// call this java method.
char* methodName = "expectedCalledByNative";
jmethodID jmethodID1 = env->GetStaticMethodID(clazz, methodName, "(J)V");
(env)->CallStaticVoidMethod(clazz, jmethodID1, 2l);
}
// 声明JNINativeMethod数组
static JNINativeMethod gJni_Methods_table[] = {
{"sayHelloStatic", "(J)V", (void *) sayHelloNativeMethodCallJavaMethod},
{"sayHiStatic", "(J)V", (void *) sayHiNativeMethod},
};
#ifdef __cplusplus
extern "C" {
#endif
static int jniTestRegisterNativeMethods(JNIEnv *env, const char *className,
const JNINativeMethod *gMethods, int numMethods) {
jclass clazz;
LOGI("Registering %s natives\n", className);
clazz = (env)->FindClass(className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'\n", className);
return -1;
}
int result = 0;
//将java方法和native方法进行绑定
if ((env)->RegisterNatives(clazz, gMethods, numMethods) < 0) {
LOGE("RegisterNatives failed for '%s'\n", className);
result = -1;
}
(env)->DeleteLocalRef(clazz);
return result;
}
// system.loadLibrary时调用
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGI("enter jni_onload");
JNIEnv *env = NULL;
jint result = -1;
if ((vm)->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
jniTestRegisterNativeMethods(env, className, gJni_Methods_table,
sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
return JNI_VERSION_1_4;
}
#ifdef __cplusplus
}
#endif
这样就实现了jni接口,不再需要静态绑定,即可进行jni方法调用。
在Activity中,我们就可以进行下面两个方法的调用:
NdkActiveRegisterTools.sayHelloStatic(1L);
NdkActiveRegisterTools.sayHiStatic(1L);
如果有两个Java类需要进行动态绑定,那么需要定义两个className字符串和两个JNINativeMethod gJni_Methods_table[]数组,调用两次jniTestRegisterNativeMethods方法即可。
至此JNI方法动态绑定示例已完成。
3.2.3.Cmake编译环境进行JNI开发
Cmake方式编译方式跟NDK不一样,java和c++编码方式,跟NDK是一致的。
参考https://www.jianshu.com/p/b4431ac22ec2,不再赘述。
4.TimerTick示例
4.1.TimerTick描述
TimerTick秒表功能,在界面上提供两个按钮(开始秒表和结束秒表功能),点击开始秒表按钮,通过c++开启一个线程,每一秒调用一次java层代码,将秒表数据抛到App的Java层进行显示;
此demo工程可以通过以下链接下载:
https://download.youkuaiyun.com/download/Railshiqian/38011524
4.2.TimerTick逻辑流程图
4.3.TimerTick实施细节
创建工程,并配置NDK环境的流程,请参考3.2.1章节;
创建的工程名为JniTimerTickTest,packageName包名为com.chen.jnitimertick。
4.3.1.定义TimerTick.java和MainActivity
MainActivity定义两个Button按钮和一个TextView,两个Button用来启动和停止秒表,TextView显示秒表数据;
TimerTick.java用于跟native层进行交互,定义以下几个接口:
接口 描述
public synchronized void startTimer() 启动秒表方法,点击按钮时调用此方法;
public synchronized void releaseTimer() 停止秒表方法,点击按钮时调用此方法;
public void timerTick(long seconds) Native层调用此方法,把计时输出到Java层Activity进行显示;
private native void startTimerNative(); 通知Native层开启线程,并进行秒表计时;
private native void releaseTimerNative(long handle); 通知Native层停止秒表计时,并停止线程;
注意此处handle代表了native层的一个c++对象!
MainActivity代码如下:
package com.chen.jnitimertick;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends Activity implements View.OnClickListener {
TimerTick timerTick;
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.tv);
findViewById(R.id.btn_start_timer).setOnClickListener(this);
findViewById(R.id.btn_stop_timer).setOnClickListener(this);
timerTick = new TimerTick();
timerTick.setTimerListener(timerListener);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_start_timer:
timerTick.startTimer();
break;
case R.id.btn_stop_timer:
timerTick.releaseTimer();
break;
}
}
private TimerTick.ITimerListener timerListener = new TimerTick.ITimerListener() {
@Override
public void onTimerTick(long timer) {
Message message = handler.obtainMessage();
message.obj = timer;
handler.sendMessage(message);
}
};
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
long timer = (long) msg.obj;
textView.setText(timer + "");
}
};
}
TimerTick.java代码如下:
package com.chen.jnitimertick;
import android.util.Log;
public class TimerTick {
private ITimerListener mTimerListener;
private boolean isTimerStarted = false;
// Avoid native object to be release
public long mNativeTimerHandle = 0l;
static {
System.loadLibrary("jnitimertick");
}
public void setTimerListener(ITimerListener timerListener) {
mTimerListener = timerListener;
}
public synchronized void startTimer() {
if (isTimerStarted) {
Log.d("jniTest111", "already started");
return;
}
isTimerStarted = true;
startTimerNative();
}
public synchronized void releaseTimer() {
if (isTimerStarted) {
isTimerStarted = false;
releaseTimerNative(mNativeTimerHandle);
} else {
Log.d("jniTest111", "not call start, no need release");
}
}
private native void startTimerNative();
private native void releaseTimerNative(long handle);
// called from native.
public void timerTick(long temp) {
Log.d("jnitest111", "timerTick:" + temp);
mTimerListener.onTimerTick(temp);
}
public interface ITimerListener {
void onTimerTick(long timer);
}
}
activity_main.xml界面如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:hint="timer"
android:padding="10dp" />
<Button
android:id="@+id/btn_start_timer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="startTimer"
android:textSize="20sp" />
<Button
android:id="@+id/btn_stop_timer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="stopTimer"
android:textSize="20sp" />
</LinearLayout>
4.3.2.编译项目并生成native代码
选择build-Build APK(s)按钮编译可以生成APK,然后在Terminal终端环境中,执行以下操作,自动生成jni层的适配代码;
在此目录下JniTimerTickTest/app/build/intermediates/classes/debug/, 执行
javah -jni com.chen.jnitimertick.TimerTick命令,会在当前debug/目录生成com_chen_jnitimertick_TimerTick.h文件;
在app/src/main/文件夹下创建jni文件夹,把此.h文件复制到jni文件夹中;
内容如下,可见javah此命令已经帮我们生成了jni对应的接口:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_chen_jnitimertick_TimerTick */
#ifndef _Included_com_chen_jnitimertick_TimerTick
#define _Included_com_chen_jnitimertick_TimerTick
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_chen_jnitimertick_TimerTick
* Method: startTimerNative
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_chen_jnitimertick_TimerTick_startTimerNative
(JNIEnv *, jobject);
/*
* Class: com_chen_jnitimertick_TimerTick
* Method: releaseTimerNative
* Signature: (J)V
*/
JNIEXPORT void JNICALL Java_com_chen_jnitimertick_TimerTick_releaseTimerNative
(JNIEnv *, jobject, jlong);
#ifdef __cplusplus
}
#endif
#endif
此时我们把.h文件复制一份,并重命名为com_chen_jnitimertick_TimerTick.cpp,修改为以下内容,在这两个方法中,对接了java层的两个native方法:
#include <jni.h>
#include "com_chen_jnitimertick_TimerTick.h"
/*
* Class: com_chen_jnitimertick_TimerTick
* Method: startTimerNative
* Signature: ()V
*/
void Java_com_chen_jnitimertick_TimerTick_startTimerNative(JNIEnv *env, jobject obj) {
};
void Java_com_chen_jnitimertick_TimerTick_releaseTimerNative(JNIEnv *env, jobject obj, jlong handleValue) {
};
4.3.3.配置build.gradle,将JNI加入APK编译和打包
在app的build.gradle中添加jni编译配置,并在刚才创建的jni文件夹下创建Android.mk文件,这样编译apk时,.so文件就可以编译进apk文件中;
Android.mk内容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jnitimertick
LOCAL_SRC_FILES := com_chen_jnitimertick_TimerTick.cpp
LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY)
build.gradle中配置如下,新增了以下红色字体部分配置:
android {
compileSdkVersion 30
defaultConfig {
applicationId "com.chen.jnitimertick"
minSdkVersion 19
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk{
moduleName "jnitimertick"
abiFilters "armeabi", "armeabi-v7a", "x86", "x86_64", "arm64-v8a"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
sourceSets.main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
}
经过以上配置后,进行build APK编译出来的apk文件,就打包了各个cpu架构相关的so文件,可以解压缩apk进行确认。
4.3.4.补充JNI代码,实现秒表逻辑
以上流程,我们已经搭建好了JNI的框架,编译apk并运行app时,已经可以把so文件打包到apk文件中,并可以实现Java和native方法的调用。
现在需要补充秒表的功能,首先定义timer_tick相关cpp和.h文件;Timer_tick.h内容如下,其中定义了class TimerTick类,及类中的方法和成员变量,包括开始秒表和结束秒表的方法,计时数据long timer_seconds秒数,内部线程pthread_t pthread_local,和javaVM相关引用变量。
//
// Created by chen on 21-10-19.
//
#ifndef _Included_timer_tick
#define _Included_timer_tick
#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>
#include "timer_tick.h"
// 要调用的本地方法
void startTimerNative(JNIEnv *env, jobject obj);
// 要调用的本地方法
void releaseTimerNative(JNIEnv *env, jobject obj, jlong handleValue);
static const char *timer_tick_java_class = "com/chen/jnitimertick/TimerTick";
class TimerTick {
public:
TimerTick(JNIEnv *env, jobject thiz);
~TimerTick();
void startTick();
static void *tickTick(void *pHandle);
void stopTick();
long timer_seconds;
pthread_t pthread_local;
jfieldID nativeHandle;
jmethodID timerTickMethod;
JavaVM *local_java_v;
jobject mObject;
private:
int isTickStart;
};
#endif
timer_tick.cpp实现计时器功能:
//
// Created by chen on 21-10-19.
//
#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>
#include "timer_tick.h"
#include <pthread.h>
#include <unistd.h>
TimerTick::TimerTick(JNIEnv *env, jobject obj) {
jclass clazz = (env)->FindClass(timer_tick_java_class);
nativeHandle = (env)->GetFieldID(clazz, "mNativeTimerHandle", "J");
timerTickMethod = (env)->GetMethodID(clazz, "timerTick", "(J)V");
mObject = env->NewWeakGlobalRef(obj);
(env)->GetJavaVM(&(local_java_v));
}
TimerTick::~TimerTick() {
LOGD("delete TimerTick start");
timerTickMethod = NULL;
JNIEnv *jniEnv = NULL;
local_java_v->GetEnv((void **) &jniEnv, JNI_VERSION_1_4);
LOGD("myjvm->AttachCurrentThread");
local_java_v->AttachCurrentThread(&jniEnv, NULL);
jniEnv->SetLongField(mObject, nativeHandle, (jlong) 0L);
jniEnv->DeleteWeakGlobalRef(mObject);
nativeHandle = NULL;
LOGD("delete TimerTick end");
}
void TimerTick::startTick() {
LOGD("startTick");
pthread_create(&pthread_local, NULL, tickTick, this);
}
void *TimerTick::tickTick(void *pHandle) {
TimerTick *timerTick = (TimerTick *) (pHandle);
timerTick->timer_seconds = 0;
timerTick->isTickStart = 1;
JNIEnv *jniEnv = NULL;
timerTick->local_java_v->GetEnv((void **) &jniEnv, JNI_VERSION_1_6);
LOGD("myjvm->AttachCurrentThread");
timerTick->local_java_v->AttachCurrentThread(&jniEnv, NULL);
while (timerTick->isTickStart == 1) {
// calljava method
jniEnv->CallVoidMethod(
timerTick->mObject, timerTick->timerTickMethod,
timerTick->timer_seconds);
if (timerTick->timer_seconds >= LONG_MAX) {
timerTick->timer_seconds = 0;
} else {
timerTick->timer_seconds++;
}
sleep(1);
}
timerTick->local_java_v->DetachCurrentThread();
LOGD("TimerTick thread End");
return NULL;
}
void TimerTick::stopTick() {
LOGD("stopTick start");
isTickStart = -1;
void *status;
//pthread_kill(timerTick->pthread_local, SIGUSR1);
pthread_join(pthread_local, &status);
delete this;
LOGI("releaseTimerNative end");
}
在com_chen_jnitimertick_TimerTick.cpp的jni方法中调用timer_tick.cpp中的方法,启动秒表:
#include <jni.h>
#include "com_chen_jnitimertick_TimerTick.h"
#include "timer_tick.h"
void Java_com_chen_jnitimertick_TimerTick_startTimerNative(JNIEnv *env, jobject obj) {
LOGI("startTimerNatived");
TimerTick *timerTick = new TimerTick(env, obj);
(env)->SetLongField(obj, timerTick->nativeHandle, (jlong) timerTick);
timerTick->startTick();
LOGI("startTimerNatived, timerTick:%ld", timerTick);
};
void Java_com_chen_jnitimertick_TimerTick_releaseTimerNative(JNIEnv *env, jobject obj,
jlong handleValue) {
LOGI("releaseTimerNative start");
TimerTick *timerTick = reinterpret_cast<TimerTick *>(handleValue);
timerTick->stopTick();
LOGI("releaseTimerNative, end, timerTick:%ld", timerTick);
};
这样,在MainActivity点击时,会调用到native层TimerTick方法,启动秒表,并每一秒上传数据给界面进行显示。
TimerTick Demo示例到此完成。
5.总结
5.1.jni关键数据结构描述
可以参考以下链接:
https://developer.android.google.cn/training/articles/perf-jni?hl=zh_cn
以下是个表格,格式乱了~~
数据结构 获取方式 介绍
JNIEnv Java方法调用到Native方法时,会传入一个JNIEnv指针对象;
也可以通过JavaVM获取当前线程的JNIEnv对象:
JNIEnv *jniEnv = NULL;
jniEnv->GetEnv((void **) &jniEnv, JNI_VERSION_1_6); 线程相关,每个线程有自己的JNIEnv对象;
可以通过JNIEnv对象调用Java方法,获取Java
层的方法jmethodID,变量jfieldId和变量的值;
JavaVM 通过JNIEnv的方法获取:
JavaVM *local_java_v;
jniEnv->GetJavaVM(&(local_java_v)); 通过JavaVM对象,可以获取当前线程相关的JNIEnv对象,然后就可以调用Java方法和获取Java
层的数据。
jobject Java方法调用到Native方法时,会传入一个jobject对象,此对象是Java层调用native方法的那个Java对象:
startTimerNative(JNIEnv *env, jobject obj);
Jobject对象可以用以下方式进行赋值,否则在其他线程中无法使用:
jobject mObject = jniEnv->NewWeakGlobalRef(obj); Java对象在Native层的体现,Native调用Java层的非static方法时,需要传入一个jobject类型的变量;
jmethodID 获取方式如下:
jmethodID timerTickMethod = (env)->GetMethodID(clazz, “timerTick”, “(J)V”); Java方法在Native层的体现,Native调用Java层的方法时,需要传入一个jmethodID 类型的变量;
jni.h:
void CallVoidMethod(jobject obj, jmethodID methodID, …)
void CallStaticVoidMethod(jclass clazz, jmethodID methodID, …)
jfieldID 获取方式如下:
jfieldID nativeHandle = (env)->GetFieldID(clazz, “mNativeTimerHandle”, “J”);
Native层在获取/设置Java层的变量数据时,需要此变量;
Jni.h:
jlong GetLongField(jobject obj, jfieldID fieldID);
void SetObjectField(jobject obj, jfieldID fieldID, jobject value);
5.2.java调用c++方法的方式
Java调用C++方法,需要以下两个要素:
Java中调用System.loadLibrary(so文件的名字),注意如果so以lib开头,如libAAA.so,则只需要System.loadLibrary(“AAA”);
so文件中按照静态/动态绑定方式注册Native方法;
Java层方法是static类型的,那么native层方法中的变量是jclass,如:
public static native void sayHiStatic(long temp);
static void sayHiNativeMethod(JNIEnv *env, jclass clazz, jlong handle);
Java层方法是非static类型的,那么native层方法中的变量是jobject,如:
private native void startTimerNative();
void Java_com_chen_jnitimertick_TimerTick_startTimerNative(JNIEnv *env, jobject obj);
5.3.C++调用java的方式
C++程序可以通过JNIEnv对象调用到java层代码,这些方法声明在jni.h中,如:
jobject (CallObjectMethod)(JNIEnv, jobject, jmethodID, …);
jboolean (CallBooleanMethod)(JNIEnv, jobject, jmethodID, …);
jobject (CallStaticObjectMethod)(JNIEnv, jclass, jmethodID, …);
jbyte (CallStaticByteMethod)(JNIEnv, jclass, jmethodID, …);
5.4.Java方法签名简介
参考https://www.jianshu.com/p/b71aeb4ed13d
Java中有方法重载的特性,多个方法可以有同一个方法名称,只需要有不同的返回值类型、参数个数和参数类型。
那么Native方法在调用Java方法,获取jmethodID时,不能只凭借“方法名称”来指定调用的方法,需要把方法返回值类型和参数个数,参数类型都考虑进来,这就需要“方法签名”的概念。
用以下获取jmethodID的代码来举例分析:
jmethodID timerTickMethod = jniEnv->GetMethodID(clazz, “timerTick”, “(J)V”);
Java代码示例:
// called from native.
public void timerTick(long temp) {
Log.d(“jnitest111”, “timerTick:” + temp);
mTimerListener.onTimerTick(temp);
}
我们可以看到,jniEnv->GetMethodID的第三个参数"(J)V",其实指向了Java层J类型参数,V类型返回值的这个方法!
那么这个J和V是如何定义的呢!
函数签名格式如下:
(第1个参数类型+第2个参数类型+第3个参数类型…)返回值类型。
当参数不是基本数据类型,是引用类型的时候,参数类型的模板为"L包名",其中包名的.(点)要换成"/";如java.lang.String类型变量,就是Ljava/lang/String,android.view.Menu类型变量就是Landroid/view/Menu。
基本数据类型的标志,和Java类型的对应关系如下图
类型标示 Java类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double
[ 数组
[D double[]
[Ljava/lang/Object String[]
那么回到我们的示例代码void timerTick(long temp),方法签名就是(J)V,也就是我们传入的第三个参数!
以下进行一些举例,来增加对方法签名的印象:
Java方法 方法签名
int test() ()I
double getBigger(double arg0, double arg1) (DD)D
String getName() ()Ljava/lang/String
void nDrawText(long nativeCanvas, String text, int start, int end, float x, float y, int flags, long nativePaint) (JLjava/lang/String;IIFFIJ)V
void nDrawText(long nativeCanvas, char[] text, int index, int count, float x, float y, int flags, long nativePaint); (J[CIIFFIJ)V
6.AOSP中的JNI实践
6.1.MessageQueue中的JNI实践
MessageQueue.java中的native方法:
/home/chen/disk2/project/aosp/q_aosp/frameworks/base/core/java/android/os/MessageQueue.java
package android.os;
public final class MessageQueue {
private long mPtr; // used by native code
private native static long nativeInit();
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
boolean enqueueMessage(Message msg, long when) {
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
}
Native方法nativeInit动态注册对应的Java方法
搜索MessageQueue_nativeInit,可以搜索到以下方法:
/home/chen/disk2/project/aosp/q_aosp/frameworks/base/core/jni/android_os_MessageQueue.cpp
// 方法动态注册的调用链为:AndroidRuntime.cpp->start->startReg(env)->register_jni_procs(gRegJNI, NELEM(gRegJNI),env)->gRegJNI[]->REG_JNI(register_android_os_MessageQueue)->gMessageQueueMethods
static const JNINativeMethod gMessageQueueMethods[] = {
/* name, signature, funcPtr */
{ "nativeInit", "()J", (void*)android_os_MessageQueue_nativeInit },
{ "nativeDestroy", "(J)V", (void*)android_os_MessageQueue_nativeDestroy },
{ "nativePollOnce", "(JI)V", (void*)android_os_MessageQueue_nativePollOnce },
{ "nativeWake", "(J)V", (void*)android_os_MessageQueue_nativeWake },
{ "nativeIsPolling", "(J)Z", (void*)android_os_MessageQueue_nativeIsPolling },
{ "nativeSetFileDescriptorEvents", "(JII)V",
(void*)android_os_MessageQueue_nativeSetFileDescriptorEvents },
};
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
if (!nativeMessageQueue) {
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
}
nativeMessageQueue->incStrong(env);
return reinterpret_cast<jlong>(nativeMessageQueue);
}
创建NativeMessageQueue对象,并将此对象的指针数据,保存到java层MessageQueue对象的mPtr中,以免局部变量NativeMessageQueue对象在方法离开时自动释放掉。
nativeWake(mPtr):
/home/chen/disk2/project/aosp/q_aosp/frameworks/base/core/jni/android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}
void NativeMessageQueue::wake() {
mLooper->wake();
}
/home/chen/disk2/project/aosp/q_aosp/system/core/libutils/Looper.cpp
void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ wake", this);
#endif
uint64_t inc = 1;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
....
}
}
以上逻辑的write方法,向文件描述符中写入一个1,代表唤醒此线程;
以下逻辑,是等待唤醒的阻塞位置,由Java层nativePollOnce方法调用到这里,没有消息处理的话就会阻塞到read函数;
/home/chen/disk2/project/aosp/q_aosp/system/core/libutils/Looper.cpp
void Looper::awoken() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ awoken", this);
#endif
uint64_t counter;
TEMP_FAILURE_RETRY(read(mWakeEventFd.get(), &counter, sizeof(uint64_t)));
}
6.2.View中的native实践
/home/chen/disk2/project/aosp/q_aosp/frameworks/base/graphics/java/android/graphics/Canvas.java
public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
@NonNull Paint paint) {
super.drawText(text, start, end, x, y, paint);
}
/home/chen/disk2/project/aosp/q_aosp/frameworks/base/graphics/java/android/graphics/BaseCanvas.java
public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
@NonNull Paint paint) {
if ((start | end | (end - start) | (text.length() - end)) < 0) {
throw new IndexOutOfBoundsException();
}
throwIfHasHwBitmapInSwMode(paint);
if (text instanceof String || text instanceof SpannedString ||
text instanceof SpannableString) {
nDrawText(mNativeCanvasWrapper, text.toString(), start, end, x, y,
paint.mBidiFlags, paint.getNativeInstance());
} else ...
}
private static native void nDrawText(long nativeCanvas, String text, int start, int end,
float x, float y, int flags, long nativePaint);
尝试搜索BaseCanvas_nDrawText字符串,没找到,然后尝试搜索nDrawText字符串:
/home/chen/disk2/project/aosp/q_aosp/frameworks/base/core/jni/android_graphics_Canvas.cpp
static const JNINativeMethod gDrawMethods[] = {
{"nDrawColor","(JII)V", (void*) CanvasJNI::drawColor},
{"nDrawColor","(JJJI)V", (void*) CanvasJNI::drawColorLong},
{"nDrawPaint","(JJ)V", (void*) CanvasJNI::drawPaint},
{"nDrawPoint", "(JFFJ)V", (void*) CanvasJNI::drawPoint},
{"nDrawPoints", "(J[FIIJ)V", (void*) CanvasJNI::drawPoints},
{"nDrawLine", "(JFFFFJ)V", (void*) CanvasJNI::drawLine},
{"nDrawLines", "(J[FIIJ)V", (void*) CanvasJNI::drawLines},
{"nDrawRect","(JFFFFJ)V", (void*) CanvasJNI::drawRect},
{"nDrawRegion", "(JJJ)V", (void*) CanvasJNI::drawRegion },
{"nDrawRoundRect","(JFFFFFFJ)V", (void*) CanvasJNI::drawRoundRect},
{"nDrawDoubleRoundRect", "(JFFFFFFFFFFFFJ)V", (void*) CanvasJNI::drawDoubleRoundRectXY},
{"nDrawDoubleRoundRect", "(JFFFF[FFFFF[FJ)V", (void*) CanvasJNI::drawDoubleRoundRectRadii},
{"nDrawCircle","(JFFFJ)V", (void*) CanvasJNI::drawCircle},
{"nDrawOval","(JFFFFJ)V", (void*) CanvasJNI::drawOval},
{"nDrawArc","(JFFFFFFZJ)V", (void*) CanvasJNI::drawArc},
{"nDrawPath","(JJJ)V", (void*) CanvasJNI::drawPath},
{"nDrawVertices", "(JII[FI[FI[II[SIIJ)V", (void*)CanvasJNI::drawVertices},
{"nDrawNinePatch", "(JJJFFFFJII)V", (void*)CanvasJNI::drawNinePatch},
{"nDrawBitmapMatrix", "(JJJJ)V", (void*)CanvasJNI::drawBitmapMatrix},
{"nDrawBitmapMesh", "(JJII[FI[IIJ)V", (void*)CanvasJNI::drawBitmapMesh},
{"nDrawBitmap","(JJFFJIII)V", (void*) CanvasJNI::drawBitmap},
{"nDrawBitmap","(JJFFFFFFFFJII)V", (void*) CanvasJNI::drawBitmapRect},
{"nDrawBitmap", "(J[IIIFFIIZJ)V", (void*)CanvasJNI::drawBitmapArray},
{"nDrawText","(J[CIIFFIJ)V", (void*) CanvasJNI::drawTextChars},
{"nDrawText","(JLjava/lang/String;IIFFIJ)V", (void*) CanvasJNI::drawTextString},
{"nDrawTextRun","(J[CIIIIFFZJJ)V", (void*) CanvasJNI::drawTextRunChars},
{"nDrawTextRun","(JLjava/lang/String;IIIIFFZJ)V", (void*) CanvasJNI::drawTextRunString},
{"nDrawTextOnPath","(J[CIIJFFIJ)V", (void*) CanvasJNI::drawTextOnPathChars},
{"nDrawTextOnPath","(JLjava/lang/String;JFFIJ)V", (void*) CanvasJNI::drawTextOnPathString},
};
static void drawTextString(JNIEnv* env, jobject, jlong canvasHandle, jstring strObj,
jint start, jint end, jfloat x, jfloat y, jint bidiFlags,
jlong paintHandle) {
ScopedStringChars text(env, strObj);
Paint* paint = reinterpret_cast<Paint*>(paintHandle);
const Typeface* typeface = paint->getAndroidTypeface();
const int count = end - start;
// drawTextString and drawTextChars doesn't use context info
get_canvas(canvasHandle)->drawText(
text.get() + start, count, // text buffer
0, count, // draw range
0, count, // context range
x, y, // draw position
static_cast<minikin::Bidi>(bidiFlags), *paint, typeface, nullptr /* measured text */);
}
AOSP中使用jni的demo示例到此结束。
7.遇到的问题
1,新建studio项目,遇到无法生成gradle项目,报错如下:
Error:A problem occurred configuring project ‘:app’.
需要确认你安装了ndk,如果没有安装那么会报这个错误。或者ndk配置路径错误。
2,出现以下错误:Invalid revision: 3.18.1-g262b901
错误是由于CMake版本过高造成的
在SDK Manager中,卸载高版本,再下载个低版本CMake即可,比如3.6版本。
3,在Native文件中无法使用c++函数,编译错误
错误原因是文件的命名,.c和.cpp文件编译链接的库不一致,发现.c文件无法访问c++的函数,将.c改为.cpp解决编译问题。