Android 平台的Python-JNI方案(二)-详细版

2 篇文章 1 订阅

目录

1 JNI

1.1 JNI编写流程

1.2 实例

1.2.1 新建Java JNI接口

1.2.2 生成Java文件对应的C++ (.h)头文件

1.2.3 VS2019中实现.h文件生成dll

1.2.4 Java调用JNI接口

2 在Android中使用NDK

2.2 为Java 编写 native方法

4 编写Python代码

5 项目调用


上一篇博客已经讲了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语言,否则出问题,不会解决。
与之相比,下一篇方案二的实现则简单得多。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值