目录
上一篇博客已经讲了Python与C的交互,本篇主要将JNI的使用和在Android项目中嵌入Python解释器,实现Java与Python相互调用。
1 JNI
JNI,即Java Native Interface。它其实就是一套java与本地代码交互的接口或者说是一个协议。通俗的比喻,就是中国人讲中国话,日本人讲日本话,于是中国人碰到日本人,各说各话,无法交流。如果这个时候,中国人学会了英语,日本人也学会了英语,那么中国人日本人就可以用英语交流了,这就是要学习JNI的原因!Java语言无法直接调用C/C++代码,反之亦然,于是就有了JNI,帮助两者相互调用。当然,这一套机制并不是那么美好,总会有一些坑要踩,就像中国人跟日本人交流还需要借助英语,怎么看都有一些多余,我们直观的感觉就是,讲同一种语言更好。
在Java的JNI参考手册中,明确指出了使用JNI的一些缺陷,因此,在真实项目中,如果要使用JNI,尽量三思而后行。既然JNI不被推荐使用,那么学习JNI的意义是什么呢?作为一个Android平台的开发者,最幸福的事情就是系统开源,这个开源的系统实际上就是一个巨大的宝库,从底层到上层,可以汲取无数的知识,而在Android系统中,JNI技术是被大量使用的,要想深入的研究学习源码,JNI是必须要掌握的一步,因此,就先从Java语言的JNI基础开始吧!
JNI(Java Native Interface),是方便Java调用C、C++等Native代码所封装的一层接口,相当于一座桥梁。通过JNI可以操作一些Java无法完成的与系统相关的特性,尤其在图像和视频处理中大量用到。
NDK(Native Development Kit)是Google提供的一套工具,其中一个特性是提供了交叉编译,即C或者C++不是跨平台的,但通过NDK配置生成的动态库却可以兼容各个平台。比如C在Windows平台编译后生成.exe文件,那么源码通过NDK编译后可以生成在安卓手机上运行的二进制文件.so。
1.1 JNI编写流程
1.2 实例
开发工具:IDEA,JDK8,VS2019。
1.2.1 新建Java JNI接口
使用IDEA新建Java项目“jni_demo”,创建package "com.jni",Java Class “JniDemo”。
public class JniDemo {
//方法一 返回名称
public native String getName();
//方法二 传递一个参数
public native void sayAWord(String prompt);
}
1.2.2 生成Java文件对应的C++ (.h)头文件
(1)第一种方法 直接用jvm命令运行
现在IDEA中build project。
没有错误,会在项目根目录/bin/com/jni下生成JniDemo.class。
在控制台输入
javah -jni com.jni.JniDemo
(2)第二种方法 在IDEA中配置External Tools
IDEA中点击File->Setting->External Tools 点击+,创建“javah”。
- Name:
javah
(可随意指定) - Program:
D:\Installed\Java\jdk1.8.0_251\bin\javah.exe
(javah所在的目录) - Arguments:
-jni -classpath $OutputPath$ $FileClass$
- Working directory:
$ProjectFileDir$
在Java文件上右击点击External Tools->javah生成头文件。
1.2.3 VS2019中实现.h文件生成dll
(1)创建动态链接库工程,工程名为Dll3
对运行环境进行重新设置
右键项目,添加包含目录
添加包含路径:
- D:\Program Files\Java\jdk1.8.0_251\include; //jni.h所在的目录
- D:\Program Files\Java\jdk1.8.0_251\include\win32;
(2)创建com_jni_JniDemo.cpp文件
#include "com_jni_JniDemo.h"
#include <iostream>
using namespace std;
JNIEXPORT jstring JNICALL Java_com_jni_JniDemo_getName
(JNIEnv* env, jobject obj)
{
std::cout << "You are the apple of mine" << std::endl;
// 返回一个字符串
const char* tmpstr = "return string succeeded";
// 要将char *生成一个jstring类型的对象,必须通过如下方式
jstring rtstr = env->NewStringUTF(tmpstr);
return rtstr;
}
JNIEXPORT void JNICALL Java_com_jni_JniDemo_sayAWord
(JNIEnv* env, jobject obj, jstring prompt)
{
const char* str;
// 作为参数的prompt不能直接被C++程序使用,先做了如下转换,将jstring类型变成一个char*类型
str = env->GetStringUTFChars(prompt, JNI_FALSE);
std::cout << str << std::endl;
env->ReleaseStringUTFChars(prompt, str);
}
将刚产生的头文件拷贝到该工程中。
(3)生成dll
点击运行若发现报错是否忘记了向源中添加“#include “pch.h”
右击项目属性-C/C++ 预编译头 不使用预编译头
1.2.4 Java调用JNI接口
package com.jni;
public class JniDemo {
public native String getName();
public native static void sayAWord();
static{
//注意把 .dll文件放入java.library.path这一jvm变量所指向的路径中
//System.out.println(System.getProperty("java.library.path"));
//第一种方式:System.loadLibrary("Dll3");
//第二种方法
System.load("F:\\Workspace\\VisualStudio\\Dll3\\x64\\Debug\\JniDemo.dll");
}
public static void main(String[] args) {
JniDemo jniDemo = new JniDemo();
System.out.println(jniDemo.getName());
jniDemo.sayAWord();
}
}
运行,输出
2 在Android中使用JNI(Java和C)
2.1 环境准备
从Crystax下载NDK,下载好NDK开发包,解压后放到本地:D:\Uninstalled\crystax-ndk-10.3.2。
配置crystax ndk环境,打开我的电脑-->鼠标右键选择属性-->高级系统设置-->环境变量--> 找到系统变量中的path变量进行编辑-->把NDK解压后文件目录主放到path最后面。
最后进入cmd命令窗口,执行ndk-build命令,如下图,则表示环境变量配置成功。
在Android Studio中不需要进行NDK的配置。
2.2 实例
2.2.1 新建项目并声明一个native方法
在Android Studio中创建一个新的Empty Activity项目,命名为c3。
在项目中创建一个Java Class,命名为JNIMethod。
package com.example.c3;
public class JNIMethod {
public native static String getStrFromJNI();
}
并在MainActivity中调用getStrFromJNI()
package com.example.c3;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView txtContent;
private Button btnStart;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txtContent = (TextView) findViewById(R.id.txt_content);
btnStart = (Button) findViewById(R.id.btn_start);
btnStart.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
txtContent.setText(JNIMethod.getStrFromJNI());
}
});
}
}
在res/layout/activity_main.xml中创建一个TextView和一个Button。
对应的代码如下:
<TextView
android:id="@+id/txt_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="waiting"
tools:layout_editor_absoluteX="234dp"
tools:layout_editor_absoluteY="38dp"
tools:text="waiting1" />
<Button
android:id="@+id/btn_start"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="JNI调用"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="16dp" />
2.2.2 通过javah命令生成.h头文件
在Android Studio的Terminal中,先进入要调用本地代码的类所在的目录,也就是在项目中的具体路径,比如这里是cd app\src\main\java
。然后通过javah命令生成该类的头文件,注意包名+类名。这里是javah -jni com.example.c3.JNIMethod
,生成头文件com_example_c3_JNIMethod.h。
查看头文件中的内容。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_c3_JNIMethod */
#ifndef _Included_com_example_c3_JNIMethod
#define _Included_com_example_c3_JNIMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_c3_JNIMethod
* Method: getStrFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_c3_JNIMethod_getStrFromJNI
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
首先引入jni.h,里面包含了很多宏定义及调用本地方法的结构体。重点是方法名的格式。这里的JNIEXPORT和JNICALL都是jni.h中所定义的宏。JNIEnv *表示一个指向JNI环境的指针,可通过它来访问JNI提供的接口方法。jclass也是jni.h中定义好的,类型是jobject,实际上是一个不确定类型的指针,这里用来接收Java中的this。实际编写中一般只要遵循Java_包名_类名_方法名
就好了。
2.2.3 实现JNI方法
上面生成的头文件只是定义了方法,并没有实现,就像一个接口一样。这里就用C实现这个无参的JNI方法。
(1)首先,在\c3\app目录下创建一个jni目录,也可以在其他目录创建,因为最终只需要编译好的动态库。把生成的头文件拷贝在jni目录下。
(2)在jni目录下创建Android.mk和demo.c文件。
Android.mk是一个makefile配置文件,安卓大量采用makefile进行自动化编译。LOCAL_MODULE定义的名称就是编译好的so库名称,比如这里是JNIMethod
,最终生成的动态链接库名称为libJNIMethod.so。 LOCAL_SRC_FILES表示参与编译的源文件名称,这里就是demo.c。
demo.c代码如下:
#include<jni.h>
jstring Java_com_example_c3_JNIMethod_getStrFromJNI(JNIEnv *env, jobject thiz){
return (*env)->NewStringUTF(env,"I am Str from jni libs!");
}
Android.mk代码如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := JNIMethod
LOCAL_SRC_FILES := demo.c
include $(BUILD_SHARED_LIBRARY)
LOCAL_PATH := $(call my-dir)设置Android.mk路径,include $(CLEAR_VARS)用于清理很多LOCAL_xxx。例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES等等。但不清理LOCAL_PATH。LOCALL_MODULE用于设置模块名称,LOCAL_SRC_FILES用于设置源文件。include $(BUILD_SHARED_LIBRARY) 表示编译成动态库。
这时候NDK编译生成的动态库会有四个CPU平台:arm64-v8a、armeabi-v7a、x86、x86_64。
(Optional)如果创建Application.mk就可以指定要生成的CPU平台,语法也很简单。
Application.mk代码如下:
APP_ABI := all
表示编译出所有的平台so文件,如:armeabi,x86,x86_64,armeabi-v7a等。
2.2.4 使用ndk-build生成.so库
编译所需文件都准备好后,切换到jni目录执行ndk-build命令。
正确执行后会在jni同级目录下生成libs和obj两个目录,libs下面就是生成的各个平台的so库文件。生成的中间临时的obj文件夹,和jni文件夹可以一起删除。将libs下面的内容复制到Android工程的libs目录下并在app的build.gradle的android下添加
sourceSets {
main() {
jniLibs.srcDirs = ["libs"]
}
}
让项目依赖上.so库,见上图中项目结构中的“jniLibs”。完成后运行项目查看运行结果。
启动项目,如上图左边所示。当点击“JNI调用”后,界面变更为右图。
3. 在Android中使用JNI(Java,C和Python)
(以下内容本人未实现,直接跳过)
配置好crystax ndk环境,并创建一个NDK项目,将crystax 包下面的 libpython3.5m.so拷贝至工程 lib/armeabi目录下。
3.1 为Java 编写 native方法
public class Util {
static {
try {
System.loadLibrary("jni_test");
} catch (Exception e) {
Log.e("jni_test", ""+e);
}
}
public static native int run(String path);
}
3.2 编写Jni代码
Jni_test.c
#include <Python.h>
#include <jni.h>
#include <android/log.h>
#define LOG(x) __android_log_write(ANDROID_LOG_WARN, "jni_test", (x))
/* --------------- */
/* Android log */
/* --------------- */
static PyObject *androidlog(PyObject *self, PyObject *args)
{
char *str;
if (!PyArg_ParseTuple(args, "s", &str))
return NULL;
LOG(str);
Py_RETURN_NONE;
}
static PyMethodDef AndroidlogMethods[] = {
{"log", androidlog, METH_VARARGS, "Logs to Android stdout"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef AndroidlogModule = {
PyModuleDef_HEAD_INIT,
"androidlog", /* m_name */
"Log for Android", /* m_doc */
-1, /* m_size */
AndroidlogMethods /* m_methods */
};
PyMODINIT_FUNC PyInit_androidlog(void)
{
return PyModule_Create(&AndroidlogModule);
}
void setAndroidLog()
{
// Inject bootstrap code to redirect python stdin/stdout
// to the androidlog module
PyRun_SimpleString(
"import sys\n" \
"import androidlog\n" \
"class LogFile(object):\n" \
" def __init__(self):\n" \
" self.buffer = ''\n" \
" def write(self, s):\n" \
" s = self.buffer + s\n" \
" lines = s.split(\"\\n\")\n" \
" for l in lines[:-1]:\n" \
" androidlog.log(l)\n" \
" self.buffer = lines[-1]\n" \
" def flush(self):\n" \
" return\n" \
"sys.stdout = sys.stderr = LogFile()\n"
);
}
/* --------------------------------------------------------------- */
/* 以上部分代码,为C语言为Python3编写拓展模块的标准模板代码。 */
/* 这套模板写起来有些繁琐,我们之前已经用SWIG自动化实现过拓展模块 */
/* 这部分代码主要功能是将Python的print输出连接到Android的Log输出中 */
/* 与我们要探讨的内容联系不大,无须感到困惑 */
/* ---------------------------------------------------------------- */
/* java对应的native方法 */
JNIEXPORT jint
JNICALL Java_com_example_jnitest_Util_run(JNIEnv *env, jobject obj, jstring path)
{
LOG("Initializing the Python interpreter");
const char *pypath = (*env)->GetStringUTFChars(env, path, NULL);
// Build paths for the Python interpreter
char paths[512];
snprintf(paths, sizeof(paths), "%s:%s/stdlib.zip", pypath, pypath);
// Set Python paths
wchar_t *wchar_paths = Py_DecodeLocale(paths, NULL);
Py_SetPath(wchar_paths);
PyImport_AppendInittab("androidlog", PyInit_androidlog);
Py_Initialize();//初始化Python解析器
if (!Py_IsInitialized())
{
LOG("Initialize failed");
return -1;
}
setAndroidLog();
PyRun_SimpleString("import test");
// Cleanup
(*env)->ReleaseStringUTFChars(env, path, pypath);
PyMem_RawFree(wchar_paths);
Py_Finalize();//释放Python解析器
return 0;
}
Android.mk
LOCAL_PATH := $(call my-dir)
CRYSTAX_PATH := D:/developer/AS_SDK/crystax-ndk-10.3.2
include $(CLEAR_VARS)
LOCAL_MODULE := jni_test
LOCAL_SRC_FILES := jni_test.c
LOCAL_LDLIBS := -llog
LOCAL_SHARED_LIBRARIES := python3.5m
include $(BUILD_SHARED_LIBRARY)
# Include libpython3.5m.so
include $(CLEAR_VARS)
LOCAL_MODULE := python3.5m
LOCAL_SRC_FILES := $(CRYSTAX_PATH)/sources/python/3.5/libs/$(TARGET_ARCH_ABI)/libpython3.5m.so
LOCAL_EXPORT_CFLAGS := -I $(CRYSTAX_PATH)/sources/python/3.5/include/python/
include $(PREBUILT_SHARED_LIBRARY)
3.3 编写Python代码
test.py
def sayHello():
print("Hello Android,from Python ")
sayHello()
3.4 项目调用
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AssetExtractor assetExtractor = new AssetExtractor(this);
assetExtractor.removeAssets("python");
assetExtractor.copyAssets("python");
final String pythonPath = assetExtractor.getAssetsDataDir() + "python";
AsyncTask.execute(new Runnable() {
@Override
public void run() {
Log.d("---> ",""+Util.run(pythonPath));
}
});
}
}
AssetExtractor.java 工具类,部分代码
public class AssetExtractor {
private final static String LOGTAG = "AssetExtractor";
private Context mContext;
private AssetManager mAssetManager;
public AssetExtractor(Context context) {
mContext = context;
mAssetManager = context.getAssets();
}
/**
* Copies the assets from the APK to the device.
*
* @param path: the source path
*/
public void copyAssets(String path) {
for (String asset : listAssets(path)) {
copyAssetFile(asset, getAssetsDataDir() + asset);
}
}
/**
* Removes recursively the assets from the device.
*
* @param path: the path to the assets folder
*/
public void removeAssets(String path) {
File file = new File(getAssetsDataDir() + path);
recursiveDelete(file);
}
}
方案一的实现到此结束,这种方案的好处是可以完全自主的控制Python解释器与java的交互,缺点是过于麻烦,且须精通Android的ndk编程以及C语言,否则出问题,不会解决。
与之相比,下一篇方案二的实现则简单得多。